Spring Boot REST服务异常处理

时间:2015-03-06 15:46:29

标签: java spring rest exception-handling spring-boot

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

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

  

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

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

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

文档对如何执行此操作一直模糊不清。在我看来,你必须创建自己的ErrorController实现或以某种方式使用ControllerAdvice,但我见过的所有示例仍然包括将响应转发给某种错误映射,这没有任何帮助。其他示例表明,您必须列出您要处理的每个例外类型,而不仅仅列出" Throwable"得到一切。

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

10 个答案:

答案 0 :(得分:116)

新答案(2016-04-20)

使用Spring Boot 1.3.1.RELEASE

新步骤1 - 将以下属性添加到application.properties很简单且不那么干扰:

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

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

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

新答案(2015-12-04)

使用Spring Boot 1.2.7.RELEASE

新的第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时,请极其谨慎地使用该注释。

原始答案:

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

第1步 - 告诉SpringBoot停止使用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做任何其他事情,那么就可以这样做。

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

@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,你可以随意使用它们。

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

任何评论/更正/改进都将受到赞赏。

答案 1 :(得分:36)

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

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

即。

@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 :(得分:25)

我认为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

答案 3 :(得分:14)

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

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

我的项目与您的项目具有相同的要求:我希望我的REST API在尝试向URL发送请求时向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客户端,请确保抛出NoHandlerFoundException 调用没有处理程序方法的URL(请参阅下面的步骤1)。
  • 创建一个自定义错误类(在我的情况下为ApiError),其中包含应返回给API客户端的所有数据(请参阅步骤2)。
  • 创建一个异常处理程序,对NoHandlerFoundException作出反应 并向API客户端返回正确的错误消息(请参阅步骤3)。
  • 为它编写测试并确保它有效(参见步骤4)。

好的,现在详情:

步骤1:配置application.properties

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

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

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

第2步:为API错误创建一个类

我在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...
}

步骤3:创建/配置全局异常处理程序

我使用以下类来处理异常(为简单起见,我删除了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 ...
}

第4步:编写测试

我想确保,即使出现故障,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);
    }
}

我方的一些进一步说明:

  • 在StackOverflow上的许多其他帖子中,人们建议设置@EnableWebMvc注释。在我的情况下,这不是必需的。
  • 这种方法适用于MockMvc(参见上面的测试)。

答案 4 :(得分: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 :(得分:5)

默认情况下,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

答案 6 :(得分:2)

对于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 {

}

答案 7 :(得分:2)

@RestControllerAdvice是Spring Framework 4.3的一个新功能,它通过一个跨领域的关注解决方案来处理Exception with 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");
    }   

}

答案 8 :(得分:0)

解决方案 dispatcherServlet.setThrowExceptionIfNoHandlerFound(true);@EnableWebMvc @ControllerAdvice 使用Spring Boot 1.3.1为我工作,而不是在1.2.7上工作

答案 9 :(得分:0)

对于希望根据http状态码进行响应的人,可以使用ErrorController方式:

@Controller
public class CustomErrorController extends BasicErrorController {

    public CustomErrorController(ServerProperties serverProperties) {
        super(new DefaultErrorAttributes(), serverProperties.getError());
    }

    @Override
    public ResponseEntity error(HttpServletRequest request) {
        HttpStatus status = getStatus(request);
        if (status.equals(HttpStatus.INTERNAL_SERVER_ERROR)){
            return ResponseEntity.status(status).body(ResponseBean.SERVER_ERROR);
        }else if (status.equals(HttpStatus.BAD_REQUEST)){
            return ResponseEntity.status(status).body(ResponseBean.BAD_REQUEST);
        }
        return super.error(request);
    }
}

这里的ResponseBean是我自定义的pojo,用于响应。