如何在Spring中处理过滤器抛出的异常?

200
我想使用通用的方法来管理5xx错误代码,具体来说是当整个Spring应用程序中的数据库停止时。我希望得到一个漂亮的错误JSON而不是堆栈跟踪。 对于控制器,我有一个@ControllerAdvice类来处理不同的异常,这也会捕获在请求过程中数据库停止的情况。但这还不够。我还碰巧有一个自定义的CorsFilter扩展了OncePerRequestFilter,当我调用doFilter时,我会得到CannotGetJdbcConnectionException,而且它不会被@ControllerAdvice管理。我在网上读了几篇文章,只让我更加困惑。
因此,我有很多问题: 1. 我需要实现自定义过滤器吗?我找到了ExceptionTranslationFilter,但它只处理AuthenticationException或AccessDeniedException。 2. 我考虑实现自己的HandlerExceptionResolver,但这让我怀疑,我没有任何自定义异常要处理,肯定有比这更明显的方法。我还试图添加try/catch并调用HandlerExceptionResolver的实现(应该足够好,我的异常没有什么特别之处),但响应中没有返回任何内容,我得到了状态200和一个空主体。
有没有好的方法来解决这个问题?谢谢

1
我们可以重写Spring Boot的BasicErrorController。我在这里写了一篇博客:https://www.naturalprogrammer.com/blog/1685463/exception-handling-spring-boot-spring-lemon - Sanjay
19个回答

1

虽然有点晚了,但我们也可以这样使用:

@ApiIgnore
@RestControllerAdvice
public class ExceptionHandlerController {

    @Autowired
    private MessageSource messageSource;

并且在过滤器中:

@Component
public class MyFilter extends OncePerRequestFilter {

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


    @Override
    protected void doFilterInternal(HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain filterChain) {
            try {
              // Some exception
            } catch (Exception e) {
                this.exceptionResolver.resolveException(request, response, null, e);
            }
        }

1
在过滤器中,我们没有使用@ControllerAdvice@RestControllerAdvice控制器处理可能发生的身份验证异常的控制权。因为,只有当控制器类被调用后,DispatcherServlet才会出现。 所以,我们需要执行以下操作。
  1. 我们需要有

    HttpServletResponse httpResponse = (HttpServletResponse) response;

我们可以从GenericFilterBean.java实现类的public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)中传递“response”对象。 2)我们可以使用下面的实用程序类将错误JSON模型或String对象写入或打印到ServletResponse输出流中。
public static void handleUnAuthorizedError(ServletResponse response,Exception e)
{
    ErrorModel error = null;
    if(e!=null)
        error = new ErrorModel(ErrorCodes.ACCOUNT_UNAUTHORIZED, e.getMessage());
    else
        error = new ErrorModel(ErrorCodes.ACCOUNT_UNAUTHORIZED, ApplicationConstants.UNAUTHORIZED);
    
    JsonUtils jsonUtils = new JsonUtils();
    HttpServletResponse httpResponse = (HttpServletResponse) response;
    httpResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);
    httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
    try {
        httpResponse.getOutputStream().println(jsonUtils.convertToJSON(error));
    } catch (IOException ex) {
        ex.printStackTrace();
    }
}


public String convertToJSON(Object inputObj) {
        ObjectMapper objectMapper = new ObjectMapper();
        String orderJson = null;
        try {
            orderJson = objectMapper.writeValueAsString(inputObj);
        }
        catch(Exception e){
            e.printStackTrace();
        }
        return orderJson;
    }

OutputStream没有'println'方法,但是你可以通过传递一个字符串并调用string.getBytes()来使用'write'。 - Luca Pasini

1

我在webflux中遇到了同样的问题,按照某人正在尝试重复使用@ ControllerAdvice的主题,您不希望在webfilter中抛出直接异常或返回mono错误,而是希望将响应设置为mono错误。

    public class YourFilter implements WebFilter {


    @Override
    public Mono<Void> filter(final ServerWebExchange exchange, final WebFilterChain chain) {
        exchange.getResponse().writeWith(Mono.error(new YouException()));
        return chain.filter(exchange)
    }
}

0

根据Java Servlet规范,过滤器总是在Servlet被调用之前执行。现在@ControllerAdvice仅适用于在DispatcherServlet内执行的控制器。因此,使用过滤器并期望调用@ControllerAdvice或在这种情况下的@ExceptionHandler是不会发生的。

