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


106

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

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

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

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

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


Spring Boot의 BasicErrorController를 재정의 할 수 있습니다. 나는 여기에 그것에 대해 블로그했다 : naturalprogrammer.com/blog/1685463/…
Sanjay

답변:


83

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

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

ErrorResponse 클래스를 게시 할 수 있습니까?
Shiva kumar

@Shivakumar ErrorResponse 클래스는 아마도 간단한 코드 / 메시지 속성을 가진 간단한 DTO 일 것입니다.
20:53에 ratijas

19

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

}

17

이 문제를 직접 발견하고 아래 단계를 수행하여 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던져 사용하려는 사람들과 솔루션을 공유합니다 .


10
글쎄, 당신은 :) 그것을 할 수있는 방법을 소리가 자신의 솔루션 공유에 오신 것을 환영합니다
라프

1
컨트롤러가 필터에 연결되는 것을 피하려면 (@ Bato-BairTsyrenov가 제가 가정하는 내용이라고 가정합니다) ErrorDTO를 생성하는 로직을 자체 @Component클래스에 쉽게 추출하고이를 필터에서 사용할 수 있습니다. 컨트롤러.
Rüdiger Schulz

1
필터에 특정 컨트롤러를 삽입하는 것이 매우 깨끗하지 않기 때문에 나는 당신에게 전적으로 동의하지 않습니다.
psv

에서 언급했듯이 answer이것이 방법 중 하나입니다! 나는 그것이 최선의 방법이라고 주장하지 않았습니다. @psv 여러분의 관심을 공유해 주셔서 감사합니다. 커뮤니티가 여러분이 염두에두고있는 솔루션에 감사 할 것이라고 확신합니다. :)
Raf

12

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

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


1
이제이 솔루션을 테스트 해 주셔서 감사하지만 제 경우에는 완벽하게 작동합니다.
Maciej

추가해야하는 스프링 부트 2.0+에 대한 깨끗하고 간단한 추가 @Override public String getErrorPath() { return null; }
Fma

"javax.servlet.error.exception"대신 javax.servlet.RequestDispatcher.ERROR_EXCEPTION을 사용할 수 있습니다
Marx

9

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

그러나 나머지 API에서 위치를 리디렉션하는 것은 좋은 해결책이 아닙니다.
jmattheis

@jmattheis 위의 내용은 리디렉션이 아닙니다.
holmis83

사실, 나는 위치를 보았고 http 위치로 할 일이 있다고 생각했습니다. 그런 다음 이것이 내가 필요한 것입니다 (:
jmattheis

web.xml에 해당하는 Java 구성이있는 경우 추가 할 수 있습니까?
k-den

1
@ k-den 현재 사양에 상응하는 Java 구성은 없지만 web.xml과 Java 구성을 혼합 할 수 있습니다.
holmis83

5

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

}

자동 구성에 영향을 줍니까?
Samet Baskıcı

HandlerExceptionResolver가 반드시 예외를 처리하는 것은 아닙니다. 따라서 HTTP 200으로 넘어갈 수 있습니다. 호출하기 전에 response.setStatus (..)를 사용하는 것이 더 안전 해 보입니다.
ThomasRS

5

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

}

3

응용 프로그램의 상태를 테스트하고 문제가 발생하면 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() {}
}

2

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

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

0

catch 블록 내에서 다음 메서드를 사용할 수 있습니다.

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

모든 HttpStatus 코드와 사용자 지정 메시지를 사용할 수 있습니다.


-1

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

CorsFilter에서 예외 처리는 작동하지만 매우 깨끗하지 않습니다. 난 정말 필요가 모든 필터에 대한 예외를 처리하는 것을 실제로
kopelitsa

35
in이 도달하지 않을 수 있기 때문에에서 예외 발생 Filter이 포착 @ControllerAdvice되지 않을 수 있습니다 DispatcherServlet.
Thanh Nguyen Van

-1

이를 위해 사용자 지정 필터를 만들 필요가 없습니다. ServletException (선언에 표시된 doFilter 메서드에서 발생)을 확장하는 사용자 지정 예외를 만들어이 문제를 해결했습니다. 그런 다음 전역 오류 처리기에 의해 포착되고 처리됩니다.

편집 : 문법


전역 오류 처리기의 코드 조각을 공유해 주시겠습니까?
Neeraj Vernekar

그것은 나를 위해 작동하지 않습니다. ServletException을 확장하는 사용자 지정 예외를 만들었고 ExceptionHandler에서이 예외에 대한 지원을 추가했지만 거기에서 가로 채지는 않았습니다.
Marx
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.