背景
validation中提供的注解都是针对单个参数的,如果两个参数之间有关联关系就只能在代码里判断了,比如:
@Data
@ApiModel(value = "User用户登录", description = "用户登录")
public class User {
@ApiModelProperty(value = "手机号")
@NotBlank(message = "手机号不能为空")
private String mobile;
@ApiModelProperty(value = "身份证号")
private String idcardNo;
@ApiModelProperty(value = "密码")
private String password;
@ApiModelProperty(value = "登录短信验证")
private String messageCode;
@ApiModelProperty(value = "登录类型: 手机号密码登录:1, 手机号短信登录:2, 身份证密码登录:3", required = true)
@NotNull(message = "登录类型不能为空!")
@Min(value = 1, message = "登录类型值不能小于1")
@Max(value = 3, message = "登录类型值不能大于3")
private Long loginTypeCode;
}
根据loginTypeCode不同的值去判断mobile、idcardNo、password、messageCode关联是否为空。
一、使用@ScriptAssert注解
@Data
@ApiModel(value = "User用户登录", description = "用户登录")
@ScriptAssert.List(value = {
@ScriptAssert(script = "_this.loginTypeCode == 1 && _this.mobile != null && _this.password != null",
lang = "javascript",
message = "手机号密码登录,手机号和密码不能为空"),
@ScriptAssert(script = "_this.loginTypeCode == 2 && _this.mobile != null && _this.messageCode != null",
lang = "javascript",
message = "手机号验证码登录,手机号和验证码不能为空"),
@ScriptAssert(script = "_this.loginTypeCode == 3 && _this.idcardNo != null && _this.password != null",
lang = "javascript",
message = "身份证号密码登录,身份证号和密码不能为空")
})
public class User {
@ApiModelProperty(value = "手机号")
@NotBlank(message = "手机号不能为空")
private String mobile;
@ApiModelProperty(value = "身份证号")
private String idcardNo;
@ApiModelProperty(value = "密码")
private String password;
@ApiModelProperty(value = "登录短信验证")
private String messageCode;
@ApiModelProperty(value = "登录类型: 手机号密码登录:1, 手机号短信登录:2, 身份证密码登录:3", required = true)
@NotNull(message = "登录类型不能为空!")
@Min(value = 1, message = "登录类型值不能小于1")
@Max(value = 3, message = "登录类型值不能大于3")
private Long loginTypeCode;
}
这种方法需要使用javascript,对于部分人来说可能不够直观也很难调试。
二、自定义注解+spring表达式
针对这种情况我利用spring表达式写了一个自定义注解来解决这个问题。
2.1 自定义注解
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @Author 黄义波
* @Date 2024/12/30 16:31
* @Description
* 多属性关联校验注解
* 用于校验多个属性之间的关联关系
* 当when条件满足时,必须满足must条件否则校验不通过
* 注意:如果解析spel表达式错误将抛出异常
*/
@Documented
@Constraint(validatedBy = {MultiFieldAssociationCheckValidator.class })
@Target({ElementType.TYPE_USE})
@Retention(RetentionPolicy.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({ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@interface List {
MultiFieldAssociationCheck[] value();
}
}
2.2 编写注解校验类
import com.xtm.anno.MultiFieldAssociationCheck;
import com.xtm.utils.SpelUtils;
import lombok.SneakyThrows;
import org.apache.commons.lang3.StringUtils;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
/**
* @Author 黄义波
* @Date 2024/12/30 16:35
* @Description 多属性关联校验注解的实现类
*/
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 = SpelUtils.parseSpel(String.format(SPEL_TEMPLATE, SPEL_PREFIX, when, SPEL_SUFFIX), spelMap);
if (whenCheck) {
//判断must是否满足条件
boolean mustCheck = 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;
}
}
2.3 SpelUtils
import org.apache.commons.lang3.StringUtils;
import org.springframework.context.expression.BeanFactoryAccessor;
import org.springframework.context.expression.MapAccessor;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.common.TemplateParserContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import java.util.Map;
/**
* @Author 黄义波
* @Date 2024/12/31 9:17
* @Description
*/
public class SpelUtils {
public static Boolean parseSpel( String spel, Map<String, Object> map) {
if (StringUtils.isBlank(spel)) {
return Boolean.FALSE;
} else {
ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
context.setVariables(map);
context.addPropertyAccessor(new MapAccessor());
context.addPropertyAccessor(new BeanFactoryAccessor());
return parser.parseExpression(spel, new TemplateParserContext()).getValue(context, Boolean.class);
}
}
}
2.4 使用注解,这个注解使用在类上
@Data
@ApiModel(value = "User用户登录", description = "用户登录")
@MultiFieldAssociationCheck.List(
value = {
@MultiFieldAssociationCheck(when = "#loginTypeCode == 1", must = "#mobile != null && #password != null", message = "手机号密码登录,手机号和密码不能为空"),
@MultiFieldAssociationCheck(when = "#loginTypeCode == 2", must = "#mobile != null && #messageCode != null", message = "手机号验证码登录,手机号和验证码不能为空"),
@MultiFieldAssociationCheck(when = "#loginTypeCode == 3", must = "#idcardNo != null && #password != null", message = "身份证号密码登录,身份证号和密码不能为空")
}
)
public class User {
@ApiModelProperty(value = "手机号")
@NotBlank(message = "手机号不能为空")
private String mobile;
@ApiModelProperty(value = "身份证号")
private String idcardNo;
@ApiModelProperty(value = "密码")
private String password;
@ApiModelProperty(value = "登录短信验证")
private String messageCode;
@ApiModelProperty(value = "登录类型: 手机号密码登录:1, 手机号短信登录:2, 身份证密码登录:3", required = true)
@NotNull(message = "登录类型不能为空!")
@Min(value = 1, message = "登录类型值不能小于1")
@Max(value = 3, message = "登录类型值不能大于3")
private Long loginTypeCode;
}
三、单字段自定义注解校验
3.1 观察现有校验规则
尽管框架提供一些校验规则,难免遇到一些现有规则不能覆盖的情况,这里我们就一些特定情况做个自定义的参数校验。
我们可以先观察下现有校验规则是怎样的:
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(Min.List.class)
@Documented
@Constraint(
validatedBy = {}
)
public @interface Min {
String message() default "{jakarta.validation.constraints.Min.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
long value();
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface List {
Min[] value();
}
}
@Documented
@Constraint(
validatedBy = {}
)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(NotBlank.List.class)
public @interface NotBlank {
String message() default "{jakarta.validation.constraints.NotBlank.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface List {
NotBlank[] value();
}
}
在做自定义的参数校验的时候,除了自定义的注解接口,另外我们还需要再实现一个接口 ConstraintValidator。
我们在 User 类中加个参数,比如加个用户地区,限定 Asia 或 Ameraica,只有这两个地区的人才可以注册。
- Address
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.*;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {AddressValidator.class})
@Target({ElementType.METHOD, ElementType.FIELD,
ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR,
ElementType.PARAMETER, ElementType.TYPE_USE})
public @interface Address {
String message() default "不在合法地区范围内";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
- AddressValidator
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import java.util.Arrays;
public class AddressValidator implements ConstraintValidator<Address, String> {
private static String[] addresses = {"Asia", "America"};
@Override
public boolean isValid(String address, ConstraintValidatorContext context) {
if (Arrays.asList(addresses).contains(address)) {
return true;
}
return false;
}
}
- 在 User类 中加入 address 字段:
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
@NotBlank(message = "地址非空")
@Address
private String address;
参考:
https://blog.csdn.net/zhen_csdn/article/details/131941489