Як керувати винятками, викинутими у фільтри навесні?


106

Я хочу використовувати загальний спосіб управління кодами помилок 5xx, скажімо, конкретно про випадок, коли db знижується в усьому моєму весняному застосуванні. Я хочу досить json помилки замість сліду стека.

Для контролерів у мене є @ControllerAdviceклас для різних винятків, і це також сприймає випадок, коли db зупиняється посеред запиту. Але це не все. У мене також трапляється користувальницьке CorsFilterрозширення, OncePerRequestFilterі там, коли я дзвоню, doFilterя отримую, CannotGetJdbcConnectionExceptionі він не буде керований @ControllerAdvice. Я прочитав кілька речей в Інтернеті, які тільки збентежили мене.

Тож у мене багато питань:

  • Чи потрібно мені впроваджувати спеціальний фільтр? Я знайшов, ExceptionTranslationFilterале це лише ручки AuthenticationExceptionабо AccessDeniedException.
  • Я думав реалізувати свій власний HandlerExceptionResolver, але це змусило мене сумніватися, я не маю ніяких спеціальних винятків для управління, має бути більш очевидний спосіб, ніж цей. Я також спробував додати спробувати / catch та викликати реалізацію HandlerExceptionResolver(має бути досить хорошою, моє виняток - нічого особливого), але це не повертає нічого у відповідь, я отримую статус 200 та порожнє тіло.

Чи є якийсь хороший спосіб впоратися з цим? Дякую


Ми можемо замінити BasicErrorController Spring Boot. Я тут про це блогував
Санджай,

Відповіді:


83

Отже, це я зробив:

Я прочитав основи про фільтри тут , і я зрозумів, що мені потрібно , щоб створити користувальницький фільтр , який буде першим в ланцюжку фільтрів і спробувати зловити зловити всі виключення часу виконання , які можуть виникнути там. Тоді мені потрібно створити json вручну і помістити його у відповідь.

Отже, ось мій спеціальний фільтр:

public class ExceptionHandlerFilter extends OncePerRequestFilter {

    @Override
    public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        try {
            filterChain.doFilter(request, response);
        } catch (RuntimeException e) {

            // custom error response class used across my project
            ErrorResponse errorResponse = new ErrorResponse(e);

            response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
            response.getWriter().write(convertObjectToJson(errorResponse));
    }
}

    public String convertObjectToJson(Object object) throws JsonProcessingException {
        if (object == null) {
            return null;
        }
        ObjectMapper mapper = new ObjectMapper();
        return mapper.writeValueAsString(object);
    }
}

А потім я додав його у web.xml перед CorsFilter. І це працює!

<filter> 
    <filter-name>exceptionHandlerFilter</filter-name> 
    <filter-class>xx.xxxxxx.xxxxx.api.controllers.filters.ExceptionHandlerFilter</filter-class> 
</filter> 


