springboot自定义validation注解:多字段属性关联校验

背景 

validation中提供的注解都是针对单个参数的,如果两个参数之间有关联关系就只能在代码里判断了,比如:

@Data
public class TestPo {

    
    private String type;

    //当type为定时发送时,必须填写发送时间,当type为立即发送时,可以不填发送时间
    private Date sendTime;

    //当type为草稿时,sendContent可以为空,否则必须有值
    private SendContent sendContent;
}

这种就只能在代码中判断type的值然后决定另外两个参数的校验。

方法1 使用@ScriptAssert注解

@Data
@ScriptAssert.List(value = {
        @ScriptAssert(script = "_this.type == '定时发送' && _this.sendTime != null",
                lang = "javascript",
                message = "当type为定时发送时,必须填写发送时间"),
        @ScriptAssert(script = "_this.type != '草稿' && _this.sendContent != null",
                lang = "javascript",
                message = "当type不是草稿时,sendContent必填")
})
public class TestPo {

    @NotBlank(message = "type不能为空")
    private String type;

    //当type为定时发送时,必须填写发送时间,当type为立即发送时,可以不填发送时间
    private Date sendTime;

    @Valid
    //当type为草稿时,sendContent可以为空,否则必须有值
    private SendContent sendContent;
}

这种方法需要使用javascript,对于部分人来说可能不够直观也很难调试。

方法2 自定义注解+spring表达式

针对这种情况我利用spring表达式写了一个自定义注解来解决这个问题。

第一步:自定义注解

/**
 * 多属性关联校验注解
 * 用于校验多个属性之间的关联关系
 * 当when条件满足时,必须满足must条件否则校验不通过
 * 注意:如果解析spel表达式错误将抛出异常
 * @author wangzhen
 */
@Documented
@Constraint(validatedBy = {MultiFieldAssociationCheckValidator.class })
@Target({TYPE_USE })
@Retention(RUNTIME)
@Repeatable(MultiFieldAssociationCheck.List.class)
public @interface MultiFieldAssociationCheck {

    /**
     * 错误信息描述,必填
     */
    String message();

    /**
     * 分组校验
     */
    Class<?>[] groups() default { };

    /**
     * 负载
     */
    Class<? extends Payload>[] payload() default { };

    /**
     * 当什么条件下校验,必须是一个spel表达式
     */
    String when();

    /**
     * 必须满足什么条件,必须是一个spel表达式
     */
    String must();
    @Target({TYPE_USE})
    @Retention(RUNTIME)
    @Documented
    public @interface List {
        MultiFieldAssociationCheck[] value();
    }
}

第二步:编写注解校验类

/**
 * 多属性关联校验注解的实现类
 */
public class MultiFieldAssociationCheckValidator implements ConstraintValidator<MultiFieldAssociationCheck, Object> {

    private static final String SPEL_TEMPLATE = "%s%s%s";
    private static final String SPEL_PREFIX = "#{";
    private static final String SPEL_SUFFIX = "}";
    private String when;

    private String must;
    @Override
    public void initialize(MultiFieldAssociationCheck constraintAnnotation) {
        this.when = constraintAnnotation.when();
        this.must = constraintAnnotation.must();
    }

    @Override
    public boolean isValid(Object value, ConstraintValidatorContext context) {

        if (StringUtils.isBlank(when) || StringUtils.isBlank(must)) {
            return true;
        }
        Map<String, Object> spelMap = getSpelMap(value);
        //when属性是一个spel表达式,执行这个表达式可以得到一个boolean值
        boolean whenCheck = Boolean.parseBoolean(SpelUtils.parseSpel(String.format(SPEL_TEMPLATE, SPEL_PREFIX, when, SPEL_SUFFIX), spelMap));
        if (whenCheck) {
            //判断must是否满足条件
            boolean mustCheck = Boolean.parseBoolean(SpelUtils.parseSpel(String.format(SPEL_TEMPLATE, SPEL_PREFIX, must, SPEL_SUFFIX), spelMap));
            if (!mustCheck) {
                //获取注解中的message属性值
                String message = context.getDefaultConstraintMessageTemplate();
                context.disableDefaultConstraintViolation();
                context.buildConstraintViolationWithTemplate(message).addConstraintViolation();
                return false;
            }
        }
        return true;
    }



    @SneakyThrows
    private Map<String,Object> getSpelMap(Object value){
        Field[] declaredFields = value.getClass().getDeclaredFields();
        Map<String,Object> spelMap = new HashMap<>();
        for (Field declaredField : declaredFields) {
            declaredField.setAccessible(true);
            //将对象中的属性名和属性值放入map中
            spelMap.put(declaredField.getName(),declaredField.get(value));
        }
        return spelMap;
    }

类中依赖的SpelUtils.parseSpel方法

    public static String parseSpel( String spel, Map<String, Object> map) {
        if (StringUtils.isBlank(spel)) {
            return "";
        } else {
            ExpressionParser parser = new SpelExpressionParser();
            StandardEvaluationContext context = new StandardEvaluationContext();
            context.setVariables(map);
            context.addPropertyAccessor(new MapAccessor());
            context.addPropertyAccessor(new BeanFactoryAccessor());
            return (String)parser.parseExpression(spel, new TemplateParserContext()).getValue(context, String.class);
        }
    }

第三步:使用注解,这个注解使用在类上

@Data
@MultiFieldAssociationCheck.List(
       value = {
               @MultiFieldAssociationCheck(when = "#type.equals('定时发送')", must = "#sendTime != null",message = "当type为定时发送时,必须填写发送时间"),
               @MultiFieldAssociationCheck(when = "!#type.equals('草稿')", must = "#sendContent != null",message = "当type为不是草稿时,sendContent必须有值"),
               @MultiFieldAssociationCheck(when = "#type.equals('立即发送')", must = "'123'.equals(#sendContent.content)",message = "当type为立即发送时,sendContent的content属性必须为123")
       }
)
public class TestPo {

    private String type;

    //当type为定时发送时,必须填写发送时间,当type为立即发送时,可以不填发送时间
    private Date sendTime;

    @Valid
    //当type为草稿时,sendContent可以为空,否则必须有值
    private SendContent sendContent;
}

注意:spring表达式平时不太常用,一定要好好检查避免出现异常。

  • 6
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
山东省基本农田shp文件是指将山东省内的基本农田信息以shp文件的形式进行存储和传输的文件。基本农田是指国家规划确定的农业用地保护的核心区域,具有重要的农业生产和生态功能。 这个shp文件包含了关于山东省基本农田的地理空间数据,如基本农田的边界、面积、位置等信息。它采用矢量数据格式,可以使用GIS软件进行打开和处理。 有了这个shp文件,可以进行一系列的农田规划和管理工作。首先,可以利用shp文件的空间属性,对基本农田进行空间分析和统计,统计山东省基本农田的总面积、分布情况等。根据这些数据,可以制定农田保护政策,合理规划农业用地,保护耕地资源。 其次,基于该shp文件,可以进行土地评价和农田质量分析,了解基本农田的土壤质量、水资源状况、适宜农作物类型等信息,以指导农业生产和土地管理。还可以结合其他数据,如气候数据和经济数据等,进行农田利用和农产品供给的研究与决策。 此外,该shp文件还可以与其它地理信息数据进行叠加分析,如交通网络数据、地形数据等,从而评估基本农田的承载能力、利用潜力及其与其他领域的关联性。 总之,山东省基本农田shp文件是一份具有重要参考价值的数据文件,对于山东省农田资源的保护、合理利用以及农业生产的规划与管理都起到了重要作用。
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值