ssc-hrep3已经很好地解决了这个问题。

在Spring安全过滤器链中,您只需要定义一个新的FilterChainExceptionHandler过滤器,并将其挂钩到您的安全配置中。该过滤器需要将异常重定向到上述定义的异常处理程序。


0

全局默认异常处理程序仅适用于控制器或服务级别。它们不会在过滤器级别工作。我发现以下解决方案与Spring Boot Security-JWT过滤器配合良好。

https://www.jvt.me/posts/2022/01/17/spring-servlet-filter-error-handling/

以下是我添加的代码片段

    httpServletResponse.setContentType("application/json");
    httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
    httpServletResponse.getWriter().write("{\"error\":\"invalid_token\",\"error_description\":\"Invalid Token\"}");

0

过滤器:

@Autowired
UnauthenticatedRequestHandler unauthenticatedRequestHandler;

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

        if (!isValid) throw new BadCredentialsException("exception.jwt.inValid");

        //...

        if (!accessTokenDTO.isValid()) throw new BadCredentialsException("exception.token.inValid");

        //...

        if (!user.isEmailVerified()) throw new DisabledException("exception.user.notVerified");

        //...

        filterChain.doFilter(request, response);
    }
    catch (AuthenticationException ex){
        unauthenticatedRequestHandler.commence(request,response,ex);
    }

处理程序:

@Component
public class UnauthenticatedRequestHandler implements 
AuthenticationEntryPoint {
final Logger log = LoggerFactory.getLogger(getClass());
@Autowired
private MessageSource messageSource;
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
    log.error(authException.toString());

    String message = messageSource.getMessage(authException.getMessage(),null,null, Locale.getDefault());

    if(message == null) message = authException.getMessage();

    ResponseDTO<Object> res = ResponseDTO.builder()
            .status(false)
            .message(message)
            .build();

    String jsonString = new ObjectMapper().writeValueAsString(res);

    response.setContentType("application/json");
    response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
    response.getOutputStream().println(jsonString);
}

在安全配置中注册AuthenticationEntryPoint。
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .csrf().disable()
        .exceptionHandling().authenticationEntryPoint(unauthenticatedHandler).and()}

0

@ssc-hrep3

无法进行评论,只能回答问题:您的解决方案是最简洁的 - 但我会在答案中更改一件事。

有意的安全过滤器上的 @Component 注释将向应用程序过滤器链添加相同的过滤器。 您可以通过简单记录调用来验证此操作。

  • 我将从 SecurityConfiguration 中的简单方法构建此对象,而不是使用 @Bean 注释 - 确保仅在 Spring 安全过滤器链代理内执行此过滤器。
  • 或者,您不会将此过滤器专门配置为 spring 安全性,而是确保您的过滤器在 spring 安全过滤器链代理之前运行。

我猜您的实现是第一种变体。

而不是:

@Component
public class FilterChainExceptionHandler extends OncePerRequestFilter {...}

移除 @Component 注解:

public class FilterChainExceptionHandler extends OncePerRequestFilter {...}

在SecurityConfiguration类中:
private FilterChainExceptionHandler filterChainExceptionHandler(...args){
  return new FilterChainExceptionHandler(...args)
}

在SecurityFilterChain中的注册基本保持不变

.addFilterBefore(filterChainExceptionHandler(...args), LogoutFilter.class)

可以通过构造函数注入将所需的bean注入到SecurityConfiguration类中,然后在调用上面定义的工厂方法时使用。


我对不将过滤器添加到“ApplicationFilterChain”而仅添加到“SpringSecurityFilterChain”很感兴趣,但是如何实现呢? - user2087103

-1
您不需要为此创建自定义过滤器。我们通过创建扩展ServletException的自定义异常来解决这个问题(该异常从声明中显示的doFilter方法中抛出)。然后,这些异常被全局错误处理程序捕获和处理。
编辑:语法

你介意分享一下你的全局错误处理程序的代码片段吗? - Neeraj Vernekar
它对我不起作用。我创建了一个扩展ServletException的自定义异常,在ExceptionHandler中添加了对此异常的支持,但它并没有被拦截。 - Marx

-3

这很奇怪,因为@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
47
Filter 抛出的异常可能无法被 @ControllerAdvice 捕获,因为它可能无法到达 DispatcherServlet - Thanh Nguyen Van

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