<filter-mapping> 
    <filter-name>exceptionHandlerFilter</filter-name> 
    <url-pattern>/*</url-pattern> 
</filter-mapping> 

<filter> 
    <filter-name>CorsFilter</filter-name> 
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> 
</filter> 

<filter-mapping>
    <filter-name>CorsFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

Чи можете ви розмістити свій клас ErrorResponse?
Шива

@Shivakumar ErrorResponse клас - це, мабуть, простий DTO з простими властивостями коду / повідомлення.
ratijas

19

Я хотів надати рішення на основі відповіді @kopelitsa . Основні відмінності:

  1. Повторне використання керування винятком контролера за допомогою HandlerExceptionResolver.
  2. Використання конфігурації Java через конфігурацію XML

По-перше, вам потрібно переконатися, що у вас є клас, який обробляє винятки, що трапляються у звичайному RestController / Controller (клас, позначений за допомогою @RestControllerAdvice або @ControllerAdviceта методів (-ів), позначених @ExceptionHandler). Це обробляє ваші винятки, що виникають у контролері. Ось приклад використання RestControllerAdvice:

@RestControllerAdvice
public class ExceptionTranslator {

    @ExceptionHandler(RuntimeException.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ErrorDTO processRuntimeException(RuntimeException e) {
        return createErrorDTO(HttpStatus.INTERNAL_SERVER_ERROR, "An internal server error occurred.", e);
    }

    private ErrorDTO createErrorDTO(HttpStatus status, String message, Exception e) {
        (...)
    }
}

Щоб повторно використовувати цю поведінку у ланцюзі фільтрів Spring Security, потрібно визначити фільтр і приєднати його до своєї конфігурації безпеки. Фільтр повинен перенаправляти виняток до вищеописаної обробки виключень. Ось приклад:

@Component
public class FilterChainExceptionHandler extends OncePerRequestFilter {

    private final Logger log = LoggerFactory.getLogger(getClass());

    @Autowired
    @Qualifier("handlerExceptionResolver")
    private HandlerExceptionResolver resolver;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {

        try {
            filterChain.doFilter(request, response);
        } catch (Exception e) {
            log.error("Spring Security Filter Chain Exception:", e);
            resolver.resolveException(request, response, null, e);
        }
    }
}

Потім створений фільтр потрібно додати до SecurityConfiguration. Вам потрібно підключити його до ланцюжка дуже рано, оскільки всі винятки з попереднього фільтра не будуть зафіксовані. У моєму випадку доцільно було додати його перед LogoutFilter. Дивіться ланцюжок фільтрів за замовчуванням та їх порядок в офіційних документах . Ось приклад:

@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Autowired
    private FilterChainExceptionHandler filterChainExceptionHandler;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .addFilterBefore(filterChainExceptionHandler, LogoutFilter.class)
            (...)
    }

}

17

Я сам стикався з цією проблемою, і я виконав наведені нижче дії, щоб повторно використовувати моє ExceptionControllerописуване @ControllerAdviseдля Exceptionsкинутого в зареєстрований фільтр.

Очевидно, що існує багато способів обробки винятку, але, в моєму випадку, я хотів, щоб виняток оброблявся моїм, ExceptionControllerтому що я впертий, а також тому, що я не хочу копіювати / вставляти один і той же код (тобто у мене є обробка / реєстрація код в ExceptionController). Я хотів би повернути прекрасну JSONвідповідь так само, як і інші винятки, викинуті не з фільтра.

{
  "status": 400,
  "message": "some exception thrown when executing the request"
}

У будь-якому випадку, мені вдалося скористатися своїм, ExceptionHandlerі мені довелося зробити трохи додаткового, як показано нижче в кроках:

Кроки


  1. У вас є спеціальний фільтр, який може або не може викидати виняток
  2. У вас є контролер Spring, який обробляє винятки, використовуючи, @ControllerAdviseнаприклад, MyExceptionController

Зразок коду

//sample Filter, to be added in web.xml
public MyFilterThatThrowException implements Filter {
   //Spring Controller annotated with @ControllerAdvise which has handlers
   //for exceptions
   private MyExceptionController myExceptionController; 

   @Override
   public void destroy() {
        // TODO Auto-generated method stub
   }

   @Override
   public void init(FilterConfig arg0) throws ServletException {
       //Manually get an instance of MyExceptionController
       ApplicationContext ctx = WebApplicationContextUtils
                  .getRequiredWebApplicationContext(arg0.getServletContext());

       //MyExceptionHanlder is now accessible because I loaded it manually
       this.myExceptionController = ctx.getBean(MyExceptionController.class); 
   }

   @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse res = (HttpServletResponse) response;

        try {
           //code that throws exception
        } catch(Exception ex) {
          //MyObject is whatever the output of the below method
          MyObject errorDTO = myExceptionController.handleMyException(req, ex); 

          //set the response object
          res.setStatus(errorDTO .getStatus());
          res.setContentType("application/json");

          //pass down the actual obj that exception handler normally send
          ObjectMapper mapper = new ObjectMapper();
          PrintWriter out = res.getWriter(); 
          out.print(mapper.writeValueAsString(errorDTO ));
          out.flush();

          return; 
        }

        //proceed normally otherwise
        chain.doFilter(request, response); 
     }
}

А тепер зразок Spring Controller, який обробляє Exceptionв звичайних випадках (тобто винятки, які зазвичай не кидаються на рівні Filter, той, який ми хочемо використовувати для виключень, викинутих у Filter)

//sample SpringController 
@ControllerAdvice
public class ExceptionController extends ResponseEntityExceptionHandler {

    //sample handler
    @ResponseStatus(value = HttpStatus.BAD_REQUEST)
    @ExceptionHandler(SQLException.class)
    public @ResponseBody MyObject handleSQLException(HttpServletRequest request,
            Exception ex){
        ErrorDTO response = new ErrorDTO (400, "some exception thrown when "
                + "executing the request."); 
        return response;
    }
    //other handlers
}

Спільний доступ до рішення з тими, хто бажає використовувати ExceptionControllerдля Exceptionsкинутого у фільтр.


10
Що ж, ви можете поділитися власним рішенням, яке звучить як це зробити :)
Раф

1
Якщо ви хочете, щоб уникнути включення контролера у ваш фільтр (на який я припускаю @ Бато-БаірЦиренов, я припускаю), ви можете легко витягнути логіку, коли ви створюєте ErrorDTO у своєму власному @Componentкласі та використовувати його у фільтрі та в контролер.
Рюдігер Шульц

1
Я не з вами повністю згідний, тому що вводити певний контролер у ваш фільтр не надто чисто.
PSV

Як було сказано в answerцьому, це один із способів! Я не стверджував, що це найкращий спосіб. Дякую за те, що ви поділилися своєю турботою @psv Я впевнений, що громада буде вдячна за рішення, яке ви маєте на увазі :)
Раф

12

Отже, ось що я зробив, грунтуючись на об'єднанні вищезазначених відповідей ... Ми вже мали GlobalExceptionHandlerанотацію, @ControllerAdviceі я також хотів знайти спосіб повторного використання цього коду для обробки винятків, які надходять із фільтрів.

Найпростіше рішення, яке я міг знайти, - це залишити обробник винятків у спокої та застосувати контролер помилок таким чином:

@Controller
public class ErrorControllerImpl implements ErrorController {
  @RequestMapping("/error")
  public void handleError(HttpServletRequest request) throws Throwable {
    if (request.getAttribute("javax.servlet.error.exception") != null) {
      throw (Throwable) request.getAttribute("javax.servlet.error.exception");
    }
  }
}

Отже, будь-які помилки, спричинені винятками, спочатку проходять через ErrorControllerобробку винятків і перенаправляються на оброблювач винятків шляхом повторного скидання їх із @Controllerконтексту, тоді як будь-які інші помилки (не викликані безпосередньо винятком) проходять через ErrorControllerбез змін.

Будь-які причини, чому це насправді погана ідея?


1
Завдяки тестуванню цього рішення, але в моєму випадку робота ідеальна.
Мацей

чисте і просте одне доповнення до весняного завантаження 2.0+, яке слід додати @Override public String getErrorPath() { return null; }
Fma

ви можете використовувати javax.servlet.RequestDispatcher.ERROR_EXCEPTION замість "javax.servlet.error.exception"
Маркс

9

Якщо ви хочете загальний спосіб, ви можете визначити сторінку помилки в web.xml:

<error-page>
  <exception-type>java.lang.Throwable</exception-type>
  <location>/500</location>
</error-page>

І додайте картографування у Spring MVC:

@Controller
public class ErrorController {

    @RequestMapping(value="/500")
    public @ResponseBody String handleException(HttpServletRequest req) {
        // you can get the exception thrown
        Throwable t = (Throwable)req.getAttribute("javax.servlet.error.exception");

        // customize response to what you want
        return "Internal server error.";
    }
}

Але в іншому випадку перенаправлення api з місцеположенням не є хорошим рішенням.
jmattheis

@jmattheis Наведене вище не є перенаправленням.
holmis83

Щоправда, я побачив місцеположення і подумав, що воно має щось із темою http. Тоді це те, що мені потрібно (:
jmattheis

Чи можете ви додати конфігурацію Java, еквівалентну web.xml, якщо така існує?
k-den

1
@ k-den Немає еквіваленту конфігурації Java в поточній специфікації, але я можу змішати web.xml та Java config.
holmis83

5

Це моє рішення, замінивши за замовчуванням обробник Spring Boot / error

package com.mypackage;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.web.ErrorAttributes;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;

/**
 * This controller is vital in order to handle exceptions thrown in Filters.
 */
