1. 概述
在多语言环境下设计应用时,我们经常需要提供本地化消息。根据用户选择的语言返回响应信息是常见需求。
当 REST 接口收到客户端请求时,必须先验证请求是否符合预定义规则,再进行后续处理。验证的目的是确保数据完整性和系统安全性。当验证失败时,服务需要返回明确的错误信息,指出请求中的问题。
本文将探讨如何在 REST 接口中实现本地化验证消息的完整方案。
2. 核心步骤
实现路径分为四个关键阶段:
- ✅ 资源包管理:使用属性文件存储本地化消息
- ✅ Spring Boot 集成:配置消息源实现动态检索
- ✅ 接口开发:创建带验证逻辑的 REST 服务
- ✅ 消息定制:覆盖默认消息、自定义资源包和动态消息生成
通过这些步骤,我们将掌握在多语言应用中提供精确、语言特定反馈的完整方案。
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 接口的本地化验证消息方案:
- ✅ 存储方案:UTF-8 编码的属性文件资源包
- ✅ Spring 集成:
MessageSource
配置与热加载 - ✅ 验证机制:Bean Validation + 自定义注解
- ✅ 消息定制:覆盖默认消息 + 插值处理
通过组合这些技术,我们能在多语言应用中提供精准的本地化验证响应。完整代码示例见 GitHub 仓库。