繁体   English   中英

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

[英]How to manage exceptions thrown in filters in Spring?

我想使用通用方法来管理 5xx 错误代码,让我们具体说一下当数据库在我的整个 spring 应用程序中关闭时的情况。 我想要一个漂亮的错误 json 而不是堆栈跟踪。

对于控制器,我有一个@ControllerAdvice class 用于不同的异常,这也捕获了数据库在请求中间停止的情况。 但这并不是全部。 我也碰巧有一个自定义的CorsFilter扩展OncePerRequestFilter并且当我调用doFilter时我得到CannotGetJdbcConnectionException并且它不会由@ControllerAdvice管理。 我在网上读了几件事,这只会让我更加困惑。

所以我有很多问题:

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

有什么好的方法可以解决这个问题吗? 谢谢

所以这就是我做的:

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

我自己遇到了这个问题,并且我执行了以下步骤来重用我的ExceptionController ,该注释使用@ControllerAdvise注释,用于在已注册的Filter中抛出的Exceptions

显然有很多方法可以处理异常,但在我的情况下,我希望异常由我的ExceptionController处理,因为我很顽固,也因为我不想复制/粘贴相同的代码(即我有一些处理/在ExceptionController记录代码)。 我想返回漂亮的JSON响应,就像从Filter中抛出的其他异常一样。

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

现在是在正常情况下处理Exception的示例Spring Controller(例如,通常不会在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
}

与那些希望在Filter中使用ExceptionController for Exceptions人共享解决方案。

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

所以,这就是我基于上述答案的合并而做的...我们已经使用@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而不进行修改。

为什么这实际上是一个坏主意的任何原因?

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

}

当你想测试一个应用程序的状态,如果出现问题,返回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() {}
}

我想根据@kopelitsa的答案提供解决方案。 主要区别是:

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

首先,您需要确保您有一个类来处理常规RestController / Controller(使用@RestControllerAdvice@ControllerAdvice注释的类以及使用@RestControllerAdvice注释的方法)中发生的@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 (RuntimeException e) {
            log.error("Spring Security Filter Chain RuntimeException:", 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)
            (...)
    }

}

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

}

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

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

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

我在 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)
    }
}

在过滤器中,我们没有带有@ControllerAdvice@RestControllerAdvice的控件来处理我们在进行身份验证时可能发生的异常。 因为,DispatcherServlet 只会在 Controller class 命中后出现。 所以,我们需要做到以下几点。

  1. 我们需要有

    HttpServletResponse httpResponse = (HttpServletResponse) 响应;

"response" object we can pass it from public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) of GenericFilterBean.java implementation class. 2) We can use the below utility class to write or print our error JSON model or String object into ServletResponse output stream。

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

这很奇怪,因为@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
}

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

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

迟到了,但我们也可以这样使用它:

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

全局默认异常处理程序将仅在 Controller 或服务级别工作。 它们不会在过滤器级别工作。 我发现以下解决方案与 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\"}");

您不需要为此创建自定义过滤器。 我们通过创建扩展 ServletException 的自定义异常(从 doFilter 方法抛出,如声明中所示)解决了这个问题。 然后我们的全局错误处理程序会捕获并处理这些错误。

编辑:语法

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM