背景
- 我们用了Spring框架后,在校验前端参数的时候,一般会使用
@NotNull
、@NotBlank
等注解,这样就不用写业务代码,判断这个字段了,省力,又简洁优雅,对真实业务处理的代码无侵入。 - 但是多个字段关联的业务就比较麻烦,比如下列场景。
public class TestValidate {
@NotBlank(message = "姓名必填")
private String name;
@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 {
private final LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();
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);
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();
if (ValidateFieldAction.IF_EQ_NOT_NULL == conditionalValidateField.action()) {
Class originalClz = fieldClzMap.get(conditionalValidateFieldInfo.getFieldName());
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;
@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;
}
}
@RequestMapping("/test")
@ConditionalValidate
public String test(@Validated TestValidate testValidate) {
return "success";
}
最终效果

结语和代码地址
更新
2022年3月16日21:34:10 一个字段支持重复注解(@Repeatable)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 25岁的心里话