Spring Boot REST服务异常处理

我正在尝试设置大型REST服务服务器。我们使用Spring Boot 1.2.1、Spring 4.1.5和Java 8。我们的控制器实现了@RestController和标准的@RequestMapping注释。

我的问题是,Spring Boot将控制器异常的默认重定向设置为/error。从文档中:

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

经过多年使用Node.JS编写REST应用程序,这对我来说一点也不明智。服务端点生成的任何异常都应在响应中返回。我不能理解为什么你会发送一个重定向给一个很可能是Angular或jQuery SPA的消费者,他们只是在寻找一个答案,而不能或不会对重定向采取任何行动。

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

医生们对如何做到这一点一直含糊其辞。在我看来,您必须要么创建自己的错误控制器实现,要么以某种方式使用控制器建议,但我看到的所有示例仍然包括将响应转发到某种错误映射,这没有帮助。其他示例表明,您必须列出要处理的每个异常类型,而不是只列出“ Throwable ”并获取所有内容。

有没有人能告诉我我错过了什么,或者给我指出正确的方向,告诉我如何做到这一点,而不是暗示Node.JS更容易处理?

333920 次浏览

我认为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) {
// ...
}
}

您可以_0检查此_ABC

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

新答案(2016-04-20)

使用Spring Boot 1.3.1.release

新步骤1-,向应用程序添加以下属性既简单又不会造成太大干扰。属性:

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

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

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


新答案(2015-12-04)

使用Spring Boot 1.2.7.释放

新步骤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代码进行大量的运行时跟踪,我终于找到了一个可以处理所有异常(不是错误,但请继续阅读)的配置,包括404。

第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;
}
}

这样做的缺点是它取代了默认的Dispatcher Servlet.这对我们来说还不是问题,没有副作用或执行问题出现。如果您出于其他原因要使用Dispatcher 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 Boot应用程序现在将在上面的处理程序类中捕获所有异常,包括404,你可以随意处理它们。

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

如有任何意见/更正/改进,我们将不胜感激。

与的

解决方案 dispatcherServlet.setThrowExceptionIfNoHandlerFound(true);@EnableWebMVC @ControllerAdvice 我可以使用Spring Boot 1.3.1,而不能使用1.2.7

这个代码呢?我使用回退请求映射来捕获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;
}


}

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

然后为您的异常定义一个或多个advice特征(或者使用默认提供的特征)。

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 {


}

在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

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

我目前正在构建一个REST API,它使用Spring Boot 1.5.2.Release和Spring Framework 4.3.7.Release.我使用Java配置方法(与XML配置相反)。此外,我的项目使用全局异常处理机制,该机制使用@RestControllerAdvice注释(请参阅后面的内容)。

我的项目与您的项目具有相同的要求:我希望我的REST API在尝试向不存在的URL发送请求时,在对API客户端的HTTP响应中返回HTTP 404 Not Found,并附带JSON有效负载。在我的例子中,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的博客上创建了一个类似于这篇文章中建议的类。此类表示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:创建/配置全局异常处理程序

我使用下面的类来处理异常(为简单起见,我删除了导入语句、日志记录代码和其他一些不相关的代码):

@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匹配器,我用它来更好地处理时间戳字段。下面是代码(我在这里中找到的):

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配合得很好(参见上面的测试)。

@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");
}


}

对于想要根据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.

带有RestController注释的简单异常控制器类将负责控制器级别的异常处理。

    @RestControllerAdvice
public class ExceptionController
{
// Mention the exception here..
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public ResponseEntity<?> exceptionHandler(MethodArgumentNotValidException e)
{
var errors = e.getBindingResult().getAllErrors().stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.toList());
var response = new ResponseBuilder()
.withHttpStatus(HttpStatus.BAD_REQUEST.value())
.withMessage(CustomStatus.FAILED.getMessage())
.withErrorCode(CustomStatus.FAILED.getValue())
.withErrorDescription(errors)
.build();
return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
}
}