趁热记录下,给未来的自己
0. 前言
Spring Validation 作为一个参数验证框架,本身提供的注解已经很强大了,能覆盖大部分业务场景需求,比如:@NotNull, @NotBlank, @Min, @Max, @Size, @Email等等。
但是对于更加复杂的业务场景,Spring Validation 自带的这些注解就无能为力了,比如,假设有个接口,其中的一个入参是 String 类型,由于业务需要,该入参值的范围只能在一个允许的 list 中,不在范围中的传入值,直接返回异常。
常规做法是在业务代码里对入参做一层逻辑判断,但是这样做会导致业务代码和校验代码耦合,且开发效率不高,代码十分不美观。 更加优雅的做法是自定义一个 validator 配合枚举类,来实现这个需求。
1. 架构图
模块说明:
1: DTO - 接收入参的DTO类,使得收到的参数变成一个对象
1-1: private String para 对象加了 @EnumCheck 注解,表示 para 需要被校验
1-2: private String para1 对象未加 @EnumCheck 注解,表示 para1 不需要被校验
2: EnumCheck - 是一个注解类,需要在该类里实现自定义注解
3: EnumUtil - 枚举工具类,通过传入一个枚举参数,判断该参数是否在指定的枚举里,存在则返回枚举,不存在返回null
4: Enum Class - 用户定义的枚举类,用于存放入参的范围
5: 异常逻辑 - 当EnumCheck失败后,进入异常逻辑
6: 后续业务逻辑 - 当EnumCheck成功后,进入后续的业务逻辑
2. 代码说明
2.1 JenkinsProcessBuildReqDTO.java
入参用该DTO类接收,在该类中定义需要被自定义validate的一个或者多个字段。
@Data
public class JenkinsProcessBuildReqDTO{
/** * 部署环境 */
@EnumCheck(message = "environment输入有误", enumClass = JenkinsProcessEnum.class)
private String environment;
}
复制代码
2.2 JenkinsProcessEnum.java
用户自定义的枚举类,用于存放入参的范围,如该枚举类中,限定了 JenkinsProcessBuildReqDTO.environment 的范围在 DEPLOY_SIT, DEPLOY_UAT, DEPLOY_SIT_UAT 这三个枚举里。
import lombok.Getter;
@Getter
public enum JenkinsProcessEnum{
/** * sit 环境 */
DEPLOY_SIT("0", "SIT", "sit 环境"),
/** * uat 环境 */
DEPLOY_UAT("1", "UAT", "uat 环境"),
/** * sit 和 uat 环境 */
DEPLOY_SIT_UAT("2", "SIT_UAT", "sit 和 uat 环境"),
;
private String code;
private String name;
private String desc;
JenkinsProcessEnum(String code, String name, String desc) {
this.code = code;
this.name = name;
this.desc = desc;
}
}
复制代码
2.3 EnumUtil.java
该类中提供了一个方法getEnumByParameter: 通过传入一个枚举参数 enumParameter,判断该参数是否在指定的枚举 clazz 里。存在则返回 enumParameter 对应的枚举,不存在返回 null
@Slf4j
public class EnumUtil{
/** * * @author arkMon * @date 14:22 2021/2/23 * @param clazz 传入的枚举类名 * @param getEnumMethodName 传入的枚举类clazz中的方法名称 * @param enumParameter 枚举参数 * @return T 具体的枚举值 或者 null */
public static > T getEnumByParameter(Class clazz, String getEnumMethodName, Object enumParameter){
T result = null;
try{
//Enum接口中没有values()方法,我们仍然可以通过Class对象取得所有的enum实例
T[] arr = clazz.getEnumConstants();
//获取定义的方法
Method targetMethod = clazz.getDeclaredMethod(getEnumMethodName);
if (targetMethod == null) {
log.error("getEnumMethodName=" + getEnumMethodName + " 不存在");
return null;
}
Object typeVal;
//遍历枚举定义
for(T entity:arr){
if (enumParameter instanceof Integer) {
typeVal = Integer.valueOf(String.valueOf(targetMethod.invoke(entity)));
} else if (enumParameter instanceof String) {
//获取传过来方法的
typeVal = String.valueOf(targetMethod.invoke(entity)).replace(" ","");
//执行的方法的值等于参数传过来要判断的值
enumParameter = ((String) enumParameter).replace(" ", "");
} else {
log.error("传入的enumType不是Integer也不是String类型");
return null;
}
if(typeVal.equals(enumParameter)){
//返回这个枚举
result = entity;
break;
}
}
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException e) {
e.printStackTrace();
}
return result;
}
}
复制代码
2.4 EnumCheck.java
自定义验证器的注解。在该注解下的validator类中的isValid方法里,实现自定义验证器的逻辑:
判断传入的参数是否可以通过枚举里的code或者name,用getEnumByParameter方法找到其对应的枚举,能找到返回true,不能找到返回false。
@Target({ ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = EnumCheck.Validator.class)
public @interface EnumCheck {
String message() default "{enum.value.invalid}"; // 错误信息 Class extends Enum>> enumClass(); // 枚举类 String enumMethodCode() default "getCode"; // 枚举校验方法 String enumMethodName() default "getName"; // 枚举校验方法 boolean allowNull() default false; // 是否允许为空 class Validator implements ConstraintValidator { private Class extends Enum>> enumClass; private String enumMethodCode; private String enumMethodName; private boolean allowNull; @Override public void initialize(EnumCheck enumValue) { enumMethodCode = enumValue.enumMethodCode(); enumMethodName = enumValue.enumMethodName(); enumClass = enumValue.enumClass(); allowNull = enumValue.allowNull(); } @Override public boolean isValid(Object value, ConstraintValidatorContext constraintValidatorContext) { if (value == null) { return allowNull; } if (enumClass == null) { return Boolean.TRUE; } JenkinsProcessEnum enumByParameter = EnumUtil.getEnumByParameter(JenkinsProcessEnum.class, enumMethodCode, value); if (enumByParameter != null) { return true; } JenkinsProcessEnum enumByParameter1 = EnumUtil.getEnumByParameter(JenkinsProcessEnum.class, enumMethodName, value); if (enumByParameter1 != null) { return true; } return false; } } }复制代码
Ref