前言
Spring的异常信息存在一个问题,输出的异常详情都是英文,国内的开发者不得不自行进行异常的拦截和处理,以满足系统显示的需要。
Spring Framework 6.x 引入了全新的异常处理机制,通过 ProblemDetail 和 ErrorResponse 接口,为异常国际化提供了优雅的解决方案。
💡 开源推荐 :我已经将这套异常国际化方案开源,项目地址:spring-exception-i18n,提供了完整的实现和丰富的示例,开箱即用!
一、ProblemDetail:Spring Framework 6.x 的异常标准化方案
1.1 什么是 ProblemDetail
ProblemDetail 是 Spring 5.0 引入的一个类,用于表示 HTTP 错误响应的详细信息。它遵循 RFC 7807 Problem Details for HTTP APIs 规范,提供了一种标准化的错误响应格式。
ProblemDetail 的核心属性:
java
public class ProblemDetail {
private URI type; // 错误类型标识
private String title; // 错误标题
private String detail; // 错误详细信息
private URI instance; // 具体问题实例
private int status; // HTTP 状态码
}
1.2 ProblemDetail 关键特性
核心属性:
java
public class ProblemDetail {
private URI type; // 错误类型标识
private String title; // 错误标题
private String detail; // 错误详细信息
private URI instance; // 具体问题实例
private int status; // HTTP 状态码
}
国际化流程:
- 初始化时设置默认的 detail 信息(英文)
- 通过
ErrorResponse.updateAndGetBody()方法,使用MessageSource进行国际化 - 重新设置国际化后的消息
注意:设置的是默认值,国际化失败时会回退到默认值
1.3 ProblemDetail 在异常处理中的应用
Spring Framework 6.x 中,许多内置异常都实现了 ErrorResponse 接口,可以返回 ProblemDetail:
java
// 内置异常示例
public class MethodNotAllowed extends HttpResponseException {
@Override
protected ResponseEntity<?> toResponse(HttpStatusCode status) {
ProblemDetail problemDetail = ProblemDetail.forStatus(status);
problemDetail.setType(URI.create("https://spring.io/errors/method-not-allowed"));
problemDetail.setTitle("Method Not Allowed");
problemDetail.setDetail("Specified HTTP method is not allowed for this resource.");
return ResponseEntity.status(status).body(problemDetail);
}
}
响应示例:
json
{
"type": "https://spring.io/errors/method-not-allowed",
"title": "Method Not Allowed",
"detail": "Specified HTTP method is not allowed for this resource.",
"status": 405,
"instance": "/api/users"
}
1.4 自定义异常使用 ProblemDetail
java
public class UserNotFoundException extends RuntimeException implements ErrorResponse {
private final ProblemDetail problemDetail;
public UserNotFoundException(String userId) {
this.problemDetail = ProblemDetail.forStatus(HttpStatus.NOT_FOUND);
this.problemDetail.setType(URI.create("https://api.example.com/errors/user-not-found"));
this.problemDetail.setTitle("User Not Found");
this.problemDetail.setDetail(String.format("User with id '%s' was not found", userId));
}
@Override
public ProblemDetail getBody() {
return this.problemDetail;
}
@Override
public HttpStatusCode getStatusCode() {
return HttpStatus.NOT_FOUND;
}
}
二、ErrorResponse 接口:国际化的核心
2.1 ErrorResponse 接口
ErrorResponse 是 Spring 6.x 引入的接口,所有需要返回结构化错误响应的异常都应该实现此接口:
java
public interface ErrorResponse {
ProblemDetail getBody(); // 返回 ProblemDetail 对象
HttpStatusCode getStatusCode(); // 返回 HTTP 状态码
HttpHeaders getHeaders(); // 返回响应头(可选)
// 核心方法:更新并国际化错误详情
ProblemDetail updateAndGetBody(MessageSource messageSource, Locale locale);
// 可选方法:自定义消息 code
String getTypeMessageCode(); // 获取 type 的国际化消息 code
String getDetailMessageCode(); // 获取 detail 的国际化消息 code
String getTitleMessageCode(); // 获取 title 的国际化消息 code
Object[] getDetailArguments(); // 获取 detail 消息的参数
}
2.2 国际化消息 Code 生成规则
Spring 使用完整的类名作为消息 code 前缀:
| Code 类型 | 生成规则 | 示例 |
|---|---|---|
type |
problemDetail.type.{全类名} |
problemDetail.type.org.example.UserNotFoundException |
detail |
problemDetail.{全类名} |
problemDetail.org.example.UserNotFoundException |
title |
problemDetail.title.{全类名} |
problemDetail.title.org.example.UserNotFoundException |
注意 :Spring 6.x 使用完整类名而不是简化名称
2.3 实现支持国际化的自定义异常
推荐方案:使用默认 Message Code
java
public class ResourceNotFoundException extends RuntimeException implements ErrorResponse {
private final ProblemDetail problemDetail;
private final String resourceName;
private final String resourceId;
public ResourceNotFoundException(String resourceName, String resourceId) {
this.resourceName = resourceName;
this.resourceId = resourceId;
this.problemDetail = ProblemDetail.forStatus(HttpStatus.NOT_FOUND);
this.problemDetail.setType(URI.create("https://api.example.com/errors/not-found"));
this.problemDetail.setTitle("Resource Not Found");
this.problemDetail.setDetail(String.format("%s with id '%s' was not found", resourceName, resourceId));
}
@Override
public ProblemDetail getBody() {
return this.problemDetail;
}
@Override
public HttpStatusCode getStatusCode() {
return HttpStatus.NOT_FOUND;
}
// 提供消息参数
@Override
public Object[] getDetailMessageArguments() {
return new Object[]{this.resourceName, this.resourceId};
}
}
资源文件示例 (messages_zh_CN.properties):
properties
problemDetail.type.com.example.ResourceNotFoundException=https://api.example.com/errors/not-found
problemDetail.title.com.example.ResourceNotFoundException=资源未找到
problemDetail.com.example.ResourceNotFoundException=ID 为 ''{1}'' 的 {0} 未找到
优势:
- 自动使用完整类名,避免命名冲突
- 代码简洁,无需手动定义 message code
三、国际化资源文件配置
3.1 资源文件结构
src/main/resources/
├── messages/
│ ├── messages.properties (默认 - 英文)
│ ├── messages_zh_CN.properties (简体中文)
│ └── messages_ja_JP.properties (日语)
3.2 常用异常资源文件示例
messages_zh_CN.properties(简体中文):
properties
# 自定义异常
problemDetail.title.com.example.ResourceNotFoundException=资源未找到
problemDetail.com.example.ResourceNotFoundException=ID 为 ''{1}'' 的 {0} 未找到
# Spring 内置异常
problemDetail.title.org.springframework.web.HttpRequestMethodNotSupportedException=不支持的请求方法
problemDetail.org.springframework.web.HttpRequestMethodNotSupportedException=不支持 HTTP 方法 ''{0}''。支持的方法:{1}
problemDetail.title.org.springframework.web.HttpMediaTypeNotSupportedException=不支持的媒体类型
problemDetail.org.springframework.web.HttpMediaTypeNotSupportedException=不支持内容类型 ''{0}''
problemDetail.title.org.springframework.web.bind.MissingServletRequestParameterException=缺少请求参数
problemDetail.org.springframework.web.bind.MissingServletRequestParameterException=缺少必需的请求参数 ''{0}''
problemDetail.title.org.springframework.web.bind.MethodArgumentNotValidException=验证失败
problemDetail.org.springframework.web.bind.MethodArgumentNotValidException=请求内容无效
3.3 常见 Spring 内置异常
| 异常类 | HTTP状态码 | Message Code |
|---|---|---|
HttpRequestMethodNotSupportedException |
405 | problemDetail.org.springframework.web.HttpRequestMethodNotSupportedException |
HttpMediaTypeNotSupportedException |
415 | problemDetail.org.springframework.web.HttpMediaTypeNotSupportedException |
MissingServletRequestParameterException |
400 | problemDetail.org.springframework.web.bind.MissingServletRequestParameterException |
MethodArgumentNotValidException |
400 | problemDetail.org.springframework.web.bind.MethodArgumentNotValidException |
💡 提示 :完整的资源文件示例请参考 spring-exception-i18n 开源项目
四、MessageSource 配置
4.1 基础配置
java
@Configuration
public class I18nConfig {
@Bean
public MessageSource messageSource() {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setBasenames("messages/messages");
messageSource.setDefaultEncoding("UTF-8");
messageSource.setCacheSeconds(3600);
return messageSource;
}
}
4.2 配置 LocaleResolver
java
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Bean
public LocaleResolver localeResolver() {
CookieLocaleResolver resolver = new CookieLocaleResolver();
resolver.setCookieName("language");
resolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
return resolver;
}
@Bean
public LocaleChangeInterceptor localeChangeInterceptor() {
LocaleChangeInterceptor interceptor = new LocaleChangeInterceptor();
interceptor.setParamName("lang");
return interceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(localeChangeInterceptor());
}
}
## 五、全局异常处理器
### 5.1 实现国际化的全局异常处理器
```java
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
@Autowired
private MessageSource messageSource;
// 处理 ErrorResponse 异常
@ExceptionHandler(ErrorResponseException.class)
public ResponseEntity<ProblemDetail> handleErrorResponseException(
ErrorResponseException ex, Locale locale) {
ProblemDetail body = ex.updateAndGetBody(messageSource, locale);
return ResponseEntity.status(ex.getStatusCode()).body(body);
}
// 处理参数校验异常
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(
MethodArgumentNotValidException ex,
HttpHeaders headers,
HttpStatusCode status,
WebRequest request) {
Locale locale = request.getLocale();
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error -> {
String message = messageSource.getMessage(
error.getDefaultMessage(),
new Object[]{error.getField()},
error.getDefaultMessage(),
locale
);
errors.put(error.getField(), message);
});
Map<String, Object> body = Map.of(
"code", "VALIDATION_ERROR",
"message", messageSource.getMessage("validation.failed", null, "Validation failed", locale),
"errors", errors
);
return ResponseEntity.badRequest().body(body);
}
}
六、实战案例
6.1 异常定义
java
public class UserNotFoundException extends RuntimeException implements ErrorResponse {
private final ProblemDetail problemDetail;
private final Long userId;
public UserNotFoundException(Long userId) {
this.userId = userId;
this.problemDetail = ProblemDetail.forStatus(HttpStatus.NOT_FOUND);
this.problemDetail.setType(URI.create("https://api.example.com/errors/user-not-found"));
this.problemDetail.setTitle("User Not Found");
this.problemDetail.setDetail(String.format("User with id %d not found", userId));
}
@Override
public ProblemDetail getBody() {
return this.problemDetail;
}
@Override
public HttpStatusCode getStatusCode() {
return HttpStatus.NOT_FOUND;
}
@Override
public Object[] getDetailArguments() {
return new Object[]{this.userId};
}
}
6.2 控制器使用
java
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
return userService.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
}
}
6.3 测试结果
中文响应:
json
{
"type": "https://api.example.com/errors/user-not-found",
"title": "用户未找到",
"detail": "ID 为 999 的用户未找到",
"status": 404
}
英文响应:
json
{
"type": "https://api.example.com/errors/user-not-found",
"title": "User Not Found",
"detail": "User with id 999 not found",
"status": 404
}
七、最佳实践
7.1 资源文件管理
properties
# 使用层级结构命名
error.user.notFound=User not found
error.validation.fieldRequired=Field ''{0}'' is required
# 参数命名清晰
error.validation.fieldInvalid=Field ''{0}'' has invalid value: ''{1}''
7.2 性能优化
java
@Bean
public MessageSource messageSource() {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setCacheSeconds(-1); // 生产环境永久缓存
return messageSource;
}
7.3 常见问题
Q: 国际化消息未生效?
A: 检查资源文件路径和编码:messageSource.setDefaultEncoding("UTF-8")
Q: 参数替换不生效?
A: 确保 getDetailArguments() 返回数组:return new Object[]{arg1, arg2}
八、总结
Spring Framework 6.x 通过 ProblemDetail 和 ErrorResponse 接口,为异常国际化提供了优雅的解决方案。本文介绍了:
- ProblemDetail:标准化的错误响应格式
- ErrorResponse:支持国际化的异常接口
- 资源文件配置:多语言消息的组织方式
- 实战示例:完整的多语言 API 实现
关键要点:
- 实现自定义异常时,优先实现
ErrorResponse接口 - 正确覆盖
getDetailArguments()方法提供消息参数 - 合理配置
MessageSource缓存策略 - 使用完整类名作为 message code 前缀
通过这套机制,你可以轻松构建支持多语言的全球化应用,为不同地区的用户提供友好的错误提示。
🚀 开源项目推荐 :spring-exception-i18n 提供了完整的实现和丰富的示例,开箱即用!
参考文档:
- RFC 7807 - Problem Details for HTTP APIs
- Spring Framework 6.x 官方文档
- Spring Boot 3.x 国际化指南