springboot 之 i18n国际化 二(参数校验提示国际化)

在 Java(尤其是 Spring Boot)中,注解校验 + i18n 国际化 是实现多语言友好错误提示的关键技术。它允许你使用 @NotNull@NotBlank 等 Bean Validation 注解,并根据用户语言环境返回对应语言的错误信息。

下面从 原理、配置步骤、完整示例高级技巧 详细说明。


一、核心原理

1. 技术栈组合

  • Bean Validation (JSR-380):如 Hibernate Validator(Spring Boot 默认集成)
  • 国际化(i18n):通过 MessageSource 加载不同语言的错误消息文件
  • Spring MVC:自动将校验失败结果绑定到 BindingResult,并支持国际化错误码解析

2. 工作流程

  1. 用户提交表单/JSON 请求
  2. Controller 方法参数标注 @Valid
  3. Spring 调用 Hibernate Validator 执行校验
  4. 校验失败时,生成 ConstraintViolation,其 默认错误码 如:
    • javax.validation.constraints.NotNull.message
    • org.hibernate.validator.constraints.Length.message
  5. Spring 从 MessageSource 中查找该错误码对应的 国际化消息
  6. 返回对应语言的错误提示(如中文“姓名不能为空”)

✅ 关键:自定义 ValidationMessages.properties 文件覆盖默认英文提示


二、完整实现步骤(Spring Boot 示例)

步骤 1:添加依赖(通常已包含)

<!-- Spring Boot Web 自动引入 Hibernate Validator -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

步骤 2:创建国际化校验消息文件

src/main/resources 下创建:

resources/
└── i18n/
    ├── ValidationMessages.properties       # 默认(兜底)
    ├── ValidationMessages_zh_CN.properties # 简体中文
    └── ValidationMessages_en_US.properties # 英文

⚠️ 必须命名为 ValidationMessages.properties!这是 Bean Validation 规范约定的默认 basename。

文件内容示例:

# ValidationMessages.properties(默认)
javax.validation.constraints.NotBlank.message=must not be blank
javax.validation.constraints.Size.message=size must be between {min} and {max}

# ValidationMessages_zh_CN.properties
javax.validation.constraints.NotBlank.message=不能为空
javax.validation.constraints.Size.message=长度必须在{min}到{max}之间
org.hibernate.validator.constraints.Length.message=长度必须为{min}到{max}个字符

# 自定义错误码(推荐)
user.name.size=用户名长度必须为2-20个字符
user.email.format=邮箱格式不正确

💡 占位符说明

  • {min}, {max}:来自注解属性(如 @Size(min=2, max=20)
  • 你也可以在自定义消息中使用这些变量

步骤 3:配置 MessageSource(可选,但推荐)

虽然 ValidationMessages 会被自动加载,但建议显式配置以统一管理:

// config/I18nConfig.java
@Configuration
public class I18nConfig {

    @Bean
    public MessageSource messageSource() {
        ResourceBundleMessageSource source = new ResourceBundleMessageSource();
        source.setBasename("i18n/ValidationMessages"); // 注意:不带 .properties
        source.setDefaultEncoding("UTF-8");
        source.setFallbackToSystemLocale(false);
        return source;
    }

    // 同时配置 LocaleResolver(用于切换语言)
    @Bean
    public LocaleResolver localeResolver() {
        AcceptHeaderLocaleResolver resolver = new AcceptHeaderLocaleResolver();
        resolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
        return resolver;
    }
}

✅ 如果你已有全局 messages.properties不要混淆

  • ValidationMessages.properties仅用于校验错误
  • messages.properties → 用于 @Value("#{...}") 或手动 messageSource.getMessage()

步骤 4:定义 DTO 并使用校验注解

import javax.validation.constraints.*;

public class UserDTO {
    
    @NotBlank(message = "{user.name.size}") // 使用自定义错误码
    @Size(min = 2, max = 20)
    private String name;

    @Email(message = "{user.email.format}")
    private String email;

    @Min(value = 18, message = "年龄不能小于{value}岁") // 直接写消息(不推荐多语言)
    private Integer age;

    // getter/setter...
}

最佳实践

  • 使用 {xxx} 引用 ValidationMessages 中的 key,而非硬编码消息
  • 这样才能实现真正的国际化

步骤 5:Controller 中处理校验结果

@RestController
public class UserController {

    @PostMapping("/users")
    public ResponseEntity<?> createUser(
            @Valid @RequestBody UserDTO user,
            BindingResult bindingResult) {

        if (bindingResult.hasErrors()) {
            List<String> errors = bindingResult.getFieldErrors().stream()
                .map(FieldError::getDefaultMessage) // 自动国际化!
                .collect(Collectors.toList());
            return ResponseEntity.badRequest().body(errors);
        }

        // 保存用户...
        return ResponseEntity.ok("Success");
    }
}

🔍 关键点
FieldError.getDefaultMessage() 会自动调用 MessageSource,根据当前 Locale 返回对应语言的消息!


步骤 6:测试多语言效果

请求 1:Accept-Language: zh-CN

POST /users
Content-Type: application/json
Accept-Language: zh-CN

{"name": "", "email": "invalid", "age": 10}

响应

[
  "不能为空",
  "邮箱格式不正确",
  "年龄不能小于18岁"
]

请求 2:Accept-Language: en-US

Accept-Language: en-US

响应

[
  "must not be blank",
  "not a well-formed email address",
  "年龄不能小于18岁"  // ← 注意:这条是硬编码,不会翻译!
]

⚠️ 再次强调:只有通过 {key} 引用的消息才会国际化


三、高级技巧

1. 自定义校验注解 + 国际化

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneNumberValidator.class)
public @interface ValidPhoneNumber {
    String message() default "{user.phone.invalid}"; // 国际化 key
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

// ValidationMessages_zh_CN.properties
user.phone.invalid=手机号格式不正确

2. 动态参数传递(如字段名)

# ValidationMessages_zh_CN.properties
user.field.required={0} 不能为空
@NotBlank(message = "{user.field.required}")
private String name;

❌ 但注意:Hibernate Validator 不支持 {0} 自动替换字段名
✅ 解决方案:使用 @FieldMatch 等自定义注解,或在 Controller 中手动拼接。

3. 全局异常处理(推荐方式)

避免每个 Controller 写 BindingResult

@RestControllerAdvice
public class ValidationExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<List<String>> handleValidationExceptions(
            MethodArgumentNotValidException ex) {
        return ResponseEntity.badRequest().body(
            ex.getBindingResult().getFieldErrors().stream()
                .map(FieldError::getDefaultMessage)
                .collect(Collectors.toList())
        );
    }
}

四、常见问题排查

问题 原因 解决方案
错误消息仍是英文 未创建 ValidationMessages_zh_CN.properties 检查文件名、路径、编码
中文乱码 文件非 UTF-8 编码 IDE 中设置 .properties 为 UTF-8,并启用 ASCII 转换
自定义消息未生效 message = "xxx" 而非 message = "{key}" 改为引用资源文件中的 key
语言未切换 未配置 LocaleResolver 添加 AcceptHeaderLocaleResolver 或参数切换

总结

实现要点

  1. 创建 ValidationMessages_{lang}.properties 文件
  2. 注解中使用 message = "{your.key}" 引用 key
  3. 确保 MessageSource 能加载到这些文件
  4. 通过 Accept-Language 或 URL 参数切换语言
posted @ 2026-01-21 14:06  蓝迷梦  阅读(4)  评论(0)    收藏  举报