Home Articles

Spring Boot REST服务异常处理

Asked
Viewed 220 times
126

我正在尝试 Build 一个大型REST服务服务器 . 我们使用的是Spring Boot 1.2.1 Spring 4.1.5和Java 8.我们的控制器正在实现@RestController和标准的@RequestMapping注释 .

我的问题是Spring Boot为控制器异常设置了默认重定向到 /error . 来自文档:

Spring Boot默认提供/错误映射,以合理的方式处理所有错误,并在servlet容器中注册为“全局”错误页面 .

从使用Node.js编写REST应用程序多年来,对我来说,这对任何事情都是明智的 . 服务 endpoints 生成的任何异常都应在响应中返回 . 我无法理解为什么你发送一个重定向到最有可能是Angular或JQuery SPA消费者的消息,它只是寻找答案而不能或不会对重定向采取任何行动 .

我想要做的是设置一个全局错误处理程序,可以接受任何异常 - 有意地从请求映射方法抛出或由Spring自动生成(如果没有找到请求路径签名的处理程序方法,则为404),并返回标准格式化错误响应(400,500,503,404)到没有任何MVC重定向的客户端 . 具体来说,我们将采用错误,使用UUID将其记录到NoSQL,然后使用JSON正文中日志条目的UUID向客户端返回正确的HTTP错误代码 .

对于如何做到这一点,文档一直含糊不清 . 在我看来,你必须创建自己的ErrorController实现或以某种方式使用ControllerAdvice,但所有的例子我都有帮助 . 其他示例表明您必须列出要处理的每个Exception类型,而不是仅列出"Throwable"并获取所有内容 .

任何人都可以告诉我我错过了什么,或指出我如何做到这一点的正确方向,而不建议Node.js更容易处理的链?

