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

111

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

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

Так что у меня много вопросов:

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

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

Копелица
источник
Мы можем переопределить Spring Boot BasicErrorController. Я писал об этом здесь: naturalprogrammer.com/blog/1685463/…
Санджай,

Ответы:

87

Вот что я сделал:

Я прочитал основы о фильтрах здесь , и я понял, что мне нужно , чтобы создать пользовательский фильтр , который будет первым в цепочке фильтров и попробовать поймать поймать все исключения времени выполнения , которые могут возникнуть там. Затем мне нужно создать 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?
Шива Кумар
1
Класс @Shivakumar ErrorResponse, вероятно, представляет собой простой DTO с простыми свойствами кода / сообщения.
ratijas
26

Я хотел предложить решение, основанное на ответе @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)
            (...)
    }

}
ssc-hrep3
источник
19

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

Раф
источник
10
Что ж, вы можете поделиться своим собственным решением, которое звучит как способ сделать это :)
Раф
1
Если вы хотите избежать подключения контроллера к вашему фильтру (что, как я полагаю, имеет в виду @ Bato-BairTsyrenov), вы можете легко извлечь логику, в которой вы создаете 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 без изменений.

Есть ли причины, по которым это на самом деле плохая идея?

AndyB
источник
1
Спасибо за тестирование этого решения, но в моем случае все работает отлично.
Maciej
одно чистое и простое дополнение для @Override public String getErrorPath() { return null; }
Spring Boot 2.0+, которое
вы можете использовать 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.";
    }
}
Holmis83
источник
Но в остальном api перенаправление с указанием местоположения не является хорошим решением.
jmattheis
@jmattheis Вышеупомянутое не является перенаправлением.
holmis83
Правда, я видел местоположение и подумал, что это имеет какое-то отношение к местоположению http. Тогда это то, что мне нужно (:
jmattheis
Не могли бы вы добавить конфигурацию Java, эквивалентную web.xml, если она существует?
k-den
1
@ k-den Я думаю, что в текущей спецификации нет эквивалента конфигурации Java, но вы можете смешивать web.xml и конфигурацию Java.
holmis83
5

Это мое решение, переопределив Spring Boot / обработчик ошибок по умолчанию

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

}
Walv
источник
влияет ли это на автоконфигурацию?
Samet Baskıcı
Обратите внимание, что 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

Вы можете использовать следующий метод внутри блока catch:

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
}
user2669657
источник
Обработка исключения в CorsFilter работает, но не очень чисто. На самом деле мне действительно нужна обработка исключения для всех фильтров
Копелица 06
35
Бросок исключения из Filterможет не быть пойман, @ControllerAdviceпотому что в него может не попасть DispatcherServlet.
Тхань Нгуен Ван
-1

Для этого не нужно создавать собственный фильтр. Мы решили эту проблему, создав пользовательские исключения, которые расширяют ServletException (которое выбрасывается из метода doFilter, показанного в объявлении). Затем они перехватываются и обрабатываются нашим глобальным обработчиком ошибок.

редактировать: грамматика

бета-брэд
источник
Не могли бы вы поделиться фрагментом кода вашего глобального обработчика ошибок?
Нирадж Вернекар,
у меня это не работает. Я сделал собственное исключение, которое расширяет ServletException, добавил поддержку этого исключения в ExceptionHandler, но там оно не было перехвачено.
Маркс