1. 概述

在多语言环境下设计应用时,我们经常需要提供本地化消息。根据用户选择的语言返回响应信息是常见需求。

当 REST 接口收到客户端请求时,必须先验证请求是否符合预定义规则,再进行后续处理。验证的目的是确保数据完整性和系统安全性。当验证失败时,服务需要返回明确的错误信息,指出请求中的问题。

本文将探讨如何在 REST 接口中实现本地化验证消息的完整方案。

2. 核心步骤

实现路径分为四个关键阶段:

  1. 资源包管理:使用属性文件存储本地化消息
  2. Spring Boot 集成:配置消息源实现动态检索
  3. 接口开发:创建带验证逻辑的 REST 服务
  4. 消息定制:覆盖默认消息、自定义资源包和动态消息生成

通过这些步骤,我们将掌握在多语言应用中提供精确、语言特定反馈的完整方案。

3. Maven 依赖

pom.xml 中添加核心依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

最新版本可在 Maven Central 获取

4. 本地化消息存储

在 Java 国际化应用中,属性文件是存储本地化消息的标准方案。这些文件本质是键值对文本:

  • 键:消息标识符
  • 值:对应语言的本地化消息

创建两个属性文件:

默认文件 CustomValidationMessages.properties(无语言后缀):

field.personalEmail=Personal Email
validation.notEmpty={field} cannot be empty
validation.email.notEmpty=Email cannot be empty

中文文件 CustomValidationMessages_zh.properties

field.personalEmail=個人電郵
validation.notEmpty={field}不能是空白
validation.email.notEmpty=電郵不能留空

⚠️ 关键提醒:所有属性文件必须使用 UTF-8 编码!这对处理中文/日文/韩文等非拉丁字符至关重要,避免乱码问题。

5. 本地化消息检索

Spring Boot 通过 MessageSource 接口简化消息检索。配置 ReloadableResourceBundleMessageSource 实现热加载(开发阶段无需重启服务器):

@Configuration
public class MessageConfig {

    @Bean
    public MessageSource messageSource() {
        ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
        messageSource.setBasename("classpath:CustomValidationMessages");
        messageSource.setDefaultEncoding("UTF-8");
        return messageSource;
    }
}

6. Bean 验证

创建 DTO 类 User,对 email 字段添加 @NotEmpty 注解:

public class User {
    
    @NotEmpty
    private String email;

    // getters and setters
}

7. REST 服务

创建 UserService 接口,通过 PUT 方法更新用户信息:

@RestController
public class UserService {

    @PutMapping(value = "/user", produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<UpdateUserResponse> updateUser(
      @RequestBody @Valid User user,
      BindingResult bindingResult) {

        if (bindingResult.hasFieldErrors()) {

            List<InputFieldError> fieldErrorList = bindingResult.getFieldErrors().stream()
              .map(error -> new InputFieldError(error.getField(), error.getDefaultMessage()))
              .collect(Collectors.toList());

            UpdateUserResponse updateResponse = new UpdateUserResponse(fieldErrorList);
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(updateResponse);
        }
        else {
            // 更新逻辑...
            return ResponseEntity.status(HttpStatus.OK).build();
        }
    }

}

7.1. 语言选择

使用 Accept-Language HTTP 头标识客户端语言偏好。Spring Boot 默认提供 LocaleResolver 自动解析该头信息:

accept-language: zh-tw

当请求语言不支持时,自动回退到英语。

7.2. 验证流程

通过 @Valid 注解触发验证,验证结果存储在 BindingResult 中:

@PutMapping("/user")
public ResponseEntity<?> updateUser(
    @RequestBody @Valid User user, 
    BindingResult bindingResult) {
    // 验证逻辑...
}

bindingResult.hasFieldErrors() 为真时,Spring Boot 自动根据当前语言填充本地化错误消息。

7.3. 响应对象

验证失败时返回 UpdateUserResponse

public class UpdateUserResponse {

    private List<InputFieldError> fieldErrors;

    // getter and setter
}

InputFieldError 封装错误详情:

public class InputFieldError {

    private String field;
    private String message;

