
上周Code Review,一个新同事提交了100多行异常处理代码。一个 Result
我问:"你知道Spring Boot 3已经内置了这套东西吗?"
他懵了。
大多数Java项目Controller里还在返回自封装的 Result
Spring Boot 3的ProblemDetail机制就是来解决这个问题的,而且这玩意儿是RFC 7807国际标准,不是Spring自己拍脑袋发明的。
先看效果。假设你有个接口:
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
return userService.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
}不开ProblemDetail时,返回的是一串白标错误页面或JSON:
{
"timestamp": "2026-06-29T10:30:00",
"status": 404,
"error": "Not Found",
"path": "/users/999"
}前端拿到这个,还得自己从 status 里推断到底出了什么问题。
开启ProblemDetail只需两件事。第一,application.yml 加一行:
spring:
mvc:
problemdetails:
enabled: true # 开启RFC 7807支持第二,自定义异常实现 ErrorResponseException:
// 业务异常统一继承ErrorResponseException,框架自动渲染
public class UserNotFoundException extends ErrorResponseException {
public UserNotFoundException(Long userId) {
// HttpStatus + ProblemDetail,框架自动拼装响应体
super(HttpStatus.NOT_FOUND,
ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND,
"用户 " + userId + " 不存在"), null);
// 设置type URI:指向你的错误码文档,前端可据此跳转
getBody().setType(URI.create("https://api.your-domain.com/errors/user-not-found"));
getBody().setTitle("用户不存在");
}
}请求 /users/999,返回:
{
"type": "https://api.your-domain.com/errors/user-not-found",
"title": "用户不存在",
"status": 404,
"detail": "用户 999 不存在",
"instance": "/users/999"
}前端拿到这个后,直接读 type 就能路由到对应的错误提示,读 detail 就能展示给用户。不需要任何Result包装类。
大部分人对ProblemDetail的误解是"那我原来写的 @RestControllerAdvice 是不是全废了"。不是,它是增强,不是替代。
原来的写法:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public Result handleBusiness(BusinessException e) {
return Result.error(e.getCode(), e.getMessage());
}
} 换成ProblemDetail版本,只需继承
ResponseEntityExceptionHandler:
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
// 处理自定义业务异常,返回ProblemDetail而非Result
@ExceptionHandler(BusinessException.class)
public ProblemDetail handleBusiness(BusinessException e) {
ProblemDetail pd = ProblemDetail.forStatusAndDetail(
HttpStatus.BAD_REQUEST, e.getMessage());
// 自定义扩展字段:带上业务错误码
pd.setProperty("bizCode", e.getCode());
pd.setProperty("timestamp", Instant.now());
return pd; // 注意:直接返回ProblemDetail,不用包在ResponseEntity里
}
// 处理参数校验异常——这个以前要写一堆代码
// 继承ResponseEntityExceptionHandler后,框架自动处理MethodArgumentNotValidException
@Override
protected ResponseEntity关键点:继承
ResponseEntityExceptionHandler 之后,Spring MVC 内置的40多种异常(
HttpMessageNotReadableException、TypeMismatchException、
MissingPathVariableException等等)全都会被自动转换成ProblemDetail格式。以前这些异常要么返回500白页,要么你一个个写Handler去兜底——现在全托管了。
RFC 7807规定 type、title、status、detail、instance 是标准字段,但你完全可以通过 setProperty() 扩展任意字段,比如带上traceId方便排查:
@ExceptionHandler(Exception.class)
public ProblemDetail handleUnknown(Exception e, HttpServletRequest request) {
ProblemDetail pd = ProblemDetail.forStatusAndDetail(
HttpStatus.INTERNAL_SERVER_ERROR, "服务器内部错误");
// 注入traceId:运维查日志时直接用这个ID定位
pd.setProperty("traceId", MDC.get("traceId"));
// 注入请求路径:方便前端判断要不要重试
pd.setProperty("path", request.getRequestURI());
return pd;
}返回:
{
"type": "about:blank",
"title": "Internal Server Error",
"status": 500,
"detail": "服务器内部错误",
"instance": "/api/orders/submit",
"traceId": "a1b2c3d4e5f6",
"path": "/api/orders/submit"
}前端收到500错误,直接把 traceId 截个图发给后端,后端 grep a1b2c3d4e5f6 app.log 秒定位。比原来靠时间戳翻日志快了不止一个数量级。
实事求是的说,ProblemDetail不是银弹。它主要解决的是异常响应的标准化问题,正常业务数据的返回格式你仍然可以自己做主。但项目里最让人头疼的从来不是正常返回,是异常情况下的各类兜底处理。ProblemDetail恰好替你扛住了这一块。
另外一个需要注意的点:ErrorResponseException 的构造方法里,如果 detail 参数直接拼接了用户输入(比如上面的 "用户 " + userId + " 不存在"),在前端渲染时要做好XSS防护。这个跟ProblemDetail本身没关系,是你写代码的基本安全习惯。
最后说一句。如果你的项目还在Spring Boot 2.x,没用ProblemDetail,那你维护的自定义Result体系继续用着,没问题。但如果项目已经是Spring Boot 3.x甚至更高版本,再多维护一套Result类就属于重复造轮子了。三行YAML配置能解决的问题,不值得写一百行代码。
大多数时候,我们不是缺技术方案,是没注意到框架已经替你做了。
更新时间:2026-07-02
本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828
© CopyRight All Rights Reserved.
Powered By 61893.com 闽ICP备11008920号
闽公网安备35020302035593号