背景
- 我们用了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;
}
}
}
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";
}
最终效果
结语和代码地址
更新
2022年3月16日21:34:10 一个字段支持重复注解(@Repeatable)