簡體   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