首发公众号: 赵侠客
引言
在后端接口开发中经常需要对接口传入的参数进行校验,如非空校验、长度校验、手机号码格式校验、邮箱格式校验等等,常用的if-else
判断虽然可以胜任任何场景的参数校验,但是有着开发效率低、冗余代码多、代码复用性差等问题,于是就出现了像validation-api
这种只要增加一个@NotNull
、@Email
这样的注解就可以很优雅的进行参数校验的框架,validation-api
在项目中的使用是非常广泛,也是非常方便的,但是它并不是万能的,在某些特定的情况下是无法很优雅的完成参数校验的,比如今天要讨论的:在不同参数条件下对不同的参数进行校验的场景它就无法完成了 。本文针对这种场景开发出validation-plus
来扩展validation-api
,让它能更方便、快捷的适用于更多、更广泛的参数校验场景。
validation-api 简单使用
以最简单的修改用户信息接口为例简单介绍一下validation-api
的使用,只需要在接收参数的实体对象上增加@NotNull(message = "用户ID不能为空")
注解,然后在接收接口的方法中增加@Valid
就可以完成用户ID不能为空的参数校验了。
User
实体参数增加@NotNull
注解:
java
@Data
public class User {
@NotNull(message = "用户ID不能为空")
private Long id;
@NotNull(message = "用户名不能为空")
private String userName;
}
用户修改方法增加@Valid
注解:
java
@PostMapping("/update")
public Map<String, Object> update(@RequestBody @Valid User user) {
return Map.of("code", 0, "data", user);
}
但是实际开发过程的参数校验可能并不是这么简单,比如我们需要开发一个保存用户通知的接口,用户可以选择邮箱或者手机号码来接收通知消息。首先我们需要定义UserNotice
对象:
java
@Data
public class UserNotice {
@NotNull(message = "通知类型不能为空 0-短信 1-邮件")
private Integer noticeType;
private String userMobile;
private String userEmail;
}
在UserNotice
对象中noticeType
通知类型肯定是不能为空的,我们以:
- 0-表示使用短信接收通知消息
- 1-表示使用邮箱接收通知消息
那么问题来了:我们不能直接在userMobile
或者userEmail
增加@NotNull
- 只有当noticeType=0时userMobile不能为空
- 当noticeType=1时userEmail才不能为空
在这种情况下validation-api
就无法完成参数校验了。
方案一、增加额外方法
最开始我想到的解决方案是:写一个方法,在方法里添加条件判断逻辑,然后增加@AssertTrue
注解达到按条件进行参数校验的目的
java
@Data
public class UserNotice {
@NotNull(message = "通知类型不能为空 0-短信 1-邮件")
private Integer noticeType;
private String userMobile;
private String userEmail;
@AssertTrue(message = "手机不能为空")
public Boolean getMobileValidator() {
return ObjectUtils.nullSafeEquals(noticeType, 1) || Validator.isMobile(userMobile);
}
@AssertTrue(message = "邮箱地址不能为空")
public Boolean getEmailValidator() {
return ObjectUtils.nullSafeEquals(noticeType, 0) || Validator.isEmail(userEmail);
}
}
但是这种方案看起来并不是很优雅,主要有以下问题:
- 还是增加了大量的if-else判断代码
- 自己还要写很多类似
Validator.isEmail()
这样的数据格式判断 - 没有直接的用
validation-api
中提供的丰富数据格式类型的判断
方案二、自己造轮子
我的想法很简单:在注解中增加一个条件参数,写上EL表达式,如果EL表达式为True则执行参数校验,为False就不执行
比如我们要自定义一个IPV4参数校验器的代码:
java
@Documented
@Target({ METHOD, FIELD, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(Ipv4.List.class)
@Constraint(validatedBy = {Ipv4Validator.class})
public @interface Ipv4 {
String message() default "not is Ipv4";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
@Target({ METHOD, FIELD, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
@interface List {
Ipv4[] value();
}
}
public class Ipv4Validator implements ConstraintValidator<Ipv4, String> {
@Override
public void initialize(Ipv4 constraintAnnotation) {
ConstraintValidator.super.initialize(constraintAnnotation);
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
// 对象为空验证通过
if (value == null) {
return true;
}
return Validator.isIpv4(value);
}
}
按照这个思路我的想法是自定义一个@NotNullOn
,然后通过EL表达式编写判断的条件:
java
@Data
@EnableCondition
public class UserNotice {
@NotNull(message = "通知类型不能为空 0-短信 1-邮件")
private Integer noticeType;
@NotNullOn(on="#noticeType=0", message = "手机不能为空")
private String userMobile;
@NotNullOn(on="#noticeType=1", message = "邮箱地址不能为空")
private String userEmail;
}
但是@NotNullOn
增加在字段上,是对字段进行拦截的,肯定是拿不到UserNotice对象,所以无法执行on中的El表达式,必须对类进行拦截,所以还要定义一个@EnableCondition
注解,添加在类上。
添加开启条件注解@EnableCondition
:
java
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Constraint(validatedBy = ConditionValidator.class)
public @interface EnableCondition {
@Deprecated
String message() default "";
@Deprecated
Class<?>[] groups() default { };
@Deprecated
Class<? extends Payload>[] payload() default { };
}
添加条件不为空注解@NotNullOn
JAVA
@Documented
@Target({ FIELD })
@Retention(RUNTIME)
public @interface NotNullOn {
String on();
String message() default "";
}
添加条件True注解@NotNullOn
JAVA
@Documented
@Target({ FIELD })
@Retention(RUNTIME)
public @interface AssertTrueOn {
String on();
String message() default "";
}
添加条件False空注解@AssertFalseOn
JAVA
@Documented
@Target({ FIELD })
@Retention(RUNTIME)
public @interface AssertFalseOn {
String on();
String message() default "";
}
添加注解解析器:ConditionValidator
java
@Slf4j
public class ConditionValidator implements ConstraintValidator<EnableCondition, Object> {
private final ExpressionParser parser = new SpelExpressionParser();
private final EvaluationContext elContext = new StandardEvaluationContext();
@Override
public boolean isValid(Object validatedBean, ConstraintValidatorContext context) {
fillBean(validatedBean);
Field[] fields = ReflectUtil.getFields(validatedBean.getClass());
Boolean res = true;
for (Field field : fields) {
//获取当前字段
Object fieldValue = ReflectUtil.getFieldValue(validatedBean, field.getName());
Annotation[] annotations = field.getAnnotations();
for (Annotation annotation : annotations) {
if (annotation instanceof NotNullOn) {
NotNullOn notNullOn = (NotNullOn) annotation;
res = res && isValid(NotNullOn.class.getName(), notNullOn.message(), context, notNullOn.on(), field.getName(), fieldValue);
}
if (annotation instanceof AssertTrueOn) {
AssertTrueOn assertTrueOn = (AssertTrueOn) annotation;
res = res && isValid(AssertTrueOn.class.getName(), assertTrueOn.message(), context, assertTrueOn.on(), field.getName(), fieldValue);
}
if (annotation instanceof AssertFalseOn) {
AssertFalseOn assertTrueOn = (AssertFalseOn) annotation;
res = res && isValid(AssertFalseOn.class.getName(), assertTrueOn.message(), context, assertTrueOn.on(), field.getName(), fieldValue);
}
}
}
return res;
}
private boolean isValid(String name, String message, ConstraintValidatorContext context, String on, String fieldName, Object fieldValue) {
Boolean res = true;
if (parseEl(on)) {
ConstraintValidator validator = SupportContext.getValidator(name);
if (!validator.isValid(fieldValue, context)) {
res = false;
context.buildConstraintViolationWithTemplate(message)
.addPropertyNode(fieldName)
.addConstraintViolation();
}
}
return res;
}
private void fillBean(Object object) {
Map<String, Object> map = BeanUtil.beanToMap(object);
map.forEach((k, v) -> elContext.setVariable(k, v));
}
protected Boolean parseEl(String el) {
Expression expression = parser.parseExpression(el);
Object value = expression.getValue(elContext);
return Boolean.valueOf(value.toString());
}
}
代码的基本思路是这样的
@EnableCondition
注解开启按条件参数校验,添加到类上,拦截整个对象@NotNullOn
、@AssertTrueOn
、@AssertFalseOn
等为自己扩展的条件注解- 在
ConditionValidator
通过判断if (parseEl(on))
解析条件 - 在isValid()调用
validation-api
原有的方法进行参数判断
开源地址
本代码已开源gitee地址:
使用方法
java
<dependency>
<groupId>com.zhaochao</groupId>
<artifactId>validation-plus</artifactId>
<version>1.0.0</version>
</dependency>
java
@Data
@EnableCondition
public class UserNotice {
@NotNull(message = "通知类型不能为空 0-短信 1-邮件")
private Integer noticeType;
@NotNullOn(on = "#noticeType==0", message = "手机号码不能为空")
private String userMobile;
@NotNullOn(on = "#noticeType==1", message = "邮箱不能为空")
private String userEmail;
@AssertTrueOn(on = "#noticeType==0", message = "必须是男生")
private Boolean isBoy;
}
less
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
@PostMapping("/notice")
public Map<String, Object> save(@RequestBody @Valid UserNotice user) {
return Map.of("code", 0, "data", user);
}
}
bash
POST http://localhost/user/notice
Content-Type: application/json
{
"noticeType":0,
"userMobile":"18093493432",
"isBoy":false
}
返回:
css
{
"code": 500,
"message": "必须是男生"
}
是不是非常的Nice?
总结
本方案解决了validation-api
无法在不同条件下使用不同参数判断的业务场景的痛点,扩展出了 @NotNullOn
、@AssertTrueOn
、@AssertFalseOn
等按条件进行参数校验的场景。看看大家有没有类似的需求场景,目前支持的类型比较少,如果大家也有类似的场景,我可以继续完善validation-plus
,扩展出更多条件参数校验的类型。