Spring中针对过滤器的异常处理

21

我正在使用@ExceptionHandler来处理Spring中的异常。任何由控制器抛出的异常都会被用@ExceptionHandler注释的方法捕获,并采取相应的操作。为了避免为每个控制器编写@exceptionHandler,我正在使用@ControllerAdvice注解。

一切正常,如预期的那样。

现在我有一个过滤器(是的,不是拦截器,用于处理某些要求),它是使用DelegatingFilterProxy和ContextLoaderListener实现的。

当我从上述过滤器中抛出相同的异常时,它没有像控制器情况下那样被捕获。它直接被抛出给用户。

这里怎么了?


还要查看这个网址 https://dev59.com/BFsW5IYBdhLWcg3wyJp3 - kopelitsa
请看这里https://dev59.com/BFsW5IYBdhLWcg3wyJp3#43242424,我使用了一个解决方法来利用```@ExceptionHandler```处理在```Filter```中抛出的```Exception```。 - Raf
8个回答

29

在控制器解析之前,过滤器就已经起作用了,所以从过滤器中抛出的异常无法被控制器建议(Controller Advice)捕获。

过滤器是Servlet的一部分,不属于MVC堆栈。


3
我觉得是这样。有什么方法可以解决这个问题吗?我们可以在过滤器中自动装配Spring bean,这意味着我们可以利用Spring的依赖注入,不是整个栈,而是它的某些特性。一种解决方法(虽然应该避免使用)是,在出现异常时,过滤器捕获它们并将其抛到控制器中,然后再抛出异常。这只是为了保持一致性。 - Rohit Jain
也许你可以创建一个索引为1(第一个过滤器)的过滤器,捕获所有异常并手动触发控制器使用的异常处理程序。 - Andreas Wederbrand
你能详细解释一下“手动触发控制器使用的异常处理程序”吗?这是否意味着实际调用控制器的一个抛出所需异常的方法。 - Rohit Jain
你的controllerAdvice今天通过Spring自动处理异常,所以我想也许你可以将controllerAdvice自动装配到过滤器中,并手动调用handle方法。虽然我没有尝试过,但这值得一试。这样你就可以从控制器和过滤器中获得相同的异常处理。 - Andreas Wederbrand
深刻但不实用,因此被踩了。Richard在下面提供了较少的上下文,但是给出了可行的解决方案。 - joerx
如果我们将控制器建议的优先级设为最高,会怎样呢? - AmeeQ

16

由于异常不是从控制器而是从过滤器中引发的,所以 @ControllerAdvice 捕获不到它。

因此,在到处查找后我找到的最佳解决方案是为这些内部错误创建一个过滤器:

