Springboot中接口参数校验N种方法你会几个?
环境: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进行全局拦截处理,统一下输出。