Spring Framework 6.x 异常国际化完全指南:让错误信息“说“多国语言

前言

Spring的异常信息存在一个问题,输出的异常详情都是英文,国内的开发者不得不自行进行异常的拦截和处理,以满足系统显示的需要。

Spring Framework 6.x 引入了全新的异常处理机制,通过 ProblemDetailErrorResponse 接口,为异常国际化提供了优雅的解决方案。

💡 开源推荐 :我已经将这套异常国际化方案开源,项目地址: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 状态码
}

国际化流程

  1. 初始化时设置默认的 detail 信息(英文)
  2. 通过 ErrorResponse.updateAndGetBody() 方法,使用 MessageSource 进行国际化
  3. 重新设置国际化后的消息

注意:设置的是默认值,国际化失败时会回退到默认值

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 通过 ProblemDetailErrorResponse 接口,为异常国际化提供了优雅的解决方案。本文介绍了:

  1. ProblemDetail:标准化的错误响应格式
  2. ErrorResponse:支持国际化的异常接口
  3. 资源文件配置:多语言消息的组织方式
  4. 实战示例:完整的多语言 API 实现

关键要点

  • 实现自定义异常时,优先实现 ErrorResponse 接口
  • 正确覆盖 getDetailArguments() 方法提供消息参数
  • 合理配置 MessageSource 缓存策略
  • 使用完整类名作为 message code 前缀

通过这套机制,你可以轻松构建支持多语言的全球化应用,为不同地区的用户提供友好的错误提示。

🚀 开源项目推荐spring-exception-i18n 提供了完整的实现和丰富的示例,开箱即用!


参考文档

相关推荐
ss2732 小时前
CompletionService:Java并发工具包
java·开发语言·算法
晓13132 小时前
后端篇——第一章 Maven基础全面教程
java·maven
二进制_博客2 小时前
JWT权限认证快速入门
java·开发语言·jwt
素素.陈2 小时前
根据图片中的起始位置的特殊内容将图片进行分组
java·linux·windows
鱼跃鹰飞2 小时前
面试题:Spring事务失效的八大场景
数据库·mysql·spring
2301_780669862 小时前
GUI编程(常用组件、事件、事件常见写法)
java
brevity_souls2 小时前
Java 中 String、StringBuffer 和 StringBuilder
java·开发语言
ss2732 小时前
类的线程安全:多线程编程-银行转账系统:如果两个线程同时修改同一个账户余额,没有适当的保护机制,会发生什么?
java·开发语言·数据库
Victor3563 小时前
Hibernate(18)Hibernate的延迟加载是什么?
后端