@RestController
@RequestMapping("/error")
public class ErrorController implements org.springframework.boot.autoconfigure.web.ErrorController {

    private final static Logger LOGGER = LoggerFactory.getLogger(ErrorController.class);

    private final ErrorAttributes errorAttributes;

    @Autowired
    public ErrorController(ErrorAttributes errorAttributes) {
        Assert.notNull(errorAttributes, "ErrorAttributes must not be null");
        this.errorAttributes = errorAttributes;
    }

    @Override
    public String getErrorPath() {
        return "/error";
    }

    @RequestMapping
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest aRequest, HttpServletResponse response) {
        RequestAttributes requestAttributes = new ServletRequestAttributes(aRequest);
        Map<String, Object> result =     this.errorAttributes.getErrorAttributes(requestAttributes, false);

        Throwable error = this.errorAttributes.getError(requestAttributes);

        ResponseStatus annotation =     AnnotationUtils.getAnnotation(error.getClass(), ResponseStatus.class);
        HttpStatus statusCode = annotation != null ? annotation.value() : HttpStatus.INTERNAL_SERVER_ERROR;

        result.put("status", statusCode.value());
        result.put("error", statusCode.getReasonPhrase());

        LOGGER.error(result.toString());
        return new ResponseEntity<>(result, statusCode) ;
    }

}

