validation中提供的注解都是针对单个参数的,如果两个参数之间有关联关系就只能在代码里判断了,比如:
@Data public class TestPo { private String type; //当type为定时发送时,必须填写发送时间,当type为立即发送时,可以不填发送时间 private Date sendTime; //当type为草稿时,sendContent可以为空,否则必须有值 private SendContent sendContent; }
这种就只能在代码中判断type的值然后决定另外两个参数的校验。
@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,对于部分人来说可能不够直观也很难调试。
针对这种情况我利用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{ 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 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 getSpelMap(Object value){ Field[] declaredFields = value.getClass().getDeclaredFields(); Map 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, Mapmap) { 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; }