前言:当我们用@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.validation
和org.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) { 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
| <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> <version>${hibernate.version}</version> </dependency> <dependency> <groupId>javax.validation</groupId> <artifactId>validation-api</artifactId> </dependency> <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> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-core</artifactId> </dependency> <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;
@Bean public Validator getValidator() { LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); validator.setValidationMessageSource(this.messageSource); return validator; }
@Bean public LocaleResolver localeResolver() { AcceptHeaderLocaleResolver acceptHeaderLocaleResolver = new AcceptHeaderLocaleResolver(); acceptHeaderLocaleResolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE); return acceptHeaderLocaleResolver; } }
|
5 国际化文件的内容:
test.properties
test_en_US.properties
test_zh_CN.properties
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
|
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); } }
|