IT박스

Spring에서 필터에 던져진 예외를 관리하는 방법은 무엇입니까?

itboxs 2020. 10. 17. 10:07
반응형

Spring에서 필터에 던져진 예외를 관리하는 방법은 무엇입니까?


일반적인 방법을 사용하여 5xx 오류 코드를 관리하고 싶습니다. 특히 전체 스프링 응용 프로그램에서 db가 다운되는 경우를 예로 들어 보겠습니다. 스택 추적 대신 예쁜 오류 json을 원합니다.

컨트롤러의 @ControllerAdvice경우 다른 예외에 대한 클래스가 있으며 요청 중간에 db가 중지되는 경우도 포착됩니다. 그러나 이것이 전부는 아닙니다. 또한 사용자 정의는이 일이 CorsFilter확장 OncePerRequestFilter하고 내가 거기에 호출 할 때 doFilter내가 얻을 CannotGetJdbcConnectionException그리고 그것은에 의해 관리되지 않습니다 @ControllerAdvice. 나는 나를 더 혼란스럽게 만들었던 몇 가지를 온라인에서 읽었다.

그래서 많은 질문이 있습니다.

  • 사용자 지정 필터를 구현해야합니까? 나는 찾았 ExceptionTranslationFilter지만 이것은 AuthenticationException또는 AccessDeniedException.
  • 내 자신의을 구현하려고 생각 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);
    }
}

그런 다음 web.xml에 CorsFilter. 그리고 작동합니다!

<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주석이 달린 등록 된 필터 @ControllerAdviseExceptions던져졌습니다.

예외를 처리하는 방법에는 분명히 여러 가지가 있지만, 제 경우에는 ExceptionController고집스럽고 동일한 코드를 복사 / 붙여 넣기하고 싶지 않기 때문에 예외가 처리되기를 원했습니다 (예 : 일부 처리 / 로깅 코드 ExceptionController). JSON필터에서 던지지 않은 나머지 예외와 마찬가지로 아름다운 응답 을 반환하고 싶습니다.

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

어쨌든, 나는 내 기능을 사용할 수 ExceptionHandler있었고 아래 단계에 표시된 것처럼 약간의 추가 작업을 수행해야했습니다.

단계


  1. 예외를 발생하거나 발생하지 않을 수있는 사용자 지정 필터가 있습니다.
  2. @ControllerAdviseie MyExceptionController를 사용하여 예외를 처리하는 Spring 컨트롤러가 있습니다.

샘플 코드

//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 (예 : 일반적으로 필터 수준에서 throw되지 않는 예외, 필터에서 throw 된 예외에 사용하려는 예외)

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

필터 ExceptionControllerExceptions던져 사용하려는 사람들과 솔루션을 공유합니다 .


일반적인 방법을 원하면 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.";
    }
}

위의 답변을 합쳐서 제가 한 작업은 다음과 같습니다. 이미 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내에서 다시 throw하여 예외 처리기로 리디렉션되고 @Controller다른 오류 (예외로 직접 발생하지 않음)는 ErrorController수정없이 통과합니다 .

이것이 실제로 나쁜 생각 인 이유는 무엇입니까?


이것은 기본 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) ;
    }

}

응용 프로그램의 상태를 테스트하고 문제가 발생하면 HTTP 오류를 반환 할 때 필터를 제안합니다. 아래 필터는 모든 HTTP 요청을 처리합니다. javax 필터를 사용하는 Spring Boot의 가장 짧은 솔루션입니다.

구현에는 다양한 조건이있을 수 있습니다. 제 경우에는 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. .NET Framework를 사용하여 컨트롤러 예외 처리를 재사용합니다 HandlerExceptionResolver.
  2. XML 구성을 통해 Java 구성 사용

먼저, 일반 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)
            (...)
    }

}

제공된 다른 훌륭한 답변을 보완하기 위해 너무 최근에 컨트롤러 메서드에서 발생할 수있는 다른 예외와 함께 예외를 throw 할 수있는 필터가 포함 된 간단한 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";
  }

}

위의 답변에서 제안 된 다른 방법을 읽은 후 사용자 지정 필터를 사용하여 인증 예외를 처리하기로 결정했습니다. 다음 방법을 사용하여 오류 응답 클래스를 사용하여 응답 상태 및 코드를 처리 할 수있었습니다.

사용자 지정 필터를 만들고 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;
}}

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

참고 URL : https://stackoverflow.com/questions/34595605/how-to-manage-exceptions-thrown-in-filters-in-spring

반응형