java validation 的国际化

背景

在一个完整的项目里面,肯定是有各种各样的入参校验的,如果业务上的一些逻辑校验,可以放在 Service 层面进行,但是如果是 Controller 里面的校验,直接可以用 validation 进行验证。配合注解可以很方便的实现各种各样的入参校验。
如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class User {

@NotBlank(message = "用户名称不能为空")
private String name;

@Max(value = 18)
@Min(value = 10)
private Integer age;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Integer getAge() {
return age;
}

public void setAge(Integer age) {
this.age = age;
}
}

然后在Controller里面

1
2
3
4
5
6
7
8
9
@PostMapping(value = "test")
public String testError(@Valid @RequestBody User user ,Errors errors){
if(errors.hasErrors()){
for (ObjectError err : errors.getAllErrors()) {
return err.getDefaultMessage();
}
}
return "OK";
}

当入参的 JSON 如果 name 是空的话,就会直接返回 用户名称不能为空,当然这里是做了一些简化,返回的应该还有 Code 和 Message。这样的话一个简单的 Controller 检验入参就实现的,但是为了扩展性和可维护性,还需要考虑国际化以及可配置化

扩展性

如果在之后需要更改一个校验的提示语,那么在上面的代码里面是需要修改代码的,那么最好的办法就是把这些提示信息都加到配置文件中,那么以后需要修改某些提示的话,直接改配置文件即可。
在 Spring 官方文档的 4.7.1 中有一个类 MessageCodesResolver 可以用来实现错误信息的可配置化

MessageCodesResolver

这个类需要和spring.mvc.message-codes-resolver-format一起来使用,根据官方文档的提示,查看DefaultMessageCodesResolver.Format,会发现这个参数有两个枚举,一个是 PREFIX_ERROR_CODE,另一个是 POSTFIX_ERROR_CODE。
这两个什么意思呢,简单来讲,根据上面的 User 类,如果我要配置当 name 不为空的提示语,下面两种枚举对应在配置文件中的键值是不一样的。

  1. PREFIX_ERROR_CODE

    NotBlank.user.name = 用户名称不能为空

  2. POSTFIX_ERROR_CODE。

    user.name.NotBlank = 用户名称不能为空

个人偏向于第二种写法的,因为感觉第二种更加符合阅读习惯。

messgae配置文件

既然需要做成配置文件,那么在 application.yml 里面把一些属性都配置好,如下:

1
2
3
4
5
spring:
mvc:
message-codes-resolver-format: postfix_error_code
messages:
basename: i18n/validation

然后在 Resources 下面新建一个 validation.properties

1
2
3
user.name.NotBlank = 用户名称不能为空
user.age.Max = 年龄最高不能高于18
user.age.Min = 年龄最高不能低于10

Controller 校验

1
2
3
4
5
6
7
8
9
10
11
12
13
@Autowired
MessageSource messageSource;

@PostMapping(value = "test/cn")
public String testErrorCn(@Valid @RequestBody User user ,Errors errors){
if(errors.hasErrors()){
for (ObjectError err : errors.getAllErrors()) {
String msg = messageSource.getMessage(err, Locale.CHINA);
return msg;
}
}
return "OK";
}

在这里暂时先不管 Locale.CHINA 这个参数的含义,首先通过 errors 先判断入参是否有问题,有的话直接通过 messageSource.getMessage() 方法直接返回错误信息即可。

PsotMan测试

1
2
3
4
POST /test/cn HTTP/1.1
Host: localhost:8071

{"age":15}

返回:用户名称不能为空

那么以后如果需要修改返回提示的话,直接修改配置文件即可,从而不需要修改代码。

国际化

如果需要将该提示国际化,直接修改配置文件即可,新建两个配置文件validation_zh_CN.propertiesvalidation_en_US.properties,然后分别新建提示配置如下:

1
2
3
4
5
6
7
8
9
validation_zh_CN.properties:
user.name.NotBlank = 用户名称不能为空
user.age.Max = 年龄最高不能高于18
user.age.Min = 年龄最高不能低于10

validation_en_US.properties:
user.name.NotBlank = user name can not be null
user.age.Max = the max gae can not grater than 18
user.age.Min = the max gae can not less than 10

此时在Controller里面代如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@RestController
@ResponseBody
public class StartController {

@Autowired
MessageSource messageSource;

@PostMapping(value = "test")
public String testError(@Valid @RequestBody User user ,Errors errors){
if(errors.hasErrors()){
for (ObjectError err : errors.getAllErrors()) {
return err.getDefaultMessage();
}
}
return "OK";
}

@PostMapping(value = "test/cn")
public String testErrorCn(@Valid @RequestBody User user ,Errors errors){
if(errors.hasErrors()){
for (ObjectError err : errors.getAllErrors()) {
String msg = messageSource.getMessage(err, Locale.CHINA);
return msg;
}
}
return "OK";
}

@PostMapping(value = "test/en")
public String testErrorEn(@Valid @RequestBody User user ,Errors errors){
if(errors.hasErrors()){
for (ObjectError err : errors.getAllErrors()) {
String msg = messageSource.getMessage(err, Locale.US);
return msg;
}
}
return "OK";
}
}

User的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class User {

@NotBlank
private String name;

@Max(value = 18)
@Min(value = 10)
private Integer age;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Integer getAge() {
return age;
}

public void setAge(Integer age) {
this.age = age;
}
}

当我们请求 test/entest/cn的时候就会出现不同的提示,

  1. test/en

    user name can not be null

  2. test/cn

    用户名称不能为空

这其中重要的实现就是 Locale.US 和 Locale.CHINA,在这里实现的话,最好是根据用户的选择语言来动态的切换实现Local的转换。

作者

Somersames

发布于

2019-12-09

更新于

2021-12-05

许可协议

评论