前言:当我们用@valid或者@validate验证controller层接收前端发来的对象数据时,在对象的实体类上的validation相关的验证注解有起效了,很多时候我们会写message=“xxx”自定义验证不通过的内容。例如:@NotNull(message = "id不能为空")很不优雅。点开源码可以看见默认的message,String message() default "{javax.validation.constraints.NotNull.message}";,这就相当优雅了,设想我们有很多的业务有很多的业务提示,直接写死的话不够优雅,后期也不方便待修改,于是就有了这篇博客,使用国际化优雅的返回提示信息。本文从对validator国际化的用法开始介绍。

validator数据验证

数据验证给了我们很多方便,避免了不少在接收完参数之后逐一验证参数的合法性所写的大量验证代码。

相关注解

  • @NotNull不为空
  • @NotBlank不为空白
  • NotEmpty至少有一个
  • @Range指定范围
  • @Length指定长度范围
  • @Min不能小于最小值
  • @Max不能大于最大值
  • @Email邮箱验证
  • @URL指定URL

还有很多注解可查看javax.validationorg.hibernate.validator包。

@Validated 和 @Valid

@Valid@Validated都可以用于验证,@Valid是JSR-303规范的注解,可以用在参数、属性、嵌套属性上,@Validated不能用在属性上。
@Validated支持分组。注解源码如下:

1
2
3
4
5
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Valid {
}
1
2
3
4
5
6
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Validated {
Class<?>[] value() default {};
}

自定义注解验证

可以自定义验证的注解,需要实现ConstraintValidator注解。以下写一个样例,功能是验证地址只能是自己的安全列表中的地址。

@MustIn:

1
2
3
4
5
6
7
8
9
10
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = MustInConstraintValidator.class)
public @interface MustIn {

String message() default "不可输入非法地址";

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

MustInConstraintValidator:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MustInConstraintValidator implements ConstraintValidator<MustIn,String> {

private final String[] local = {"安徽","淮南","寿县","北京","合肥","上海"};

private final Log logger = LogFactory.getLog(MustInConstraintValidator.class);

@Override
public void initialize(MustIn mustIn) {
logger.info("初始化自定义validate注解MustIn");
}

@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
// 在local数组中就可
return "".equals(value) || Arrays.stream(local).parallel().anyMatch(e -> e.equals(value));
}
}

用法

相关依赖:

不要直接cv,自己找一下对应的版本号。

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
41
42
43
44
45
46
47
48
<!-- hibernate-validator -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>${hibernate.version}</version>
</dependency>
<!-- validation-api -->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
</dependency>
<!--java.el-->
<dependency>
<groupId>javax.el</groupId>
<artifactId>javax.el-api</artifactId>
<version>${javax-el.version}</version>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.el</artifactId>
<version>${javax-el.version}</version>
</dependency>
<!-- hibernate -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
</dependency>
<!-- hibernate-core -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-envers</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-c3p0</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-proxool</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-ehcache</artifactId>
</dependency>

实体类Student:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student {

@NotNull(message = "id不能为空")
private Long id;

@NotNull(message = "插入时,name不能为空", groups = {InsertGroup.class})
@NotBlank(message = "修改时,name不能为空", groups = {UpdateGroup.class})
private String name;

@MustIn(message = "不可输入非法地址")
private String local;

@Length(min = 11, max = 11, message = "11位手机号")
private String tel;
}

controller层DemoController:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestController
@RequestMapping("/demo")
public class DemoController {

@Autowired
private MessageSource messageSource;

private final Log logger = LogFactory.getLog(MustInConstraintValidator.class);

@PostMapping("validate")
public void testValidator(@Validated({InsertGroup.class, Default.class}) @RequestBody Student student){
logger.info(student);
}
}

异常拦截(我直接返回字符串了,一般会封装一个响应类)

1
2
3
4
5
6
7
8
9
@RestControllerAdvice(annotations = {RestController.class, Controller.class})
public class ExceptionHandle {

@ResponseStatus(HttpStatus.EXPECTATION_FAILED)
@ExceptionHandler(MethodArgumentNotValidException.class)
public String validate(MethodArgumentNotValidException e){
return Objects.requireNonNull(e.getBindingResult().getFieldError()).getDefaultMessage();
}
}

