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

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

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

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

  • 사용자 지정 필터를 구현해야합니까? 나는 찾았 ExceptionTranslationFilter지만 이것은 AuthenticationException또는 AccessDeniedException.
  • 나는 내 자신의을 구현할 생각을 HandlerExceptionResolver했지만 이로 인해 관리 할 사용자 정의 예외가 없으며 이것보다 더 분명한 방법이 있어야합니다. 나는 또한 try / catch를 추가하고 HandlerExceptionResolver(충분히 좋을 것, 내 예외는 특별한 것이 아님) 의 구현을 호출하려고 시도 했지만 응답에 아무것도 반환하지 않고 상태 200과 빈 본문을 얻습니다.

이것을 처리하는 좋은 방법이 있습니까? 감사



답변

그래서 이것이 내가 한 일입니다.

여기에서 필터에 대한 기본 사항을 읽었으며 필터 체인에서 첫 번째가 될 사용자 지정 필터를 만들어야하며 거기에서 발생할 수있는 모든 런타임 예외를 포착하기 위해 시도 할 수 있다는 것을 알아 냈습니다. 그런 다음 수동으로 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>


답변

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

}


답변

이 문제를 직접 발견하고 아래 단계를 수행하여 ExceptionController주석이 달린 내 등록 된 필터 @ControllerAdviseExceptions던져졌습니다.

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

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

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

단계


  1. 예외를 발생하거나 발생하지 않을 수있는 사용자 지정 필터가 있습니다.
  2. @ControllerAdviseMyExceptionController를 사용하여 예외를 처리하는 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던져 사용하려는 사람들과 솔루션을 공유합니다 .


답변

위의 답변을 합쳐서 제가 한 작업은 다음과 같습니다. 이미 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수정없이 통과합니다 .

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


답변

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


답변

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

}


답변

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

}