it-roy-ru.com

Как управлять исключениями, добавленными в фильтры в Spring?

Я хочу использовать универсальный способ управления кодами ошибок 5xx, скажем, в частности, когда БД не работает во всем приложении Spring. Я хочу красивую ошибку JSON вместо стека трассировки. 

Для контроллеров у меня есть класс @ControllerAdvice для разных исключений, и это также ловит случай, когда БД останавливается в середине запроса. Но это еще не все. У меня также есть собственный CorsFilter, расширяющий OncePerRequestFilter, и когда я вызываю doFilter, я получаю CannotGetJdbcConnectionException, и он не будет управляться @ControllerAdvice. В Интернете я прочитал несколько вещей, которые только запутали меня. 

Поэтому у меня много вопросов:

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

Есть ли хороший способ справиться с этим? Спасибо

51
kopelitsa

Так вот что я сделал:

Я прочитал основы о фильтрах здесь и понял, что мне нужно создать пользовательский фильтр, который будет первым в цепочке фильтров и будет иметь ловушку try для перехвата всех исключений времени выполнения, которые могут там произойти. Затем мне нужно создать 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>
49
kopelitsa

Я сам столкнулся с этой проблемой и выполнил следующие шаги, чтобы повторно использовать мой 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 в нормальных случаях (т.е. исключения, которые обычно не генерируются на уровне фильтра, тот, который мы хотим использовать для исключений, генерируемых в фильтре) 

//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, брошенного в фильтр.

11
Raf

Если вам нужен общий способ, вы можете определить страницу ошибки в 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.";
    }
}
8
holmis83

Это мое решение, переопределив стандартный загрузчик Spring/обработчик ошибок

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) ;
    }

}
5
walv

Итак, вот что я сделал на основе объединения приведенных выше ответов ... У нас уже была 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 без изменений.

Любые причины, почему это на самом деле плохая идея?

5
AndyB

Если вы хотите проверить состояние приложения и в случае возникновения проблемы вернуть ошибку 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() {}
}
4
Cyva

Я хотел предложить решение, основанное на ответе @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 (RuntimeException e) {
            log.error("Spring Security Filter Chain RuntimeException:", 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)
            (...)
    }

}
1
ssc-hrep3

Это странно, потому что @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
}
1
user2669657

Просто чтобы дополнить другие прекрасные ответы, так как я недавно хотел использовать компонент single error/исключения в простом приложении 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";
  }

}
0
Tom Bunting