[英]Spring Boot REST service exception handling
我正在嘗試建立一個大型 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 錯誤代碼返回給客戶端。
文檔對如何執行此操作含糊不清。 在我看來,您必須要么創建自己的ErrorController實現,要么以某種方式使用ControllerAdvice ,但我看到的所有示例仍然包括將響應轉發到某種錯誤映射,這無濟於事。 其他示例表明您必須列出要處理的每種異常類型,而不是僅列出“可拋出”並獲取所有內容。
誰能告訴我我錯過了什么,或者在不建議 Node.js 更容易處理的鏈條的情況下為我指出如何做到這一點的正確方向?
新答案 (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 實例(如下)容易得多! - 喬'
如果使用完整的 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 代碼的大量運行時跟蹤,我終於找到了一個可以處理所有異常的配置(不是錯誤,但請繼續閱讀)包括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;
}
}
這樣做的缺點是它替換了默認的調度程序 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 boot 應用程序現在將捕獲上述處理程序類中的所有異常,包括 404,您可以隨意處理它們。
最后一點 - 似乎沒有辦法讓它捕捉拋出的錯誤。 我有一個古怪的想法,即使用方面來捕獲錯誤並將它們轉換為上面的代碼可以處理的異常,但我還沒有時間實際嘗試實現它。 希望這可以幫助某人。
任何評論/更正/增強將不勝感激。
在 Spring Boot 1.4+ 中,添加了用於更輕松處理異常的新酷類,這有助於刪除樣板代碼。
為異常處理提供了一個新的@RestControllerAdvice
,它是@ControllerAdvice
和@ResponseBody
的組合。 使用此新注釋時,您可以刪除@ResponseBody
方法上的@ExceptionHandler
。
IE
@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
我認為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) {
// ...
}
}
你可以看看這個帖子
雖然這是一個較老的問題,但我想分享我對此的看法。 我希望它對你們中的一些人有所幫助。
我目前正在構建一個 REST API,它使用 Spring Boot 1.5.2.RELEASE 和 Spring Framework 4.3.7.RELEASE。 我使用 Java Config 方法(與 XML 配置相反)。 此外,我的項目使用@RestControllerAdvice
注釋使用全局異常處理機制(見下文)。
我的項目與您的項目具有相同的要求:當它嘗試向不存在的 URL 發送請求時,我希望我的 REST API 在對 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"
}
我終於讓它工作了。 以下是您需要完成的主要任務:
NoHandlerFoundException
(請參閱下面的步驟 1)。ApiError
),其中包含應返回給 API 客戶端的所有數據(參見步驟 2)。NoHandlerFoundException
做出反應並向 API 客戶端返回正確的錯誤消息(請參見步驟 3)。好的,現在進入細節:
第 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);
}
}
我這邊的一些進一步說明:
@EnableWebMvc
注釋。 在我的情況下,這不是必需的。這段代碼呢? 我使用回退請求映射來捕獲 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;
}
}
@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");
}
}
默認情況下,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
對於 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 {
}
對於想要根據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。
使用dispatcherServlet.setThrowExceptionIfNoHandlerFound(true);
和@EnableWebMvc @ControllerAdvice
在 Spring Boot 1.3.1 上為我工作,而在 1.2.7 上沒有工作
帶有 RestController Annotation 的簡單異常控制器類將負責控制器級別的異常處理。
@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);
}
}
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.