环境:springboot2.2.10.RELEASE
Spring Validation验证框架对参数的验证机制提供了@Validated(Spring's JSR-303规范,是标准JSR-303的一个变种),javax提供了@Valid(标准JSR-303规范),结合BindingResult对象可以直接获取错误信息。
JSR是什么?
JSR是Java Specification Requests的缩写,意思是Java 规范提案。是指向JCP(Java Community Process)提出新增一个标准化技术规范的正式请求。任何人都可以提交JSR,以向Java平台增添新的API和服务。JSR已成为Java界的一个重要标准。
JSR-303 是JAVA EE 6 中的一项子规范,叫做Bean Validation,Hibernate Validator 是 Bean Validation 的参考实现 . Hibernate Validator 提供了 JSR 303 规范中所有内置 constraint 的实现,除此之外还有一些附加的 constraint。
关于JSR303规范中定义的注解及Hibernate的一些扩展注解可参见如下文章:
在Spring环境中这两个注解都可以使用。
本篇文章主要介绍如下内容:
- 参数校验分组
- 单个参数校验
- 嵌套参数校验
- 自定义工具类参数校验
- 国际化支持
- AOP验证参数统一处理
- 校验分组
@Validated支持分组,@Valid不支持
Bean定义:
public class Users {
@NotEmpty(message = "姓名不能为空1", groups = G1.class)
private String name ;
@Min(value = 10, message = "年龄不能小于10", groups = G1.class)
@Min(value = 20, message = "年龄不能小于20", groups = G2.class)
private Integer age ;
@Length(min = 6, max = 18, message = "邮箱介于6到18之间", groups = {G1.class, G2.class})
private String email ;
public static interface G1 {}
public static interface G2 {}
}
这里定义了2个分组G1,G2这里的定义可以随意。
定义接口:
@CustomEndpoint
@ResponseBody
public class UsersController extends BaseController {
@PackMapping(value = "/valid/save1", method = RequestMethod.POST)
public Object save1(@RequestBody @Validated(Users.G1.class) Users user, BindingResult result) {
Optional<List<String>> op = valid(result) ;
if (op.isPresent()) {
return op.get() ;
}
return "success" ;
}
@PackMapping(value = "/valid/save2", method = RequestMethod.POST)
public Object save2(@RequestBody @Validated(Users.G2.class) Users user, BindingResult result) {
Optional<List<String>> op = valid(result) ;
if (op.isPresent()) {
return op.get() ;
}
return "success" ;
}
}
public class BaseController {
protected Optional<List<String>> valid(BindingResult result) {
if (result.hasErrors()) {
return Optional.of(result.getAllErrors().stream().map(err -> err.getDefaultMessage()).collect(Collectors.toList())) ;
}
return Optional.empty() ;
}
}
这里我自定义了HandlerMapping使用了自定义的注解@CustomEndpoint和@PackMapping 具体怎么使用自定义的参见如下文章:
这里定义了两个接口在参数校验时指明了使用哪个分组,测试:
在这里我们的分组校验已经生效了。
注意:这里在做参数校验时分别指明了具体的分组,如果在Bean中没有指明具体的分组,那么这个校验将不会生效,比如这里:
@Length(min = 6, max = 18, message = "邮箱介于6到18之间")
private String email ;
如果这里没有设定groups属性,那么这里的校验将不会生效,因为我们的Controller都指明了具体使用的分组。
- 单个参数校验
JSR303的Hibernate Validator实现只能对对象进行参数校验不能对当个方法参数进行校验,Spring对此进行了扩展
@CustomEndpoint
@ResponseBody
@Validated
public class UsersController extends BaseController {
@PackMapping("/valid/find")
public Object find(@NotEmpty(message = "参数Id不能为空") String id) {
return "查询到参数【" + id + "】" ;
}
}
注意:这里在类上需要加入@Validated注解。参数的校验直接在相应的参数前写上对应的注解即可。
同时后台抛出了异常信息:
- 嵌套参数校验
嵌套的对象需要使用@Valid 注解
情况1:
public class Users {
@NotEmpty(message = "电话必需填写")
private String phone ;
@Valid
private Address address;
}
public class Address {
@NotEmpty(message = "地址信息必需填写")
private String addr ;
}
接口:
@PackMapping(value = "/valid/save3", method = RequestMethod.POST)
public Object save3(@RequestBody @Validated Users user, BindingResult result) {
Optional<List<String>> op = valid(result) ;
if (op.isPresent()) {
return op.get() ;
}
return "success" ;
}
测试:
发现地址根本就没有校验。
调整参数继续校验:
把Address对应的属性address加上后能够校验了。
情况2:
public class Users {
@NotEmpty(message = "电话必需填写")
private String phone ;
@Valid
private Address address = new Address();
}
这里直接把Address对象new好再测试:
这回没有添加address也可以校验了。
- 自定义工具类参数校验
参数校验不一定都Controller层进行校验,也可能在Service校验,比如你一个通用的save方法,不仅你会调用,别人也会调用,那这时候就有必要在Service中进行参数的校验。
public class ValidatorUtil {
private static Validator validator;
static {
validator = Validation.buildDefaultValidatorFactory().getValidator();
}
/**
* <p>
* 抛出异常方式
* </p>
* <p>时间:2021年2月7日-下午5:49:29</p>
* @author xg
* @param object 校验的对象参数
* @param groups 分组
* @return void
*/
public static <T> void validateParams(T object, Class<?>... groups) {
Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups);
if (!constraintViolations.isEmpty()) {
String messages = constraintViolations.stream().map(cv -> cv.getMessage()).collect(Collectors.joining("\n")) ;
throw new ParamsException(messages) ;
}
}
/**
* <p>
* 收集错误提示信息
* </p>
* <p>时间:2021年2月7日-下午5:51:27</p>
* @author xg
* @param object 校验的对象参数
* @param groups 分组
* @return
* @return Optional<List<String>>
*/
public static <T> Optional<List<String>> validateParamsRet(T object, Class<?>... groups) {
Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups);
if (!constraintViolations.isEmpty()) {
return Optional.of(constraintViolations.stream().map(cv -> cv.getMessage()).collect(Collectors.toList())) ;
}
return Optional.empty() ;
}
}
先来验证下:
public static void main(String[] args) {
Users user = new Users() ;
System.out.println(validateParamsRet(user).get()) ;
validateParams(user) ;
}
控制台输出了错误信息。
工具类说明:
static {
validator = Validation.buildDefaultValidatorFactory().getValidator();
}
这里底层是通过SPI技术来获取所有的Validator验证器:
Validation.GetValidationProviderListAction类中有如下方法:
private List<ValidationProvider<?>> loadProviders(ClassLoader classloader) {
ServiceLoader<ValidationProvider> loader = ServiceLoader.load( ValidationProvider.class, classloader );
Iterator<ValidationProvider> providerIterator = loader.iterator();
List<ValidationProvider<?>> validationProviderList = new ArrayList<>();
while ( providerIterator.hasNext() ) {
try {
validationProviderList.add( providerIterator.next() );
}
catch ( ServiceConfigurationError e ) {
}
}
return validationProviderList;
}
ServiceLoader<ValidationProvider> loader = ServiceLoader.load( ValidationProvider.class, classloader );
在我的环境中有引入Hibernate验证器。
关于验证工厂类的获取,可以通过如下3种方式:
- 国际化支持
在resources下建立,ValidationMessages.properties文件这是默认的,中文:ValidationMessages_zh_CN.properties,英文:ValidationMessages_en_US.properties
内容如下:
name.notempty=姓名必需填写
英文:
name.notempty=name is require
Bean配置:
public class Users {
@NotEmpty(message = "{name.notempty}", groups = G1.class)
private String name ;
}
这里message的写法:{properties中定义的key}
模拟英文环境:
请求中需要添加:Accept-Language头信息指明语言。
- AOP验证参数统一处理
以上都接口测试都是在每个方法中自己调用验证逻辑进行处理,接下来通过AOP类对这里有参数校验的进行统一的处理。
引入依赖:
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<scope>runtime</scope>
</dependency>
自定义注解:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface EnableValidate {
}
定义AOP:
@Component
@Aspect
public class ValidateAspect {
@Pointcut("@annotation(com.pack.params.valid.EnableValidate)")
public void valid() {}
@Before("valid()")
public void validateBefore(JoinPoint jp) {
Object[] args = jp.getArgs() ;
for (Object arg : args) {
if (arg instanceof BindingResult) {
BindingResult result = (BindingResult) arg ;
if (result.hasErrors()) {
String messages = result.getAllErrors().stream().map(err -> err.getDefaultMessage()).collect(Collectors.joining(",")) ;
throw new ParamsException(messages) ;
}
}
}
}
}
接口:
@PackMapping(value = "/valid/save1", method = RequestMethod.POST)
@EnableValidate
public Object save1(@RequestBody @Validated(Users.G1.class) Users user, BindingResult result) {
/*Optional<List<String>> op = valid(result) ;
if (op.isPresent()) {
return op.get() ;
}*/
return "success" ;
}
测试:
其实在这里对于这样的异常可以用@ControllerAdvice进行全局拦截处理,统一下输出。
完毕!!!
创作不易给个关注+转发呗谢谢