Springboot3新特性異常信息ProblemDetail詳解
環(huán)境:Springboot3.0.5
概述
RFC 7807定義了為HTTP響應(yīng)中錯(cuò)誤的可讀詳細(xì)信息,以避免需要為HTTP API定義新的錯(cuò)誤響應(yīng)格式。HTTP [RFC7230]狀態(tài)碼有時(shí)不足以傳達(dá)關(guān)于錯(cuò)誤的足夠信息。
RFC 7807 定義了簡(jiǎn)單的JSON[RFC7159]和XML[W3C.REC-XML-20081126]文檔格式以滿足此目的。它們被設(shè)計(jì)為可由HTTP API重用,HTTP API可以識(shí)別特定于其需求的不同“問(wèn)題類型”。
因此,API客戶端既可以知道高級(jí)錯(cuò)誤類(使用狀態(tài)碼),也可以知道問(wèn)題的細(xì)粒度細(xì)節(jié)。
例如,考慮一個(gè)響應(yīng),該響應(yīng)表明客戶的賬戶沒(méi)有足夠的權(quán)限。403禁止?fàn)顟B(tài)代碼可能被認(rèn)為是最適合使用的,因?yàn)樗鼘⑾騂TTP通用軟件(如客戶端庫(kù)、緩存和代理)通知響應(yīng)的一般語(yǔ)義。然而,這并沒(méi)有為API客戶端提供足夠的信息,說(shuō)明為什么禁止請(qǐng)求、適用的帳戶余額或如何糾正問(wèn)題。如果這些細(xì)節(jié)以可讀的格式包含在響應(yīng)體中,則客戶端可以適當(dāng)?shù)靥幚硭?;例如觸發(fā)將更多的信用轉(zhuǎn)移到賬戶中。
RFC 7807規(guī)范通過(guò)使用URI[RFC3986]識(shí)別特定類型的問(wèn)題(例如,“信用不足”)來(lái)實(shí)現(xiàn)這一點(diǎn);HTTP API可以通過(guò)指定受其控制的新URI或重用現(xiàn)有URI來(lái)實(shí)現(xiàn)這一點(diǎn)。
此外,Problem Detail信息可以包含其他信息,例如標(biāo)識(shí)問(wèn)題具體發(fā)生的URI(有效地為“Joe上周四沒(méi)有足夠的信用”這一概念提供了標(biāo)識(shí)符),這對(duì)于支持或取證目的可能很有用。
Problem Detail的數(shù)據(jù)模型是一個(gè)JSON[RFC7159]對(duì)象;當(dāng)格式化為JSON文檔時(shí),它使用“application/problem+json”媒體類型。
請(qǐng)注意,Problem Detail 不是在HTTP中傳達(dá)問(wèn)題細(xì)節(jié)的唯一方式;例如,如果響應(yīng)仍然是資源的表示,那么通常最好以該應(yīng)用程序的格式來(lái)描述相關(guān)細(xì)節(jié)。同樣,在許多情況下,有一個(gè)適當(dāng)?shù)腍TTP狀態(tài)代碼,不需要傳遞額外的細(xì)節(jié)。
Problem Detail消息格式
Problem Detail的規(guī)范模型是JSON對(duì)象。當(dāng)序列化為JSON文檔時(shí),該格式用“application/problem+json”媒體類型標(biāo)識(shí)。
例如,一個(gè)帶有JSONProblem Detail的HTTP響應(yīng):
HTTP/1.1 403 Forbidden
Content-Type: application/problem+json
Content-Language: en
{
"type": "https://pack.com/probs/out-of-credit",
"title": "你沒(méi)有足夠的信用。",
"detail": "你現(xiàn)在的余額是30,但是要花50。",
"instance": "/account/12345/msgs/abc",
"balance": 30,
"accounts": ["/account/12345", "/account/67890"]
}
這里,結(jié)余不足問(wèn)題(由其類型URI標(biāo)識(shí))
- type:標(biāo)識(shí)問(wèn)題類型的URI引用
- title:中指明了403的原因
- instance:給出了具體問(wèn)題發(fā)生的參考
- detail:中給出了發(fā)生的具體細(xì)節(jié),并添加了兩個(gè)擴(kuò)展
- balance:表示帳戶的余額
- accounts:提供了可以充值帳戶的鏈接
傳遞問(wèn)題特定擴(kuò)展的能力允許傳遞多個(gè)問(wèn)題。例如:
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json
Content-Language: en
{
"type": "https://example.net/validation-error",
"title": "Your request parameters didn't validate.",
"invalid-params": [ {
"name": "age",
"reason": "must be a positive integer"
},
{
"name": "color",
"reason": "must be 'green', 'red' or 'blue'"
}
]
}
Spring支持
從Spring6.x開(kāi)始支持Problem Detail。
REST服務(wù)的一個(gè)常見(jiàn)需求是在錯(cuò)誤響應(yīng)的主體中包含詳細(xì)信息。Spring框架支持“Problem Details for HTTP APIs”規(guī)范,即RFC 7807。
以下是此支持的主要抽象:
- ProblemDetail — ?RFC 7807問(wèn)題細(xì)節(jié)的表示;一個(gè)簡(jiǎn)單的容器,用于規(guī)范中定義的標(biāo)準(zhǔn)字段和非標(biāo)準(zhǔn)字段。
- ErrorResponse — 以RFC 7807的格式暴露HTTP錯(cuò)誤響應(yīng)細(xì)節(jié),包括HTTP狀態(tài)、響應(yīng)頭和響應(yīng)體;這允許異常封裝并暴露它們?nèi)绾斡成涞紿TTP響應(yīng)的細(xì)節(jié)。所有Spring MVC異常都實(shí)現(xiàn)了這一點(diǎn)。
- ErrorResponseException?—?基本的ErrorResponse實(shí)現(xiàn),其他人可以作為一個(gè)方便的基類使用。
- ResponseEntityExceptionHandler?—?@ControllerAdvice的方便基類,它處理所有Spring MVC異常,以及任何ErrorResponseException,并渲染一個(gè)帶有主體的錯(cuò)誤響應(yīng)。
Spring中要使用ProblemDetail首先需要通過(guò)如下配置開(kāi)啟:
spring:
mvc:
problemdetails:
enabled: true
我們可以在任何使用@ExceptionHandler或任何@RequestMapping方法返回ProblemDetail或ErrorResponse以呈現(xiàn)RFC 7807響應(yīng)。處理方式如下:
- ProblemDetail的status屬性決定了HTTP的狀態(tài)。
- 如果還沒(méi)有設(shè)置,則從當(dāng)前URL路徑設(shè)置ProblemDetail的實(shí)例屬性。
- 對(duì)于內(nèi)容協(xié)商,Jackson HttpMessageConverter在渲染ProblemDetail時(shí)更喜歡“application/problem+json”而不是“application/json”,如果沒(méi)有找到兼容的媒體類型,也會(huì)使用它。
要為Spring WebFlux異常和任何ErrorResponseException啟用RFC 7807響應(yīng),需要擴(kuò)展 ResponseEntityExceptionHandler,并在Spring配置中把它聲明為@ControllerAdvice。處理程序有一個(gè)@ExceptionHandler方法,可以處理所有ErrorResponse異常,其中包括所有內(nèi)置的web異常。您可以添加更多的異常處理方法,并使用protected方法將任何異常映射到ProblemDetail。
非標(biāo)準(zhǔn)字段
可以通過(guò)以下兩種方式之一使用非標(biāo)準(zhǔn)字段擴(kuò)展RFC7807響應(yīng)。
一、ProblemDetail類中有個(gè)Map集合的'properties'屬性。在使用Jackson庫(kù)時(shí),Spring框架注冊(cè)了ProblemDetailJacksonMixin,以確保這個(gè)“properties”映射被展開(kāi),并在響應(yīng)中作為頂級(jí)JSON屬性呈現(xiàn),同樣,反序列化期間的任何未知屬性都會(huì)插入到這個(gè)Map中。
你還可以擴(kuò)展ProblemDetail以添加專用的非標(biāo)準(zhǔn)屬性。ProblemDetail中的復(fù)制構(gòu)造函數(shù)允許從現(xiàn)有的ProblemDetail中輕松創(chuàng)建子類。這可以集中完成,例如從@ControllerAdvice,如ResponseEntityExceptionHandler,它將異常的ProblemDetail重新創(chuàng)建到一個(gè)具有額外非標(biāo)準(zhǔn)字段的子類中。
ProblemDetail類
public class ProblemDetail {
private static final URI BLANK_TYPE = URI.create("about:blank");
private URI type = BLANK_TYPE;
@Nullable
private String title;
private int status;
@Nullable
private String detail;
@Nullable
private URI instance;
@Nullable
private Map<String, Object> properties;
}
測(cè)試接口:
@RestController
@RequestMapping("/demo")
public class DemoController {
@GetMapping("")
public Object index(Integer age) {
System.out.println(1 / 0) ;
return "success" ;
}
}
示例1:
基礎(chǔ)使用
@RestControllerAdvice
public class GlobalExceptionHandler {
// 當(dāng)發(fā)生異常后直接返回ProblemDetail對(duì)象
@ExceptionHandler({Exception.class})
public ProblemDetail handle(Exception e) {
ProblemDetail detail = ProblemDetail.forStatusAndDetail(HttpStatusCode.valueOf(500), e.getMessage());
return detail ;
}
}
運(yùn)行結(jié)果:
示例2:
添加擴(kuò)展屬性
@RestControllerAdvice
public class GlobalExceptionHandler {
// 這里使用的是ErrorResponse
@ExceptionHandler({ArithmeticException.class})
public ErrorResponse handle(Exception e) {
ErrorResponse errorResponse = new ErrorResponseException(HttpStatusCode.valueOf(500), e) ;
errorResponse.getBody().setProperty("operator_time", new Date()) ;
return errorResponse ;
}
}
運(yùn)行結(jié)果:
示例3:
繼承自ResponseEntityExceptionHandler該類中定義了@ExceptionHandler注解的方法,能夠處理大多數(shù)常見(jiàn)的異常。
@ControllerAdvice
final class ProblemDetailsExceptionHandler extends ResponseEntityExceptionHandler {
}
ResponseEntityExceptionHandler
public abstract class ResponseEntityExceptionHandler implements MessageSourceAware {
@ExceptionHandler({
HttpRequestMethodNotSupportedException.class,
HttpMediaTypeNotSupportedException.class,
HttpMediaTypeNotAcceptableException.class,
MissingPathVariableException.class,
MissingServletRequestParameterException.class,
MissingServletRequestPartException.class,
ServletRequestBindingException.class,
MethodArgumentNotValidException.class,
NoHandlerFoundException.class,
AsyncRequestTimeoutException.class,
ErrorResponseException.class,
ConversionNotSupportedException.class,
TypeMismatchException.class,
HttpMessageNotReadableException.class,
HttpMessageNotWritableException.class,
BindException.class
})
@Nullable
public final ResponseEntity<Object> handleException(Exception ex, WebRequest request) throws Exception {
if (ex instanceof HttpMessageNotWritableException theEx) {
return handleHttpMessageNotWritable(theEx, headers, HttpStatus.INTERNAL_SERVER_ERROR, request);
}
// ...
}
protected ResponseEntity<Object> handleHttpMessageNotWritable(HttpMessageNotWritableException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) {
ProblemDetail body = createProblemDetail(ex, status, "Failed to write request", null, null, request);
return handleExceptionInternal(ex, body, headers, status, request);
}
}
該類是Spring提供的默認(rèn)實(shí)現(xiàn),要使用該類是需要通過(guò)如下配置開(kāi)啟:
spring.mvc.problemdetails.enabled=true
處理原理
當(dāng)返回結(jié)果是ProblemDetail或者ErrorResponse時(shí)通過(guò)如下類進(jìn)行解析處理:
public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodProcessor {
public boolean supportsReturnType(MethodParameter returnType) {
Class<?> type = returnType.getParameterType();
return ((HttpEntity.class.isAssignableFrom(type) && !RequestEntity.class.isAssignableFrom(type)) || ErrorResponse.class.isAssignableFrom(type) || ProblemDetail.class.isAssignableFrom(type));
}
public void handleReturnValue(...) throws Exception {
HttpEntity<?> httpEntity;
if (returnValue instanceof ErrorResponse response) {
httpEntity = new ResponseEntity<>(response.getBody(), response.getHeaders(), response.getStatusCode());
} else if (returnValue instanceof ProblemDetail detail) {
httpEntity = ResponseEntity.of(detail).build();
}
if (httpEntity.getBody() instanceof ProblemDetail detail) {
if (detail.getInstance() == null) {
URI path = URI.create(inputMessage.getServletRequest().getRequestURI());
detail.setInstance(path);
}
}
// ...
writeWithMessageConverters(httpEntity.getBody(), returnType, inputMessage, outputMessage);
outputMessage.flush();
}
}
以上就是ProblemDetail在Spring中的實(shí)現(xiàn)原理。