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

我想使用通用的方法来管理5xx 的错误代码,具体来说就是在我的整个春季应用程序中数据库出现故障的情况。我想要一个漂亮的错误 json 而不是堆栈跟踪。

对于控制器,我有一个针对不同异常的 @ControllerAdvice类,这也捕获了 db 在请求中间停止的情况。但这还不是全部。我也碰巧有一个自定义的 CorsFilter扩展 OncePerRequestFilter和那里,当我调用 doFilter我得到的 CannotGetJdbcConnectionException,它将不会被管理的 @ControllerAdvice。我在网上看到了一些让我更困惑的东西。

所以我有很多问题:

  • 我需要实现自定义过滤器吗? 我找到了 ExceptionTranslationFilter,但这只处理 AuthenticationExceptionAccessDeniedException
  • 我想实现我自己的 HandlerExceptionResolver,但这使我怀疑,我没有任何自定义的异常管理,必须有一个比这更明显的方式。我还尝试添加 try/catch 并调用 HandlerExceptionResolver的一个实现(应该足够好了,我的异常没有什么特别的) ,但是在响应中没有返回任何东西,我得到的是一个状态200和一个空主体。

有什么好办法吗? 谢谢

175782 次浏览

这很奇怪,因为@ControllerAdvisory 应该可以工作,你捕捉到正确的异常了吗?

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

如果需要通用的方法,可以在 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.";
}
}

所以我就这么做了:

我阅读了有关过滤器 给你的基础知识,并且发现我需要创建一个自定义过滤器,它将位于过滤器链中的第一个,并且有一个 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>

当您想要测试应用程序的状态并且在出现问题时返回 HTTP 错误时,我建议使用一个过滤器。下面的过滤器处理所有 HTTP 请求。使用 javax 过滤器的 Spring 引导中的最短解决方案。

在实现中可以有各种条件。在我的例子中,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() {}
}

我自己也遇到过这个问题,我执行了下面的步骤来重用我的 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 控制器在正常情况下处理 Exception(例如,通常不会在 Filter 级别抛出的异常,我们希望用它来处理 Filter 中抛出的异常)

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

与那些希望对抛入过滤器中的 Exceptions使用 ExceptionController的用户共享解决方案。

这是我的解决方案,通过覆盖默认的 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) ;
}


}

所以,这里是我所做的基于上述答案的合并... 我们已经有了一个 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

有什么理由说明这是个坏主意吗?

只是为了补充提供的其他答案,因为我最近想要一个简单的 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";
}


}

我想提供一个基于 @ kopelitsa 的答案的解决方案。主要的区别是:

  1. 通过使用 HandlerExceptionResolver重用控制器异常处理。
  2. 在 XML 配置上使用 Java 配置

首先,您需要确保您有一个类来处理常规 RestController/Controller 中发生的异常(用 @RestControllerAdvice@ControllerAdvice注释的类和用 @ExceptionHandler注释的方法)。这将处理在控制器中发生的异常。下面是一个使用 RestControllerAdvisory 的示例:

@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 筛选器链中重用此行为,需要定义 Filter 并将其挂钩到安全配置中。过滤器需要将异常重定向到上面定义的异常处理。这里有一个例子:

@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)
(...)
}


}

在阅读了上述答案中建议的不同方法之后,我决定使用自定义过滤器来处理身份验证异常。我能够使用一个错误响应类使用以下方法处理响应状态和代码。

我创建了一个自定义过滤器,并使用 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;
}}

可以在 catch 块中使用以下方法:

response.sendError(HttpStatus.UNAUTHORIZED.value(), "Invalid token")

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

不需要为此创建自定义 Filter。我们通过创建扩展 ServletException 的自定义异常来解决这个问题(该异常是从 doFilter 方法引发的,如声明中所示)。然后由我们的全局错误处理程序捕获和处理这些错误。

编辑: 语法

我在 webflow 中遇到过同样的问题,主题是有人希望重新使用@ControllerAdvisory,你不想在 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)
}
}

在 Filters 中,我们没有具有 @ControllerAdvice@RestControllerAdvice的控件来处理执行身份验证时可能发生的异常。因为,DispatcherServlet 只有在 Controller 类命中之后才会出现。 因此,我们需要执行以下操作。

  1. 我们需要

    HttpServletResponse httpResponse = (HttpServletResponse)响应;

我们可以从 GenericFilterBean.java实现类的 public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)中传递“ response”对象。 2)我们可以使用下面的工具类来写或打印我们的错误 JSON 模型或字符串对象到 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;
}

派对迟到了,但我们也可以这样用:

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