    // getter and setter
}

8. 验证消息类型

测试请求体(故意留空 email):

{
    "email": ""
}

8.1. 标准消息

无语言头时的默认响应:

{
    "fieldErrors": [
        {
            "field": "email",
            "message": "must not be empty"
        }
    ]
}

添加中文头 accept-language: zh-tw 后:

{
    "fieldErrors": [
        {
            "field": "email",
            "message": "不得是空的"
        }
    ]
}

这些是 Hibernate Validator 提供的默认消息,但通常不够友好。

8.2. 覆盖默认消息

通过 ValidationMessages.properties 覆盖默认消息

英文版 ValidationMessages.properties

jakarta.validation.constraints.NotEmpty.message=The field cannot be empty

中文版 ValidationMessages_zh.properties

jakarta.validation.constraints.NotEmpty.message=本欄不能留空

更新后响应:

{
    "fieldErrors": [
        {
            "field": "email",
            "message": "The field cannot be empty"
        }
    ]
}

8.3. 自定义消息

在注解中直接引用资源包键值

public class User {
    
    @NotEmpty(message = "{validation.email.notEmpty}")
    private String email;

    // getter and setter
}

响应变为:

{
    "fieldErrors": [
        {
            "field": "email",
            "message": "Email cannot be empty"
        }
    ]
}

8.4. 插值消息

使用消息插值减少重复定义。创建自定义注解 @FieldNotEmpty

public class User {

    @FieldNotEmpty(
        message = "{validation.notEmpty}", 
        field = "{field.personalEmail}"
    )
    private String email;

    // getter and setter
}

定义注解:

@Documented
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
@Constraint(validatedBy = {FieldNotEmptyValidator.class})
public @interface FieldNotEmpty {

    String message() default "{validation.notEmpty}";

    String field() default "Field";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

验证器实现:

public class FieldNotEmptyValidator implements ConstraintValidator<FieldNotEmpty, Object> {

    @Override
    public boolean isValid(Object value, ConstraintValidatorContext context) {
        return (value != null && !value.toString().trim().isEmpty());
    }
}

此时响应:

{
    "fieldErrors": [
        {
            "field": "email",
            "message": "{field.personalEmail} cannot be empty"
        }
    ]
}

❌ 问题:占位符未被替换!

8.5. 自定义消息插值器

默认插值器仅处理单层占位符,需自定义递归插值器:

public class RecursiveLocaleContextMessageInterpolator extends AbstractMessageInterpolator {

    private static final Pattern PATTERN_PLACEHOLDER = Pattern.compile("\\{([^}]+)\\}");
    private final MessageInterpolator interpolator;

    public RecursiveLocaleContextMessageInterpolator(ResourceBundleMessageInterpolator interpolator) {
        this.interpolator = interpolator;
    }

    @Override
    public String interpolate(MessageInterpolator.Context context, Locale locale, String message) {
        int level = 0;
        while (containsPlaceholder(message) && (level++ < 2)) {
            message = this.interpolator.interpolate(message, context, locale);
        }
        return message;
    }

    private boolean containsPlaceholder(String code) {
        return PATTERN_PLACEHOLDER.matcher(code).find();
    }
}

MessageConfig 中注册:

@Bean
public MessageInterpolator getMessageInterpolator(MessageSource messageSource) {
    MessageSourceResourceBundleLocator locator = new MessageSourceResourceBundleLocator(messageSource);
    ResourceBundleMessageInterpolator interpolator = new ResourceBundleMessageInterpolator(locator);
    return new RecursiveLocaleContextMessageInterpolator(interpolator);
}

@Bean
public LocalValidatorFactoryBean getValidator(MessageInterpolator messageInterpolator) {
    LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean();
    bean.setMessageInterpolator(messageInterpolator);
    return bean;
}

最终完美响应:

{
    "fieldErrors": [
        {
            "field": "email",
            "message": "Personal Email cannot be empty"
        }
    ]
}

9. 总结

本文系统实现了 REST 接口的本地化验证消息方案:

  1. 存储方案:UTF-8 编码的属性文件资源包
  2. Spring 集成MessageSource 配置与热加载
  3. 验证机制:Bean Validation + 自定义注解
  4. 消息定制:覆盖默认消息 + 插值处理

通过组合这些技术,我们能在多语言应用中提供精准的本地化验证响应。完整代码示例见 GitHub 仓库