public class ExceptionHandlerFilter extends OncePerRequestFilter {
    @Override
    public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        try {
            filterChain.doFilter(request, response);

        } catch (JwtException e) {
            setErrorResponse(HttpStatus.BAD_REQUEST, response, e);
            e.printStackTrace();
        } catch (RuntimeException e) {
            e.printStackTrace();
            setErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, response, e);
        }
    }

    public void setErrorResponse(HttpStatus status, HttpServletResponse response, Throwable ex){
        response.setStatus(status.value());
        response.setContentType("application/json");
        // A class used for errors
        ApiError apiError = new ApiError(status, ex);
        try {
            String json = apiError.convertToJson();
            System.out.println(json);
            response.getWriter().write(json);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

将其添加到您的配置中,我正在使用WebSecurityConfigurerAdapter实现:

// Custom JWT based security filter
httpSecurity
        .addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);

// Custom Exception Filter for filter
httpSecurity
        .addFilterBefore(exceptionHandlerFilterBean(), JwtAuthenticationTokenFilter.class);

错误类:

public class ApiError {

    private HttpStatus status;
    @JsonSerialize(using = LocalDateTimeSerializer.class)
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy hh:mm:ss")
    private LocalDateTime timestamp;
    private String message;
    private String debugMessage;

    private ApiError() {
        timestamp = LocalDateTime.now();
    }
    public ApiError(HttpStatus status) {
        this();
        this.status = status;
    }

    public ApiError(HttpStatus status, Throwable ex) {
        this();
        this.status = status;
        this.message = "Unexpected error";
        this.debugMessage = ex.getLocalizedMessage();
    }

    public ApiError(HttpStatus status, String message, Throwable ex) {
        this();
        this.status = status;
        this.message = message;
        this.debugMessage = ex.getLocalizedMessage();
    }

    public String convertToJson() throws JsonProcessingException {
        if (this == null) {
            return null;
        }
        ObjectMapper mapper = new ObjectMapper();
        mapper.registerModule(new JavaTimeModule());
        mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

        return mapper.writeValueAsString(this);
    }

  //Setters and getters
}

ApiError中的if (this == null)有意义吗?我认为this永远不可能是null,就像这个答案所说的那样:https://dev59.com/IG865IYBdhLWcg3wfeqU#3789534 - Smile

4

由于该异常不是由控制器抛出,所以除非您提供一个自定义过滤器来委托您的异常,否则控制器建议不会捕获该异常。

您可以创建另一个过滤器来将您的异常委托给控制器建议。诀窍在于在所有其他自定义过滤器之前提供此新创建的过滤器。

例如:

  1. Create a new Filter to delegate your Exception

    @Component
    public class FilterExceptionHandler extends OncePerRequestFilter {
    
    private static Logger logger = LoggerFactory.getLogger(FilterExceptionHandler.class);
    
    @Autowired
    @Qualifier("handlerExceptionResolver")
    private HandlerExceptionResolver resolver;
    
    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        try {
            filterChain.doFilter(httpServletRequest, httpServletResponse);
        } catch (Exception ex) {
            logger.error("Spring Security filter chain exception : {}", ex.getMessage());
            resolver.resolveException(httpServletRequest, httpServletResponse, null, ex);
        }
    }}
    
  2. Create a custom exception if you need. In my case I'm creating an exception JukeBoxUnAuthorizedException

    public class JukeBoxUnauthorizedException extends RuntimeException {
      private static final long serialVersionUID = 3231324329208948384L;
      public JukeBoxUnauthorizedException() {
        super();
      }
    
      public JukeBoxUnauthorizedException(String message) {
        super(message);
      }
    
      public JukeBoxUnauthorizedException(String message, Throwable cause) {
        super(message, cause);
      }
    }
    
  3. Create a Controller Advice which would handle this exception

    @Order(Ordered.HIGHEST_PRECEDENCE)
    @ControllerAdvice
    public class RestExceptionHandler  {
      @ExceptionHandler(value = {JukeBoxUnauthorizedException.class})
      public ResponseEntity<JukeboxResponse> handleUnAuthorizedException(JukeBoxUnauthorizedException exception) {
      return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(new ErrorResponse(exception.getMessage()));
      }
    }
    
  4. Add your exception delegation filter in SecurityConfigurtion. i.e in the configure(HttpSecurity http) method . please note that the exception delegating filter should be in the top of the hierarchy. It should be before all your custom filters

http.addFilterBefore(exceptionHandlerFilter, AuthTokenFilter.class);

将异常处理过滤器添加到认证令牌过滤器之前。

HandleExceptionResolver 对我来说始终为 null。 - BBacon
@BBacon 你也可以尝试使用SimpleMappingExceptionResolver,如此处所述 - https://spring.io/blog/2013/11/01/exception-handling-in-spring-mvc - emilpmp

3

你可能希望在Filter抛出异常时设置HTTP状态码吗? 如果是这样,只需按照以下方式设置状态:

HttpServletResponse response = (HttpServletResponse) res; response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);


2

以下是我在过滤器类中为抛出错误所做的操作:

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        if (req.getHeader("Content-Type") == null) {
            HttpServletResponse httpResponse = (HttpServletResponse) response;                
            httpResponse.sendError(HttpServletResponse.SC_BAD_REQUEST,
                   "Required headers not specified in the request");            
        }
        chain.doFilter(request, response);
    }