чи впливає на автоматичну конфігурацію?
Самет Баскічі

Зауважте, що HandlerExceptionResolver не обов'язково обробляє виняток. Таким чином, він може потрапити як HTTP 200. Використання response.setStatus (..) перед викликом здається більш безпечним.
ThomasRS

5

Просто для доповнення інших чітких відповідей, оскільки я нещодавно хотів отримати єдиний компонент обробки помилок / винятків у простому додатку SpringBoot, що містить фільтри, які можуть викидати винятки, з іншими винятками, потенційно викинутими з методів контролера.

На щастя, здається, що ніщо не заважає вам поєднувати поради контролера з перекриттям обробника помилок Spring за замовчуванням, щоб забезпечити послідовне навантаження на відповідь, дозволити вам ділитися логікою, перевіряти винятки з фільтрів, виловлювати винятки, викинуті на службу, тощо.

Напр


@ControllerAdvice
@RestController
public class GlobalErrorHandler implements ErrorController {

  @ResponseStatus(HttpStatus.BAD_REQUEST)
  @ExceptionHandler(ValidationException.class)
  public Error handleValidationException(
      final ValidationException validationException) {
    return new Error("400", "Incorrect params"); // whatever
  }

  @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
  @ExceptionHandler(Exception.class)
  public Error handleUnknownException(final Exception exception) {
    return new Error("500", "Unexpected error processing request");
  }

  @RequestMapping("/error")
  public ResponseEntity handleError(final HttpServletRequest request,
      final HttpServletResponse response) {

    Object exception = request.getAttribute("javax.servlet.error.exception");

    // TODO: Logic to inspect exception thrown from Filters...
    return ResponseEntity.badRequest().body(new Error(/* whatever */));
  }

  @Override
  public String getErrorPath() {
    return "/error";
  }

}

3

Коли ви хочете перевірити стан програми та у випадку проблеми з поверненням помилки HTTP, я б запропонував фільтр. Фільтр нижче обробляє всі HTTP запити. Найкоротше рішення у Spring Boot з фільтром javax.

У реалізації можуть бути різні умови. У моєму випадку тест applicationManager тестує, чи програма готова.

import ...ApplicationManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class SystemIsReadyFilter implements Filter {

    @Autowired
    private ApplicationManager applicationManager;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {}

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        if (!applicationManager.isApplicationReady()) {
            ((HttpServletResponse) response).sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, "The service is booting.");
        } else {
            chain.doFilter(request, response);
        }
    }

    @Override
    public void destroy() {}
}

2

Прочитавши різні методи, запропоновані у вищезазначених відповідях, я вирішив обробити винятки аутентифікації за допомогою спеціального фільтра. Мені вдалося обробити стан відповіді та коди, використовуючи клас відповіді на помилку, використовуючи наступний метод.

