背景
实际项目开发中,我们总能遇到一下的参数校验场景,当某一个或多个请求参数不为空或者为某个值时,另一个参数或者另几个参数应该不为空或者应该满足某些条件。一般情况下,我们可能会在代码中通过硬编码的方式进行校验,最近重温SpEL和AOP,想到可以采用自定义注解加这两者结合的方式,进行校验。
AOP的使用可参考https://yhsblog.cn/archives/springaop
SpEL的使用可参考https://yhsblog.cn/archives/spel-biao-da-shi-shi-yong
代码实现
AOP 实现
考虑到参数可能会有多个对象,于是定义了一个@DynamicValidParam的注解,用于标记需要进行校验的参数对象。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface DynamicValidParam {
}
AOP中有一个@args语法,用于将参数的类上标记了指定注解的方法进行拦截,于是考虑使用此AOP语法进行处理。
经测试,此语法条件比较苛刻:
- 必须和其他AOP语法一起使用缩小拦截范围,这个可以理解,否则代理范围可能会很大
- 方法必须只有标记了注解的类1个参数,这样就比较麻烦(也许是我使用的不对,欢迎大佬指正)
因此考虑采用笨办法来解决:使用自定义注解@DynamicValidMethod标记需要处理的方法,再在AOP中查找标记了@DynamicValidParam的类
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DynamicValidMethod {
}
再定义一个用于数据校验的注解@DynamicValidField标记需要校验属性
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface DynamicValidField {
/**
* SpringEL表达式,用于标记前置校验条件,当满足条件时,执行validExpressions,执行结果需为boolean类型
*/
String[] expressions();
/**
* 当validExpressions返回false时,抛出异常,提示message
*/
String message();
/**
* 校验表达式,当expressions返回均为true时执行,执行结果需为boolean类型,可以调用方法,若在调用的方法中抛出异常,则不会使用message的提示
*/
String validExpressions();
}
考虑到某些场景下,可能会有这样的校验规则:当A=1时B=“a”,当A=2时B=“b”,于是考虑采用Java8新增的@Repeatable,让这个注解可以重复标记于是修改如下
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface DynamicValidFields {
DynamicValidField[] value();
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Repeatable(DynamicValidFields.class)
public @interface DynamicValidField {
/**
* SpringEL表达式,用于标记前置校验条件,当满足条件时,执行validExpressions,执行结果需为boolean类型
*/
String[] expressions();
/**
* 当validExpressions返回false时,抛出异常,提示message
*/
String message();
/**
* 校验表达式,当expressions返回均为true时执行,执行结果需为boolean类型,可以调用方法,若在调用的方法中抛出异常,则不会使用message的提示
*/
String validExpressions();
}
此时可以编写相关AOP实现,用于参数校验,代码如下:
@Aspect
@Configuration
public class DynamicValidAspectJ {
private ExpressionParser expressionParser = new SpelExpressionParser();
@Pointcut("@annotation(young.test.dynamic.validator.core.anno.DynamicValidMethod)")
public void pointCut() {
}
@Before("pointCut()")
public void valid(JoinPoint joinPoint) {
Object[] args = joinPoint.getArgs();
for (Object arg : args) {
// 查看参数的类型上是否标记了DynamicValidParam注解
if (!arg.getClass().isAnnotationPresent(DynamicValidParam.class)) {
continue;
}
// 获取全部属性
List<Field> declaredFields = FieldUtils.getAllFieldsList(arg.getClass());
for (Field declaredField : declaredFields) {
// 静态属性不处理
if (Modifier.isStatic(declaredField.getModifiers())){
continue;
}
// 获取属性的注解
DynamicValidField[] annotations = getDynamicValidFields(declaredField);
if (annotations == null) {
continue;
}
for (DynamicValidField DynamicValidField : annotations) {
String[] expressions = DynamicValidField.expressions();
boolean validResult = true;
for (String expression : expressions) {
validResult = validResult && (boolean) expressionParser.parseExpression(expression).getValue(arg);
}
if (!validResult) {
continue;
}
declaredField.setAccessible(true);
boolean validSuccess = (boolean) expressionParser.parseExpression(DynamicValidField.validExpressions()).getValue(arg);
if (!validSuccess) {
throw new RuntimeException(DynamicValidField.message());
}
}
}
}
}
private static DynamicValidField[] getDynamicValidFields(Field declaredField) {
DynamicValidField[] annotations;
DynamicValidField annotation = declaredField.getAnnotation(DynamicValidField.class);
if (annotation == null) {
DynamicValidFields DynamicValidFields = declaredField.getAnnotation(DynamicValidFields.class);
if (DynamicValidFields == null) {
return null;
} else {
annotations = DynamicValidFields.value();
}
} else {
annotations = new DynamicValidField[]{annotation};
}
return annotations;
}
}
考虑到某些场景下,可能需要通过一些复杂的逻辑来进行校验,或者需要通过现有的一些方法进行校验,这些方法可能是Spring容器中的bean,因此给表达式解析器增加Spring容器的扩展,增加上下文StandardEvaluationContext,并给它设置BeanResolver,代码改动后如下:
@Aspect
@Configuration
public class DynamicValidAspectJ implements BeanFactoryAware {
private ExpressionParser expressionParser = new SpelExpressionParser();
private StandardEvaluationContext context = new StandardEvaluationContext();
@Pointcut("@annotation(young.test.dynamic.validator.core.anno.DynamicValidMethod)")
public void pointCut() {
}
@Before("pointCut()")
public void check(JoinPoint joinPoint) {
Object[] args = joinPoint.getArgs();
for (Object arg : args) {
// 查看参数的类型上是否标记了DynamicValidParam注解
if (!arg.getClass().isAnnotationPresent(DynamicValidParam.class)) {
continue;
}
// 获取全部属性
List<Field> declaredFields = FieldUtils.getAllFieldsList(arg.getClass());
for (Field declaredField : declaredFields) {
// 静态属性不处理
if (Modifier.isStatic(declaredField.getModifiers())) {
continue;
}
// 获取属性的注解
DynamicValidField[] annotations = getDynamicValidFields(declaredField);
if (annotations == null) {
continue;
}
for (DynamicValidField DynamicValidField : annotations) {
String[] expressions = DynamicValidField.expressions();
boolean checkResult = true;
for (String expression : expressions) {
checkResult = checkResult && (boolean) expressionParser.parseExpression(expression).getValue(context, arg);
}
if (!checkResult) {
continue;
}
declaredField.setAccessible(true);
boolean checkSuccess = (boolean) expressionParser.parseExpression(DynamicValidField.validExpressions()).getValue(context, arg);
if (!checkSuccess) {
throw new RuntimeException(DynamicValidField.message());
}
}
}
}
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
context.setBeanResolver(new BeanFactoryResolver(beanFactory));
}
private static DynamicValidField[] getDynamicValidFields(Field declaredField) {
DynamicValidField[] annotations;
DynamicValidField annotation = declaredField.getAnnotation(DynamicValidField.class);
if (annotation == null) {
DynamicValidFields DynamicValidFields = declaredField.getAnnotation(DynamicValidFields.class);
if (DynamicValidFields == null) {
return null;
} else {
annotations = DynamicValidFields.value();
}
} else {
annotations = new DynamicValidField[]{annotation};
}
return annotations;
}
}
编写简单的测试方法进行测试
@DynamicValidParam
public class TestVo {
@DynamicValidField(expressions = "name!=null", message = "name不能为空", validExpressions = "@testService.check(name)")
private String name;
@DynamicValidField(expressions = "age!=null", message = "age不能为空", validExpressions = "@testService.check(age)")
private Integer age;
// @DynamicValidField(expressions = {"name!=null", "age!=null"}, message = "birth不能为空", validExpressions = "birth!=null")
// private LocalDate birth;
@DynamicValidField(expressions = "\"b\".equals(name)", message = "xxx要等于xx", validExpressions = "xxx!=null && xxx.equals(\"xx\")")
@DynamicValidField(expressions = "\"a\".equals(name)", message = "xxx不能为空", validExpressions = "xxx!=null")
private String xxx;
}
@Autowired
private TestService service;
@GetMapping("/test")
@DynamicValidMethod
public void test(TestVo vo) {
service.test(vo);
}
@Service
public class TestService {
public void test(TestVo vo) {
System.out.println("test");
}
public boolean check(String name) {
if ("abc".equals(name)) {
throw new RuntimeException("不行啊");
}
return true;
}
public boolean check(Integer age) {
if (age < 18) {
throw new RuntimeException("太小了");
}
return true;
}
}
JSR-303 实现
基于 AOP 实现之后,突然想到之前项目上有做过继续两个字段的同时校验操作,类似于在修改密码时,校验新密码与确认密码是否相同,通过扩展 hibernate-validator(https://hibernate.org/validator/) 来实现,当时写的时候和 Class 是绑定死的,现在采用 SpEL的话,灵活性更高,于是考虑采用此种方式进行重写。
首先要创建一个校验类,实现ConstraintValidator 接口
public interface ConstraintValidator<A extends Annotation, T> {
default void initialize(A constraintAnnotation) {
}
boolean isValid(T var1, ConstraintValidatorContext var2);
}
initialize方法只在初始化的时候执行一次,isValid 方法在校验的时候进行调用,Spring 在调用时会将其注册到容器中,因此BeanFactoryAware接口也可以继续使用。
当时想让其实用性更强,于是考虑将校验进行封装,在这里只是调用封装好的方法即可,校验之后,需要填写对应的提示消息,于是需要对校验结果和消息进行封装代码如下:
@AllArgsConstructor
@NoArgsConstructor
public class DynamicValidResult {
private boolean valid;
private String message;
}
@Configuration
public class DynamicValidProcess implements BeanFactoryAware {
private static ExpressionParser expressionParser = new SpelExpressionParser();
private static StandardEvaluationContext context = new StandardEvaluationContext();
public static DynamicValidResult valid(Object param) {
if (param == null) {
return new DynamicValidResult(true, "");
}
if (!param.getClass().isAnnotationPresent(DynamicValidParam.class)) {
return new DynamicValidResult(true, "");
}
List<Field> declaredFields = FieldUtils.getAllFieldsList(param.getClass());
for (Field declaredField : declaredFields) {
// 静态属性不处理
if (Modifier.isStatic(declaredField.getModifiers())) {
continue;
}
// 获取属性的注解
DynamicValidField[] annotations = getDynamicValidFields(declaredField);
if (annotations == null) {
continue;
}
for (DynamicValidField DynamicValidField : annotations) {
String[] expressions = DynamicValidField.expressions();
boolean validResult = true;
for (String expression : expressions) {
validResult = validResult && (boolean) expressionParser.parseExpression(expression).getValue(context, param);
}
if (!validResult) {
continue;
}
boolean validSuccess = (boolean) expressionParser.parseExpression(DynamicValidField.validExpressions()).getValue(context, param);
if (!validSuccess) {
return new DynamicValidResult(false, DynamicValidField.message());
}
}
}
return new DynamicValidResult(true, "");
}
private static DynamicValidField[] getDynamicValidFields(Field declaredField) {
DynamicValidField[] annotations;
DynamicValidField annotation = declaredField.getAnnotation(DynamicValidField.class);
if (annotation == null) {
DynamicValidFields DynamicValidFields = declaredField.getAnnotation(DynamicValidFields.class);
if (DynamicValidFields == null) {
return null;
} else {
annotations = DynamicValidFields.value();
}che
} else {
annotations = new DynamicValidField[]{annotation};
}
return annotations;
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
context.setBeanResolver(new BeanFactoryResolver(beanFactory));
}
}
public class DynamicValidParamValidator implements ConstraintValidator<DynamicValidParam, Object>{
@Override
public void initialize(DynamicValidParam constraintAnnotation) {
ConstraintValidator.super.initialize(constraintAnnotation);
}
@Override
public boolean isValid(Object o, ConstraintValidatorContext constraintValidatorContext) {
if (o == null) {
return true;
}
DynamicValidResult valid = DynamicValidProcess.valid(o);
if (!valid.isValid()) {
constraintValidatorContext.disableDefaultConstraintViolation();
constraintValidatorContext.buildConstraintViolationWithTemplate(valid.getMessage()).addConstraintViolation();
}
return valid.isValid();
}
}
之后还需根据相关规范,修改我们的注解
@Constraint(validatedBy = DynamicValidParamValidator.class)
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface DynamicValidParam {
String message() default "参数校验失败";
Class<?>[] groups() default {};
Class<?>[] payload() default {};
}
需要校验的地方,加上@Valid注解即可
@GetMapping("/test")
@DynamicValidMethod
public void test(@Valid TestVo vo) {
service.test(vo);
}
扩展
在上面的工作做完之后,突然想到Validator的工厂提供了快速失败与非快速失败两种校验方式
Validation.byProvider(HibernateValidator.class).configure().failFast(failFast).buildValidatorFactory().getValidator();
于是可以考虑也将我们的校验机制增加此类机制进行机制,于是对代码进行修改,首先需要获取到 BeanFactory
@Configuration
public class BeanFactorySupport implements BeanFactoryAware {
private static org.springframework.beans.factory.BeanFactory beanFactory;
public static BeanFactory getBeanFactory() {
return beanFactory;
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
BeanFactorySupport.beanFactory = beanFactory;
}
}
其次定义一个 DynamicValidtor的接口
public interface DynamicValidator {
Iterable<DynamicValidResult> valid(Object o);
}
定义一个自定义的校验异常
public class DynamicValidException extends RuntimeException{
public DynamicValidException(String message) {
super(message);
}
public DynamicValidException(Throwable cause) {
super(cause);
}
}
修改DynamicValidatorProcess,让它支持返回迭代器类型的校验结果,同时支持快速返回和非快速返回,同时支持校验的SpEL返回DynamicValidResult,这样一来,就不能让 Spring 去管理这个类了
public class DynamicValidatorProcess implements DynamicValidator {
private ExpressionParser expressionParser = new SpelExpressionParser();
private StandardEvaluationContext context = new StandardEvaluationContext();
private boolean failFast;
DynamicValidatorProcess(BeanFactory beanFactory, boolean failFast) {
context.setBeanResolver(new BeanFactoryResolver(beanFactory));
this.failFast = failFast;
}
public Iterable<DynamicValidResult> valid(Object param) {
if (param == null) {
return Collections.emptyList();
}
if (!param.getClass().isAnnotationPresent(DynamicValidParam.class)) {
return Collections.emptyList();
}
List<DynamicValidResult> result = new ArrayList<>();
List<Field> declaredFields = FieldUtils.getAllFieldsList(param.getClass());
for (Field declaredField : declaredFields) {
// 静态属性不处理
if (Modifier.isStatic(declaredField.getModifiers())) {
continue;
}
// 获取属性的注解
DynamicValidField[] annotations = getDynamicCheckFields(declaredField);
if (annotations == null) {
continue;
}
for (DynamicValidField dynamicValidField : annotations) {
String[] expressions = dynamicValidField.expressions();
boolean validResult = true;
for (String expression : expressions) {
validResult = validResult && (boolean) expressionParser.parseExpression(expression).getValue(context, param);
}
if (!validResult) {
continue;
}
try {
Object value = expressionParser.parseExpression(dynamicValidField.checkExpressions()).getValue(context, param);
if (value instanceof Boolean){
boolean validSuccess = (boolean) value;
if (!validSuccess) {
if (failFast){
return Arrays.asList(new DynamicValidResult(false, dynamicValidField.message()));
}else {
result.add(new DynamicValidResult(false, dynamicValidField.message()));
}
}
}else if (value instanceof DynamicValidResult){
DynamicValidResult fieldValidResult = (DynamicValidResult) value;
if (failFast){
return Arrays.asList(fieldValidResult);
}else {
result.add(fieldValidResult);
}
}
}catch (Exception e){
if (e instanceof DynamicValidException){
if (failFast){
return Arrays.asList(new DynamicValidResult(false, e.getMessage()));
}else{
result.add(new DynamicValidResult(false, e.getMessage()));
}
}
throw new DynamicValidException(e);
}
}
}
return result;
}
private static DynamicValidField[] getDynamicCheckFields(Field declaredField) {
DynamicValidField[] annotations;
DynamicValidField annotation = declaredField.getAnnotation(DynamicValidField.class);
if (annotation == null) {
DynamicValidFields dynamicValidFields = declaredField.getAnnotation(DynamicValidFields.class);
if (dynamicValidFields == null) {
return null;
} else {
annotations = dynamicValidFields.value();
}
} else {
annotations = new DynamicValidField[]{annotation};
}
return annotations;
}
}
最后写一个简单的工厂来获取这个动态校验器
public class DynamicValidatorFactory {
public static DynamicValidator getValidator(){
return new DynamicValidatorProcess(BeanFactorySupport.getBeanFactory(),true);
}
public static DynamicValidator getValidator(boolean failFast){
return new DynamicValidatorProcess(BeanFactorySupport.getBeanFactory(),failFast);
}
}
最后,修改我们的ConstraintValidator,让它通过工厂去获取校验器,采用快速失败的方式
public class DynamicValidParamValidator implements ConstraintValidator<DynamicValidParam, Object> {
private static DynamicValidator dynamicValidator;
@Override
public void initialize(DynamicValidParam constraintAnnotation) {
ConstraintValidator.super.initialize(constraintAnnotation);
dynamicValidator = DynamicValidatorFactory.getValidator();
}
@Override
public boolean isValid(Object o, ConstraintValidatorContext constraintValidatorContext) {
if (o == null) {
return true;
}
Iterable<DynamicValidResult> validResults = dynamicValidator.valid(o);
Iterator<DynamicValidResult> iterator = validResults.iterator();
if (iterator.hasNext()) {
DynamicValidResult valid = iterator.next();
if (!valid.isValid()) {
constraintValidatorContext.disableDefaultConstraintViolation();
constraintValidatorContext.buildConstraintViolationWithTemplate(valid.getMessage()).addConstraintViolation();
}
return valid.isValid();
}
return true;
}
}
考虑到某些场景下可能需要采用非快速失败的方式,将所有属性校验完之后统一进行返回,于是对@DynamicValidParam注解和DynamicValidParamValidator 进行修改
@Constraint(validatedBy = DynamicValidParamValidator.class)
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface DynamicValidParam {
String message() default "参数校验失败";
Class<?>[] groups() default {};
Class<?>[] payload() default {};
boolean failFast() default true;
}
public class DynamicValidParamValidator implements ConstraintValidator<DynamicValidParam, Object> {
private DynamicValidator dynamicValidator;
@Override
public void initialize(DynamicValidParam constraintAnnotation) {
ConstraintValidator.super.initialize(constraintAnnotation);
dynamicValidator = DynamicValidatorFactory.getValidator(constraintAnnotation.failFast());
}
@Override
public boolean isValid(Object o, ConstraintValidatorContext constraintValidatorContext) {
if (o == null) {
return true;
}
Iterable<DynamicValidResult> validResults = dynamicValidator.valid(o);
Iterator<DynamicValidResult> iterator = validResults.iterator();
StringJoiner messageJoiner = new StringJoiner(",");
boolean isValid = true;
while (iterator.hasNext()) {
DynamicValidResult valid = iterator.next();
isValid = isValid && valid.isValid();
if (!valid.isValid()) {
constraintValidatorContext.disableDefaultConstraintViolation();
messageJoiner.add(valid.getMessage());
}
}
constraintValidatorContext.buildConstraintViolationWithTemplate(messageJoiner.toString()).addConstraintViolation();
return isValid;
}
}
如此一来这种通过SpEL 的校验方式就彻底完成了。