1
我使用REST API构建了我的应用程序,因此通过在可能引发异常的过滤器中捕获它并编写一些内容来解决了这个问题。请记住必须包含filterChain.doFilter(request, response);
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    try {
        // something may throw an exception
        filterChain.doFilter(request, response);
    } catch (Exception e) {
        // ResponseWrapper is a customized class
        ResponseWrapper responseWrapper = new ResponseWrapper().fail().msg(e.getMessage());
        response.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE);
        response.getWriter().write(JSON.toJSONString(responseWrapper));
    }
}

这是一种非常简单的方法,而不是创建单独的过滤器、处理程序等。 - Arjun Sankarlal

0
请查看下面的代码片段,它对我有效。
final HttpServletResponseWrapper wrapper = new 
HttpServletResponseWrapper((HttpServletResponse) res);    
wrapper.sendError(HttpServletResponse.SC_UNAUTHORIZED, "<your error msg>");    
res = wrapper.getResponse();

如果您正在过滤器中使用此代码,请添加一个返回语句,否则chain.doFilter(req,res)将覆盖此代码。

嘿,我已经编辑了这段代码,但显示的是其他用户的名字。实际上,我是在未登录的情况下编辑的代码。但是在登录后,它显示了@Sumner Evans的名字。 - Vijay Shegokar
@vijayshegokar,我接受了您的编辑,但进行了一些额外的改进。(请参见完整的编辑历史记录)。 - Sumner Evans
@SumnerEvans,感谢您的快速回复并接受我的编辑。不过我仍然认为我的名字应该显示在这里,因为我进行了编辑。 :) - Vijay Shegokar
@vijayshegokar,“x分钟前编辑”只显示最新编辑者。这就是StackOverflow的工作方式。在SO上有一些帖子有5个以上的合作者。如果所有人都被放在这些帖子上,那么所有编辑者的头像将会使它们难以阅读。 - Sumner Evans

0

如果像我一样,你被困在Spring 3.1(仅比@ControllerAdvice晚0.1个版本)中,你可以尝试我刚想出来的解决方案。


那么,你听说过异常处理器吗?如果没有,请在此处阅读:

@Component
public class RestExceptionResolver extends ExceptionHandlerExceptionResolver {

    @Autowired
    //If you have multiple handlers make this a list of handlers
    private RestExceptionHandler restExceptionHandler;
    /**
     * This resolver needs to be injected because it is the easiest (maybe only) way of getting the configured MessageConverters
     */
    @Resource
    private ExceptionHandlerExceptionResolver defaultResolver;

    @PostConstruct
    public void afterPropertiesSet() {
        setMessageConverters(defaultResolver.getMessageConverters());
        setOrder(2); // The annotation @Order(2) does not work for this type of component
        super.afterPropertiesSet();
    }

    @Override
    protected ServletInvocableHandlerMethod getExceptionHandlerMethod(HandlerMethod handlerMethod, Exception exception) {
        ExceptionHandlerMethodResolver methodResolver = new ExceptionHandlerMethodResolver(restExceptionHandler.getClass());
        Method method = methodResolver.resolveMethod(exception);
        if (method != null) {
            return new ServletInvocableHandlerMethod(restExceptionHandler, method);
        }
        return null;
    }

    public void setRestExceptionHandler(RestExceptionHandler restExceptionHandler) {
        this.restExceptionHandler = restExceptionHandler;
    }

    public void setDefaultResolver(ExceptionHandlerExceptionResolver defaultResolver) {
        this.defaultResolver = defaultResolver;
    }
}

那么一个示例处理器会长这样

@Component
public class RestExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    @ResponseBody
    public Map<String, Object> handleException(ResourceNotFoundException e, HttpServletResponse response) {
        Map<String, Object> error = new HashMap<>();
        error.put("error", e.getMessage());
        error.put("resource", e.getResource());
        return error;
    }
 }

当然你不会忘记注册你的 beens。

然后创建一个过滤器,在你想要的过滤器之前调用它(可选择全部)

然后在那个过滤器中

try{
   chain.doFilter(request, response);
catch(Exception e){
   exceptionResolver(request, response, exceptionHandler, e);
   //Make the processing stop here... 
   return; //just in case
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接