Я створив спеціальний фільтр і змінив конфігурацію безпеки за допомогою методу addFilterAfter і додав після класу CorsFilter.

@Component
public class AuthFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    //Cast the servlet request and response to HttpServletRequest and HttpServletResponse
    HttpServletResponse httpServletResponse = (HttpServletResponse) response;
    HttpServletRequest httpServletRequest = (HttpServletRequest) request;

    // Grab the exception from the request attribute
    Exception exception = (Exception) request.getAttribute("javax.servlet.error.exception");
    //Set response content type to application/json
    httpServletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);

    //check if exception is not null and determine the instance of the exception to further manipulate the status codes and messages of your exception
    if(exception!=null && exception instanceof AuthorizationParameterNotFoundException){
        ErrorResponse errorResponse = new ErrorResponse(exception.getMessage(),"Authetication Failed!");
        httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        PrintWriter writer = httpServletResponse.getWriter();
        writer.write(convertObjectToJson(errorResponse));
        writer.flush();
        return;
    }
    // If exception instance cannot be determined, then throw a nice exception and desired response code.
    else if(exception!=null){
            ErrorResponse errorResponse = new ErrorResponse(exception.getMessage(),"Authetication Failed!");
            PrintWriter writer = httpServletResponse.getWriter();
            writer.write(convertObjectToJson(errorResponse));
            writer.flush();
            return;
        }
        else {
        // proceed with the initial request if no exception is thrown.
            chain.doFilter(httpServletRequest,httpServletResponse);
        }
    }

public String convertObjectToJson(Object object) throws JsonProcessingException {
    if (object == null) {
        return null;
    }
    ObjectMapper mapper = new ObjectMapper();
    return mapper.writeValueAsString(object);
}
}

Клас SecurityConfig

    @Configuration
    public class JwtSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    AuthFilter authenticationFilter;
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.addFilterAfter(authenticationFilter, CorsFilter.class).csrf().disable()
                .cors(); //........
        return http;
     }
   }

Клас ErrorResponse

public class ErrorResponse  {
private final String message;
private final String description;

public ErrorResponse(String description, String message) {
    this.message = message;
    this.description = description;
}

public String getMessage() {
    return message;
}

public String getDescription() {
    return description;
}}

0

Ви можете використовувати наступний метод всередині блоку вилову:

response.sendError(HttpStatus.UNAUTHORIZED.value(), "Invalid token")

Зауважте, що ви можете використовувати будь-який код HttpStatus та користувацьке повідомлення.


-1

Це дивно, тому що @ControllerAdvice має працювати, ви ловите правильний виняток?

@ControllerAdvice
public class GlobalDefaultExceptionHandler {

    @ResponseBody
    @ExceptionHandler(value = DataAccessException.class)
    public String defaultErrorHandler(HttpServletResponse response, DataAccessException e) throws Exception {
       response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
       //Json return
    }
}

Також спробуйте зловити цей виняток у CorsFilter та надішліть 500 помилок, щось подібне

@ExceptionHandler(DataAccessException.class)
@ResponseBody
public String handleDataException(DataAccessException ex, HttpServletResponse response) {
    response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
    //Json return
}

Обробка винятку в CorsFilter працює, але це не дуже чисто. Насправді те, що мені справді потрібно, - це обробка виключення для всіх фільтрів
kopelitsa

35
Кидок винятку з Filterне може бути спійманий, @ControllerAdviceоскільки в не може досягти DispatcherServlet.
Тхань Нгуен Ван

-1

Для цього не потрібно створювати спеціальний фільтр. Ми вирішили це, створивши спеціальні винятки, що розширюють ServletException (який викидається з методу doFilter, показаного в декларації). Потім їх вловлює та обробляє наш глобальний обробник помилок.

ред .: граматика


Ви не хочете поділитися фрагментом коду свого глобального обробника помилок?
Неєрай Вернекар

це не працює для мене. Я зробив спеціальний виняток, який розширює ServletException, додав підтримку цього винятку в ExceptionHandler, але він там не перехоплений.
Маркс
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.