9 Answers

  • 103

    虽然这是一个较老的问题,但我想就此分享我的想法 . 我希望,对你们中的一些人有所帮助 .

    我目前正在构建一个REST API,它使用Spring Boot 1.5.2.RELEASE和Spring Framework 4.3.7.RELEASE . 我使用Java Config方法(而不是XML配置) . 此外,我的项目使用 @RestControllerAdvice 注释的全局异常处理机制(请参阅下文) .

    我的项目与您的项目具有相同的要求:我希望我的REST API在尝试向不存在的URL发送请求时,在API响应的HTTP响应中返回带有JSON有效负载的 HTTP 404 Not Found . 在我的例子中,JSON有效负载看起来像这样(明显不同于Spring Boot默认值,顺便说一句 . ):

    {
        "code": 1000,
        "message": "No handler found for your request.",
        "timestamp": "2017-11-20T02:40:57.628Z"
    }
    

    我终于成功了 . 以下是您需要做的主要任务:

    • 如果API客户端调用不存在处理程序方法的URL,请确保抛出 NoHandlerFoundException (请参阅下面的步骤1) .

    • 创建一个自定义错误类(在我的情况下为 ApiError ),其中包含应返回给API客户端的所有数据(请参阅步骤2) .

    • 创建一个异常处理程序,它对 NoHandlerFoundException 做出反应,并向API客户端返回正确的错误消息(请参阅步骤3) .

    • 为它编写测试并确保它有效(参见步骤4) .

    好的,现在详细说明:

    Step 1: Configure application.properties

    我必须将以下两个配置设置添加到项目的 application.properties 文件中:

    spring.mvc.throw-exception-if-no-handler-found=true
    spring.resources.add-mappings=false
    

    这样可以确保在客户端尝试访问不存在能够处理请求的控制器方法的URL的情况下抛出 NoHandlerFoundException .

    Step 2: Create a Class for API Errors

    我在Eugen Paraschiv的博客上创建了类似于this article中建议的类 . 此类表示API错误 . 如果出现错误,此信息将发送到HTTP响应正文中的客户端 .

    public class ApiError {
    
        private int code;
        private String message;
        private Instant timestamp;
    
        public ApiError(int code, String message) {
            this.code = code;
            this.message = message;
            this.timestamp = Instant.now();
        }
    
        public ApiError(int code, String message, Instant timestamp) {
            this.code = code;
            this.message = message;
            this.timestamp = timestamp;
        }
    
        // Getters and setters here...
    }
    

    Step 3: Create / Configure a Global Exception Handler

    我使用以下类来处理异常(为简单起见,我删除了import语句,记录代码和其他一些不相关的代码片段):

    @RestControllerAdvice
    public class GlobalExceptionHandler {
    
        @ExceptionHandler(NoHandlerFoundException.class)
        @ResponseStatus(HttpStatus.NOT_FOUND)
        public ApiError noHandlerFoundException(
                NoHandlerFoundException ex) {
    
            int code = 1000;
            String message = "No handler found for your request.";
            return new ApiError(code, message);
        }
    
        // More exception handlers here ...
    }
    

    Step 4: Write a test

    我想确保,即使出现故障,API也始终向调用客户端返回正确的错误消息 . 因此,我写了一个这样的测试:

    @RunWith(SpringRunner.class)
    @SpringBootTest(webEnvironment = SprintBootTest.WebEnvironment.RANDOM_PORT)
    @AutoConfigureMockMvc
    @ActiveProfiles("dev")
    public class GlobalExceptionHandlerIntegrationTest {
    
        public static final String ISO8601_DATE_REGEX =
            "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$";
    
        @Autowired
        private MockMvc mockMvc;
    
        @Test
        @WithMockUser(roles = "DEVICE_SCAN_HOSTS")
        public void invalidUrl_returnsHttp404() throws Exception {
            RequestBuilder requestBuilder = getGetRequestBuilder("/does-not-exist");
            mockMvc.perform(requestBuilder)
                .andExpect(status().isNotFound())
                .andExpect(jsonPath("$.code", is(1000)))
                .andExpect(jsonPath("$.message", is("No handler found for your request.")))
                .andExpect(jsonPath("$.timestamp", RegexMatcher.matchesRegex(ISO8601_DATE_REGEX)));
        }
    
        private RequestBuilder getGetRequestBuilder(String url) {
            return MockMvcRequestBuilders
                .get(url)
                .accept(MediaType.APPLICATION_JSON);
        }
    

    @ActiveProfiles("dev") 注释可以保留 . 我只在使用不同的配置文件时使用它 . RegexMatcher 是一个自定义Hamcrest matcher我用来更好地处理时间戳字段 . 这是代码(我发现它here):

    public class RegexMatcher extends TypeSafeMatcher<String> {
    
        private final String regex;
    
        public RegexMatcher(final String regex) {
            this.regex = regex;
        }
    
        @Override
        public void describeTo(final Description description) {
            description.appendText("matches regular expression=`" + regex + "`");
        }
    
        @Override
        public boolean matchesSafely(final String string) {
            return string.matches(regex);
        }
    
        // Matcher method you can call on this matcher class
        public static RegexMatcher matchesRegex(final String string) {
            return new RegexMatcher(regex);
        }
    }
    

    Some further notes from my side:

    • 在StackOverflow上的许多其他帖子中,人们建议设置 @EnableWebMvc 注释 . 在我的情况下,这不是必要的 .

    • 这种方法适用于MockMvc(参见上面的测试) .

  • 33

    对于REST控制器,我建议使用 Zalando Problem Spring Web .

    https://github.com/zalando/problem-spring-web

    如果Spring Boot旨在嵌入一些自动配置,那么这个库可以为异常处理提供更多功能 . 您只需要添加依赖项:

    <dependency>
        <groupId>org.zalando</groupId>
        <artifactId>problem-spring-web</artifactId>
        <version>LATEST</version>
    </dependency>
    

    然后为您的例外定义一个或多个建议特征(或使用默认提供的特征)

    public interface NotAcceptableAdviceTrait extends AdviceTrait {
    
        @ExceptionHandler
        default ResponseEntity<Problem> handleMediaTypeNotAcceptable(
                final HttpMediaTypeNotAcceptableException exception,
                final NativeWebRequest request) {
            return Responses.create(Status.NOT_ACCEPTABLE, exception, request);
        }
    
    }
    

    然后,您可以将异常处理的控制器建议定义为:

    @ControllerAdvice
    class ExceptionHandling implements MethodNotAllowedAdviceTrait, NotAcceptableAdviceTrait {
    
    }
    
  • 20

    New answer (2016-04-20)

    使用Spring Boot 1.3.1.RELEASE

    New Step 1 - 将以下属性添加到application.properties很容易且不那么干扰:

    spring.mvc.throw-exception-if-no-handler-found=true
    spring.resources.add-mappings=false
    

    比修改现有的DispatcherServlet实例(如下所示)容易得多! - JO'

    如果使用完整的RESTful应用程序,禁用静态资源的自动映射非常重要,因为如果您使用Spring Boot的默认配置来处理静态资源,那么资源处理程序将处理请求(它最后排序并映射到/ **这意味着它会获取应用程序中任何其他处理程序尚未处理的任何请求),因此调度程序servlet没有机会抛出异常 .


    New Answer (2015-12-04)

    使用Spring Boot 1.2.7.RELEASE

    New Step 1 - 我发现设置"throExceptionIfNoHandlerFound"标志的方式要少得多 . 在应用程序初始化类中将此替换DispatcherServlet替换代码(步骤1):

    @ComponentScan()
    @EnableAutoConfiguration
    public class MyApplication extends SpringBootServletInitializer {
        private static Logger LOG = LoggerFactory.getLogger(MyApplication.class);
        public static void main(String[] args) {
            ApplicationContext ctx = SpringApplication.run(MyApplication.class, args);
            DispatcherServlet dispatcherServlet = (DispatcherServlet)ctx.getBean("dispatcherServlet");
            dispatcherServlet.setThrowExceptionIfNoHandlerFound(true);
        }
    

    在这种情况下,我们在现有DispatcherServlet上设置标志,该标志保留Spring Boot框架的任何自动配置 .

    我发现还有一件事 - @EnableWebMvc注释对Spring Boot来说是致命的 . 是的,该注释能够捕获所有控制器异常,如下所述,但它也会杀死Spring Boot通常提供的大量有用的自动配置 . 使用Spring Boot时,请极其谨慎地使用该注释 .


    Original Answer:

    经过大量研究并跟进这里发布的解决方案(感谢帮助!)以及对Spring代码的少量运行时跟踪,我终于找到了一个可以处理所有异常的配置(不是错误,而是继续阅读)包括404s .

    Step 1 - 告诉SpringBoot停止在"handler not found"情况下使用MVC . 我们希望Spring抛出异常,而不是返回到客户端,视图重定向到"/error" . 为此,您需要在其中一个配置类中包含一个条目:

    // NEW CODE ABOVE REPLACES THIS! (2015-12-04)
    @Configuration
    public class MyAppConfig {
        @Bean  // Magic entry 
        public DispatcherServlet dispatcherServlet() {
            DispatcherServlet ds = new DispatcherServlet();
            ds.setThrowExceptionIfNoHandlerFound(true);
            return ds;
        }
    }
    

    这样做的缺点是它取代了默认的调度程序servlet . 这对我们来说还不是问题,没有出现副作用或执行问题 . 如果您出于其他原因要对调度程序servlet执行任何其他操作,则可以执行此操作 .

    Step 2 - 现在,当没有找到处理程序时,spring引导将抛出异常,可以使用统一异常处理程序中的任何其他异常处理该异常:

    @EnableWebMvc
    @ControllerAdvice
    public class ServiceExceptionHandler extends ResponseEntityExceptionHandler {
    
        @ExceptionHandler(Throwable.class)
        @ResponseBody
        ResponseEntity<Object> handleControllerException(HttpServletRequest req, Throwable ex) {
            ErrorResponse errorResponse = new ErrorResponse(ex);
            if(ex instanceof ServiceException) {
                errorResponse.setDetails(((ServiceException)ex).getDetails());
            }
            if(ex instanceof ServiceHttpException) {
                return new ResponseEntity<Object>(errorResponse,((ServiceHttpException)ex).getStatus());
            } else {
                return new ResponseEntity<Object>(errorResponse,HttpStatus.INTERNAL_SERVER_ERROR);
            }
        }
    
        @Override
        protected ResponseEntity<Object> handleNoHandlerFoundException(NoHandlerFoundException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
            Map<String,String> responseBody = new HashMap<>();
            responseBody.put("path",request.getContextPath());
            responseBody.put("message","The URL you have reached is not in service at this time (404).");
            return new ResponseEntity<Object>(responseBody,HttpStatus.NOT_FOUND);
        }
        ...
    }
    

    请记住,我认为“@EnableWebMvc”注释在这里很重要 . 似乎如果没有它,这一切都无法奏效 . 就是这样 - 您的Spring启动应用程序现在将捕获上述处理程序类中的所有异常,包括404,您可以随意使用它们 .

    最后一点 - 似乎没有办法让它捕获抛出的错误 . 我有一个古怪的想法,使用方面来捕获错误,并将它们变成异常上面的代码然后可以处理,但我还没有时间实际尝试实现它 . 希望这有助于某人 .

    任何评论/更正/改进将不胜感激 .

  • 13

    @RestControllerAdvice是Spring Framework 4.3的一个新功能,它通过一个跨领域的关注解决方案来处理RestfulApi的异常:

    package com.khan.vaquar.exception;
    
    import javax.servlet.http.HttpServletRequest;
    
    import org.owasp.esapi.errors.IntrusionException;
    import org.owasp.esapi.errors.ValidationException;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.http.HttpStatus;
    import org.springframework.web.bind.MissingServletRequestParameterException;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    import org.springframework.web.bind.annotation.ResponseStatus;
    import org.springframework.web.bind.annotation.RestControllerAdvice;
    import org.springframework.web.servlet.NoHandlerFoundException;
    
    import com.fasterxml.jackson.core.JsonProcessingException;
    import com.khan.vaquar.domain.ErrorResponse;
    
    /**
     * Handles exceptions raised through requests to spring controllers.
     **/
    @RestControllerAdvice
    public class RestExceptionHandler {
    
        private static final String TOKEN_ID = "tokenId";
    
        private static final Logger log = LoggerFactory.getLogger(RestExceptionHandler.class);
    
        /**
         * Handles InstructionExceptions from the rest controller.
         * 
         * @param e IntrusionException
         * @return error response POJO
         */
        @ResponseStatus(HttpStatus.BAD_REQUEST)
        @ExceptionHandler(value = IntrusionException.class)
        public ErrorResponse handleIntrusionException(HttpServletRequest request, IntrusionException e) {       
            log.warn(e.getLogMessage(), e);
            return this.handleValidationException(request, new ValidationException(e.getUserMessage(), e.getLogMessage()));
        }
    
        /**
         * Handles ValidationExceptions from the rest controller.
         * 
         * @param e ValidationException
         * @return error response POJO
         */
        @ResponseStatus(HttpStatus.BAD_REQUEST)
        @ExceptionHandler(value = ValidationException.class)
        public ErrorResponse handleValidationException(HttpServletRequest request, ValidationException e) {     
            String tokenId = request.getParameter(TOKEN_ID);
            log.info(e.getMessage(), e);
    
            if (e.getUserMessage().contains("Token ID")) {
                tokenId = "<OMITTED>";
            }
    
            return new ErrorResponse(   tokenId,
                                        HttpStatus.BAD_REQUEST.value(), 
                                        e.getClass().getSimpleName(),
                                        e.getUserMessage());
        }
    
        /**
         * Handles JsonProcessingExceptions from the rest controller.
         * 
         * @param e JsonProcessingException
         * @return error response POJO
         */
        @ResponseStatus(HttpStatus.BAD_REQUEST)
        @ExceptionHandler(value = JsonProcessingException.class)
        public ErrorResponse handleJsonProcessingException(HttpServletRequest request, JsonProcessingException e) {     
            String tokenId = request.getParameter(TOKEN_ID);
            log.info(e.getMessage(), e);
            return new ErrorResponse(   tokenId,
                                        HttpStatus.BAD_REQUEST.value(), 
                                        e.getClass().getSimpleName(),
                                        e.getOriginalMessage());
        }
    
        /**
         * Handles IllegalArgumentExceptions from the rest controller.
         * 
         * @param e IllegalArgumentException
         * @return error response POJO
         */
        @ResponseStatus(HttpStatus.BAD_REQUEST)
        @ExceptionHandler(value = IllegalArgumentException.class)
        public ErrorResponse handleIllegalArgumentException(HttpServletRequest request, IllegalArgumentException e) {
            String tokenId = request.getParameter(TOKEN_ID);
            log.info(e.getMessage(), e);
            return new ErrorResponse(   tokenId,
                                        HttpStatus.BAD_REQUEST.value(), 
                                        e.getClass().getSimpleName(), 
                                        e.getMessage());
        }
    
        @ResponseStatus(HttpStatus.BAD_REQUEST)
        @ExceptionHandler(value = UnsupportedOperationException.class)
        public ErrorResponse handleUnsupportedOperationException(HttpServletRequest request, UnsupportedOperationException e) {
            String tokenId = request.getParameter(TOKEN_ID);
            log.info(e.getMessage(), e);
            return new ErrorResponse(   tokenId,
                                        HttpStatus.BAD_REQUEST.value(), 
                                        e.getClass().getSimpleName(), 
                                        e.getMessage());
        }
    
        /**
         * Handles MissingServletRequestParameterExceptions from the rest controller.
         * 
         * @param e MissingServletRequestParameterException
         * @return error response POJO
         */
        @ResponseStatus(HttpStatus.BAD_REQUEST)
        @ExceptionHandler(value = MissingServletRequestParameterException.class)
        public ErrorResponse handleMissingServletRequestParameterException( HttpServletRequest request, 
                                                                            MissingServletRequestParameterException e) {
            String tokenId = request.getParameter(TOKEN_ID);
            log.info(e.getMessage(), e);
            return new ErrorResponse(   tokenId,
                                        HttpStatus.BAD_REQUEST.value(), 
                                        e.getClass().getSimpleName(), 
                                        e.getMessage());
        }
    
        /**
         * Handles NoHandlerFoundExceptions from the rest controller.
         * 
         * @param e NoHandlerFoundException
         * @return error response POJO
         */
        @ResponseStatus(HttpStatus.NOT_FOUND)
        @ExceptionHandler(value = NoHandlerFoundException.class)
        public ErrorResponse handleNoHandlerFoundException(HttpServletRequest request, NoHandlerFoundException e) {
            String tokenId = request.getParameter(TOKEN_ID);
            log.info(e.getMessage(), e);
            return new ErrorResponse(   tokenId,
                                        HttpStatus.NOT_FOUND.value(), 
                                        e.getClass().getSimpleName(), 
                                        "The resource " + e.getRequestURL() + " is unavailable");
        }
    
        /**
         * Handles all remaining exceptions from the rest controller.
         * 
         * This acts as a catch-all for any exceptions not handled by previous exception handlers.
         * 
         * @param e Exception
         * @return error response POJO
         */
        @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
        @ExceptionHandler(value = Exception.class)
        public ErrorResponse handleException(HttpServletRequest request, Exception e) {
            String tokenId = request.getParameter(TOKEN_ID);
            log.error(e.getMessage(), e);
            return new ErrorResponse(   tokenId,
                                        HttpStatus.INTERNAL_SERVER_ERROR.value(), 
                                        e.getClass().getSimpleName(), 
                                        "An internal error occurred");
        }   
    
    }
    
  • 13

    这段代码怎么样?我使用回退请求映射来捕获404错误 .

    @Controller
    @ControllerAdvice
    public class ExceptionHandlerController {
    
        @ExceptionHandler(Exception.class)
        public ModelAndView exceptionHandler(HttpServletRequest request, HttpServletResponse response, Exception ex) {
            //If exception has a ResponseStatus annotation then use its response code
            ResponseStatus responseStatusAnnotation = AnnotationUtils.findAnnotation(ex.getClass(), ResponseStatus.class);
    
            return buildModelAndViewErrorPage(request, response, ex, responseStatusAnnotation != null ? responseStatusAnnotation.value() : HttpStatus.INTERNAL_SERVER_ERROR);
        }
    
        @RequestMapping("*")
        public ModelAndView fallbackHandler(HttpServletRequest request, HttpServletResponse response) throws Exception {
            return buildModelAndViewErrorPage(request, response, null, HttpStatus.NOT_FOUND);
        }
    
        private ModelAndView buildModelAndViewErrorPage(HttpServletRequest request, HttpServletResponse response, Exception ex, HttpStatus httpStatus) {
            response.setStatus(httpStatus.value());
    
            ModelAndView mav = new ModelAndView("error.html");
            if (ex != null) {
                mav.addObject("title", ex);
            }
            mav.addObject("content", request.getRequestURL());
            return mav;
        }
    
    }
    
  • 5

    使用Spring Boot 1.4,增加了新的酷类,以便更轻松地处理异常处理,这有助于删除样板代码 .

    为异常处理提供了新的 @RestControllerAdvice ,它是 @ControllerAdvice@ResponseBody 的组合 . 使用此新注释时,可以删除 @ExceptionHandler 方法上的 @ResponseBody .

    @RestControllerAdvice
    public class GlobalControllerExceptionHandler {
    
        @ExceptionHandler(value = { Exception.class })
        @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
        public ApiErrorResponse unknownException(Exception ex, WebRequest req) {
            return new ApiErrorResponse(...);
        }
    }
    

    为了处理404错误,将 @EnableWebMvc 注释和以下内容添加到application.properties就足够了:
    spring.mvc.throw-exception-if-no-handler-found=true

    您可以在这里找到并使用这些来源:
    https://github.com/magiccrafter/spring-boot-exception-handling

  • 2

    使用 dispatcherServlet.setThrowExceptionIfNoHandlerFound(true);@EnableWebMvc @ControllerAdvice 的解决方案适用于Spring Boot 1.3.1,而不适用于1.2.7

  • 1

    我认为 ResponseEntityExceptionHandler 符合您的要求 . HTTP 400的示例代码:

    @ControllerAdvice
    public class MyExceptionHandler extends ResponseEntityExceptionHandler {
    
      @ResponseStatus(value = HttpStatus.BAD_REQUEST)
      @ExceptionHandler({HttpMessageNotReadableException.class, MethodArgumentNotValidException.class,
          HttpRequestMethodNotSupportedException.class})
      public ResponseEntity<Object> badRequest(HttpServletRequest req, Exception exception) {
        // ...
      }
    }
    

    你可以查看post

  • 0

    默认情况下,Spring Boot为json提供错误详细信息 .

    curl -v localhost:8080/greet | json_pp
    [...]
    < HTTP/1.1 400 Bad Request
    [...]
    {
       "timestamp" : 1413313361387,
       "exception" : "org.springframework.web.bind.MissingServletRequestParameterException",
       "status" : 400,
       "error" : "Bad Request",
       "path" : "/greet",
       "message" : "Required String parameter 'name' is not present"
    }
    

    它也适用于所有类型的请求映射错误 . 查看这篇文章http://www.jayway.com/2014/10/19/spring-boot-error-responses/

    如果要创建将其登录到NoSQL . 您可以在其中创建@ControllerAdvice,然后重新抛出异常 . 文档中有例子https://spring.io/blog/2013/11/01/exception-handling-in-spring-mvc

Related