多字段关联校验

背景

  • 我们用了Spring框架后,在校验前端参数的时候,一般会使用@NotNull@NotBlank等注解,这样就不用写业务代码,判断这个字段了,省力,又简洁优雅,对真实业务处理的代码无侵入。
  • 但是多个字段关联的业务就比较麻烦,比如下列场景。
public class TestValidate {
    /**
     * 姓名
     **/
    @NotBlank(message = "姓名必填")
    private String name;

    /*** 是否有房 0 - 没房子 1 - 有房子 */
    @NotNull(message = "是否有房必填")
    private Integer isHaveHours;
    /***
     * 房子面积
     * */
    private Integer hoursAreas;
    
......get/set方法  省略.....

  • 如上代码所示,当选择没有房子的时候,hoursAreas房子面积有可能是不必校验的,isHaveHours为1就需要校验不为空。每次都在业务代码上加if或者断言我觉得不舒服,所以想办法抽出来。
  • 我在网上查到使用DefaultGroupSequenceProvider的办法,但好像又太依赖分组了,个人感觉不太合适。
  • 我最终选择的是AOP的方式。

代码实现

  • 我们可以提取共性,做成一个注解。
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ConditionalValidateField {

    /***
     * 关联字段
     * */
    String relationField();

    /***
     * 要执行的的校验动作
     * */
    int action();

    /** 该字段的值为**/
    String value();

    /***
     * 异常信息
     * */
    String message() default "";
}
  • 再写一个需要拦截的标记注解(感觉这个可以扩展分组功能)
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ConditionalValidate {
}
  • 然后AOP拦截这个注解,处理参数,校验参数。
  • 解析参数使用的是SPEL

@Aspect
@Component
public class ConditionalValidateAspect {

    //将方法参数纳入Spring管理
    private final LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();

    //解析spel表达式
    private final ExpressionParser parser = new SpelExpressionParser();


    @Before("@annotation(conditionalValidate)")
    public void doBefore(JoinPoint joinPoint, ConditionalValidate conditionalValidate) throws Throwable {
        //获取参数对象数组
        Object[] args = joinPoint.getArgs();
        Assert.notEmpty(args, "没有参数");
        Assert.isTrue(args.length <= 1, "只能有一个参数");

        //获取方法
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();


        //获取方法参数名
        String[] params = discoverer.getParameterNames(method);
        //将参数纳入Spring管理
        EvaluationContext context = new StandardEvaluationContext();
        for (int len = 0; len < params.length; len++) {
            context.setVariable(params[len], args[len]);
        }

        Object firstParams = args[0];
        if (!StringUtils.isEmpty(firstParams)) {
            List<Field> allFields = getAllFields(firstParams);

            // 把要校验的找到
            List<ConditionalValidateFieldInfo> validateFieldList = new ArrayList<>();
            // 字段类型
            Map<String, Class> fieldClzMap = new HashMap<>();
            allFields.forEach(field -> {
                ConditionalValidateField conditionalValidateField = AnnotationUtils.findAnnotation(field, ConditionalValidateField.class);
                String fieldName = field.getName();
                if (!StringUtils.isEmpty(conditionalValidateField)) {
                    validateFieldList.add(new ConditionalValidateFieldInfo(fieldName, conditionalValidateField));
                }
                fieldClzMap.put(fieldName, field.getType());
            });


            // 执行校验动作,这块要分很多种情况处理
            validateFieldList.forEach(conditionalValidateFieldInfo -> {
                if (!StringUtils.isEmpty(conditionalValidateFieldInfo)) {
                    ConditionalValidateField conditionalValidateField = conditionalValidateFieldInfo.getConditionalValidateField();
                    //TODO 这个地方可以使用策略模式优化下,共性的地方用模板方法
                    // 如果是相等 执行校验
                    if (ValidateFieldAction.IF_EQ_NOT_NULL == conditionalValidateField.action()) {
                        // 判断该字段类型
                        Class originalClz = fieldClzMap.get(conditionalValidateFieldInfo.getFieldName());
                        //TODO 只写了Integer类型的
                        if (Integer.class.getSimpleName().equals(originalClz.getSimpleName())) {
                            Expression expression = parser.parseExpression("#" + params[0] + "." + conditionalValidateFieldInfo.getFieldName());
                            Integer originalValue = expression.getValue(context, Integer.class);
                            if (!StringUtils.isEmpty(conditionalValidateField.value())) {
                                // 如果是相等的
                                if (Integer.valueOf(conditionalValidateField.value()).equals(originalValue)) {
                                    Expression relationExpression = parser.parseExpression("#" + params[0] + "." + conditionalValidateField.relationField());
                                    String relationField = conditionalValidateField.relationField();
                                    Object value = relationExpression.getValue(context, fieldClzMap.get(relationField));
                                    Assert.isTrue(!StringUtils.isEmpty(value), conditionalValidateField.message());
                                }
                            } else {
                                // 为空的情况,有可能要求原字段为空,关联字段不能为空的情况;判断都是空就校验
                                if (StringUtils.isEmpty(conditionalValidateField.value()) && StringUtils.isEmpty(originalValue)) {
                                    Expression relationExpression = parser.parseExpression("#" + params[0] + "." + conditionalValidateField.relationField());
                                    String relationField = conditionalValidateField.relationField();
                                    Object value = relationExpression.getValue(context, fieldClzMap.get(relationField));
                                    Assert.isTrue(!StringUtils.isEmpty(value), conditionalValidateField.message());
                                }
                            }
                        }
                    }
                }
            });

        }


    }