postman测试一下下:

ok 下面介绍国际化

国际化

springboot对国际化的支持很好,只需要简单的配置就可以实现。

1 引入依赖(省略)
2 配置
application.yml

1
2
3
spring: 
messages:
basename: i18n/test

3 创建国际化文件
basname指定的位置创建也就是在resources下创建i18n文件夹,在i18n中创建test.properties相关文件。


注意格式basename_local.properties是下划线。

4 配置类

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
@Configuration
public class ValidatorConfig {

@Autowired
private MessageSource messageSource;

/**
* Description 配置validator<br/>
* date 2022/3/4 10:47 <br/>
* @author TabTan <br/>
* @return Validator
*/
@Bean
public Validator getValidator() {
LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
validator.setValidationMessageSource(this.messageSource);
return validator;
}

/**
* Description 配置本地解析器<br/>
* date 2022/3/4 10:48 <br/>
* @author TabTan <br/>
* @return LocaleResolver
*/
@Bean
public LocaleResolver localeResolver() {
AcceptHeaderLocaleResolver acceptHeaderLocaleResolver = new AcceptHeaderLocaleResolver();
// 设置默认的Local为中文
acceptHeaderLocaleResolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
return acceptHeaderLocaleResolver;
}
}

5 国际化文件的内容:

test.properties

1
test = test

test_en_US.properties

1
test = hihi

test_zh_CN.properties

1
test = 嗨嗨

6 代码调用测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestController
@RequestMapping("/demo")
public class DemoController {

@Autowired
private MessageSource messageSource;

private final Log logger = LogFactory.getLog(DemoController.class);

@GetMapping("i18n")
public String i18nTest(){
return messageSource.getMessage("test",null, LocaleContextHolder.getLocale());
}
}

postman 调用查看
中文(配置类中的默认)

英文(添加请求头Accept-Language值为en-US)注意en-US不是下划线

自定义业务验证提示国际化文件

严重注解中的message可以指定国际化配置。如@NotNull(message = "{student.idNotNull}")只需要在国际化配置中配置tudent.idNotNull即可。问题是所有的业务配置全部都写在一个国际化文件里岂不是很乱,很不优雅。最好是一个业务一个国际化配置。那么咋么实现呢?在配置文件中可以这样配置spring.messages.basename=i18n/test,i18n/student中间用逗号隔开。但直接在配置文件中这么写还是不够优雅。于是我动态加载了spring.messages.basename这一配置。只要在启动之初利用System.setProperty注入即可。我的思路是,启动时读取resources/i18n下的所有文件,然后将国际化配置文件拼接最后用System.setProperty注入即可。于是自定义启动插件类Launch,编写静态方法launcher()负责读取文件写入spring.messages.basename,在SpringBoot启动类的main方法开头添加 Launch.launcher();即可。代码如下:

Launch类:

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
41
/**
* ClassName: launch <br/>
* Description: <br/>
* date: 2022/3/4 15:06 <br/>
* author TabTan <br/>
*/
public class Launch {

private static final Log logger = LogFactory.getLog(Launch.class);
static {
logger.info("加载自定义启动组件");
}
public static void launcher() {
// 加载国际化配置文件
setProps();
}

// 读取所有配置文件 添加国际化配置文件
@SneakyThrows
private static void setProps() {
StringBuffer i18nValue = new StringBuffer();
File file = ResourceUtils.getFile("classpath:i18n");
File[] properties = file.listFiles(f -> f.getName().endsWith("properties"));
assert properties != null;
Arrays.stream(properties)
.parallel()
.map(File::getName)
.forEach(e -> {
if (!e.contains("_")){
e = e.substring(0,e.indexOf("."));
i18nValue.append("i18n/");
i18nValue.append(e);
i18nValue.append(",");
}
});
// 删除多余的逗号
i18nValue.deleteCharAt(i18nValue.length()-1);
System.setProperty("spring.messages.basename", String.valueOf(i18nValue));
logger.info("spring.messages.basename:" + i18nValue);
}
}

启动类:

1
2
3
4
5
6
7
8
@SpringBootApplication
public class FileDemoApplication {
public static void main(String[] args) {
//启动自定义组件
Launch.launcher();
SpringApplication.run(FileDemoApplication.class,args);
}
}