通过SpEL进行参数动态校验

young 175 2024-11-23

背景

实际项目开发中,我们总能遇到一下的参数校验场景,当某一个或多个请求参数不为空或者为某个值时,另一个参数或者另几个参数应该不为空或者应该满足某些条件。一般情况下,我们可能会在代码中通过硬编码的方式进行校验,最近重温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语法进行处理。

经测试,此语法条件比较苛刻:

  1. 必须和其他AOP语法一起使用缩小拦截范围,这个可以理解,否则代理范围可能会很大
  2. 方法必须只有标记了注解的类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 的校验方式就彻底完成了。