如何在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个回答

148
所以这是我所做的:
我阅读了有关过滤器的基础知识 此处,并想出了我需要创建一个自定义过滤器,它将是过滤器链中的第一个,并且将具有try catch以捕获可能在其中发生的所有运行时异常。然后,我需要手动创建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);
    }
}

然后我在 CorsFilter 之前的 web.xml 文件中添加了它。现在它已经生效了!

<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类吗? - Shiva kumar
1
@Shivakumar ErrorResponse类可能是一个简单的DTO,具有简单的code/message属性。 - ratijas
1
这不会重复使用ControllerAdvice/RestControllerAdvice,对吧? - Puce
1
@Puce,您无法在此上下文中利用@ControllerAdvice,因为“根据Java Servlet规范的规定,过滤器始终在调用Servlet之前执行[而] @ControllerAdvice仅对在DispatcherServlet内执行的控制器有用。” 您可以在此处阅读:https://dev59.com/n10a5IYBdhLWcg3wboMv#30345053 - ToFi

133

我希望提供一种基于 @kopelitsa的答案 的解决方案。 主要区别在于:

  1. 通过使用 HandlerExceptionResolver 重用控制器异常处理。
  2. 使用Java config而不是XML config。

首先,您需要确保有一个处理常规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)
            (...)
    }

}

5
非常感谢,你让我的一天变得美好。我花了很多时间来处理这件事,但最终看到这个结果非常棒。 - Az.MaYo
1
过滤器如何将异常重定向到上面定义的异常处理程序? - Yoshimitsu
1
@Yoshimitsu 它将异常传递给HandlerExceptionResolver,然后使用默认的Spring机制处理异常(即使用定义的@ExceptionHandler)。 - ssc-hrep3
2
这似乎是处理错误的最佳方式。我曾尝试手动编写响应,但时间戳未被正确序列化,导致部分 JSON 响应。手动调用异常解析器完美地解决了问题。 - David
6
很棒的回答,但请注意,如果未处理异常,响应状态将为200。为了避免这种情况,我会添加一个检查,如果resolveException返回null,则重新抛出捕获的异常。 - Tomasz Jagiełło
显示剩余5条评论

27

我自己遇到了这个问题,我执行了下面的步骤来重用我的 @ControllerAdvise 注解的 ExceptionController 来处理在注册的过滤器中抛出的 Exceptions

显然有许多方法来处理异常,但在我的情况下,我希望将异常处理由我的 ExceptionController 处理,因为我很固执,而且我不想复制/粘贴相同的代码(即我在 ExceptionController 中有一些处理/日志记录代码)。我希望像从非过滤器抛出的其他异常一样返回美观的 JSON 响应。

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

无论如何,我成功地利用了我的ExceptionHandler,并且我需要按照以下步骤进行一些额外的工作:

步骤


  1. 您有一个自定义过滤器,可能会抛出异常
  2. 您有一个使用@ControllerAdvise处理异常的Spring控制器,即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 控制器,它处理正常情况下的 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
好的,欢迎分享你自己的解决方案,听起来是解决它的方法 :) - Raf
2
如果你想避免在过滤器中连接控制器(我猜这就是@Bato-BairTsyrenov所指的),你可以轻松地将创建ErrorDTO的逻辑提取到自己的@Component类中,并在过滤器和控制器中使用它。 - Rüdiger Schulz
2
我不完全同意你的观点,因为在过滤器中注入特定控制器并不是很规范。 - psv
如在“回答”中提到的那样,这是其中一种方法!我并没有声称这是最好的方法。感谢@psv分享您的关注,我相信社区会欣赏您所考虑的解决方案 :) - Raf

18

基于上面回答的综合,我做了以下操作...我们已经有了一个用@ControllerAdvice注释过的GlobalExceptionHandler,我还想找到一种方法来重复使用该代码以处理来自过滤器的异常。

我能找到的最简单的解决方案是保留异常处理程序不变,并实现如下错误控制器:

@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
谢谢,现在正在测试这个解决方案,但在我的情况下完美运作。 - Maciej
1
Spring Boot 2.0+ 的一个简单的补充是,您应该添加以下代码:@Override public String getErrorPath() { return null; } - Fma
1
你可以使用 javax.servlet.RequestDispatcher.ERROR_EXCEPTION 代替 "javax.servlet.error.exception"。 - Marx
这个解决方案非常整洁,对我很有效。 - Ranjan Kumar

10
如果您想使用一种通用的方式,可以在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.";
    }
}

1
@jmattheis 上述内容不是重定向。 - holmis83
没错,我看到了“location”这个词,以为它与HTTP位置有关。那么这就是我需要的内容:( - jmattheis
如果存在Java配置的等效项,您能否将其添加到web.xml中? - k-den
1
@k-den 我认为当前规范中没有Java配置的等效项,但您可以混合使用web.xml和Java配置。 - holmis83
如果您使用spring-boot,您也可以将org.springframework.boot.web.servlet.error.ErrorAttributes注入到控制器中。这个bean已经有了提取异常和其他有用信息的方法。 - Ruslan Stelmachenko
显示剩余2条评论

8

为了补充其他优秀答案,我最近也想在一个简单的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";
  }

}

7

我通过覆盖默认的 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) ;
    }

}

它会影响任何自动配置吗? - Samet Baskıcı
请注意,HandlerExceptionResolver 不一定处理异常。因此,它可能会作为 HTTP 200 而继续执行。在调用之前使用 response.setStatus(..) 似乎更安全。 - ThomasRS

5

当您想要测试应用程序的状态,并在出现问题时返回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() {}
}

3
在阅读上述答案中提出的不同方法之后,我决定通过使用自定义过滤器来处理身份验证异常。我能够使用错误响应类处理响应状态和代码,使用以下方法:
我创建了一个自定义过滤器,并通过使用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);
}
}

安全配置类

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

错误响应类

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

2
您可以在 catch 块内使用以下方法:
response.sendError(HttpStatus.UNAUTHORIZED.value(), "Invalid token")

请注意,您可以使用任何HttpStatus代码和自定义消息。

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