    public static List<Field> getAllFields(Object object) {
        Class clazz = object.getClass();
        List<Field> fieldList = new ArrayList<>();
        while (clazz != null) {
            fieldList.addAll(new ArrayList<>(Arrays.asList(clazz.getDeclaredFields())));
            clazz = clazz.getSuperclass();
        }
        return fieldList;
    }

    /***
     * 封装字段信息
     * */
    public class ConditionalValidateFieldInfo {
        private String fieldName;
        private ConditionalValidateField conditionalValidateField;

        public ConditionalValidateFieldInfo(String fieldName, ConditionalValidateField conditionalValidateField) {
            this.fieldName = fieldName;
            this.conditionalValidateField = conditionalValidateField;
        }

        public String getFieldName() {
            return fieldName;
        }

        public void setFieldName(String fieldName) {
            this.fieldName = fieldName;
        }

        public ConditionalValidateField getConditionalValidateField() {
            return conditionalValidateField;
        }

        public void setConditionalValidateField(ConditionalValidateField conditionalValidateField) {
            this.conditionalValidateField = conditionalValidateField;
        }
    }

}

  • 参数和Controller调用层。

public class TestValidate {


    /**
     * 姓名
     **/
    @NotBlank(message = "姓名必填")
    private String name;

    /*** 是否有房 0 - 没房子 1 - 有房子 */
    @NotNull(message = "是否有房必填")
    @ConditionalValidateField(relationField = "hoursAreas", value = "1",
            action = ValidateFieldAction.IF_EQ_NOT_NULL,
            message = "有房子,房子面积必填")
    private Integer isHaveHours;

    /***
     * 房子面积
     * */
    private Integer hoursAreas;


    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getIsHaveHours() {
        return isHaveHours;
    }

    public void setIsHaveHours(Integer isHaveHours) {
        this.isHaveHours = isHaveHours;
    }

    public Integer getHoursAreas() {
        return hoursAreas;
    }

    public void setHoursAreas(Integer hoursAreas) {
        this.hoursAreas = hoursAreas;
    }
}

//ValidateController
  @RequestMapping("/test")
   @ConditionalValidate
    public String test(@Validated TestValidate testValidate) {
        return "success";
    }

最终效果

  • isHaveHours为1时才校验。如下图所示。

在这里插入图片描述

结语和代码地址

更新

2022年3月16日21:34:10 一个字段支持重复注解(@Repeatable)

posted on 2022-03-24 18:01  愤怒的苹果ext  阅读(325)  评论(0编辑  收藏  举报

导航