1. 简介
最近在项目中遇到一个问题。简单来说就是对对象的属性进行有关联性的校验。
比如:登录接口
public ResponseResult login(@Validated(Groups.Query.class) @RequestBody LoginReqVO loginRepVO){
//do something
}
public class LoginReqVO {
@NotNull(message = "userId 不能为空", groups = {Groups.Query.class, Groups.Search.class})
@ApiModelProperty(required = true, value = "用户应用id", example = "123456")
private String userId;
@NotNull(message = "pwd 不能为空", groups = {Groups.Query.class, Groups.Search.class})
@ApiModelProperty(required = true, value = "用户密码", example = "200")
private String pwd;
@NotNull(message = "appId 不能为空", groups = {Groups.Query.class, Groups.Search.class})
@ApiModelProperty(required = true, value = "应用id", example = "1111")
private String appId;
@ApiModelProperty(value = "加密类型(0原始数据传输,1加密传输)默认0", example = "0")
private Integer encryptType;
@ApiModelProperty(value = "公钥加密过的秘钥(当encryptType为1是为必传字段)", example = "xxxx")
private String key;
@ApiModelProperty(value = "公钥版本号(当encryptType为1是为必传字段)", example = "1.0.0")
private String version;
}
在上面的登录的请求对象中存在这样的一个需求:
当加密类型字段encryptType为1(数据加密传输)时,对应的key(密钥)和version(公钥版本号)不能为空。如果encryptType为0时,对应的key和version传不传都是可以的。
一开始最为简单,也是最先想到的方式便是和业务功能耦合在一起,在login方法内部进行对这三个参数进行校验。代码编写起来也是最为简单的。
但是这种方式的缺点就是功能性代码和业务代码耦合在了一起,而且之前所有的参数校验都是通过JSR303进行的,采用这种方式也打破了之前的代码习惯,可能有点小强迫症吧。hahaha
所以便有了一下两种自定义校验方式的探索:
2. @ScriptAssert注解
@ScriptAssert注解的作用:对于复杂业务逻辑可以通过脚本验证
先来了解一下@ScriptAssert注解
@Documented
@Constraint(
validatedBy = {}
)
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(ScriptAssert.List.class)
public @interface ScriptAssert {
String message() default "{org.hibernate.validator.constraints.ScriptAssert.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String lang();
String script();
String alias() default "_this";
String reportOn() default "";
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface List {
ScriptAssert[] value();
}
}
首先我们看到@Target({ElementType.TYPE})
表明整个注解适用于类或接口上的。
@Repeatable(ScriptAssert.List.class)
这个注解作用:表明@ScriptAssert是可以多个一起使用的。如下面这样:
@ScriptAssert.List({
@ScriptAssert(script = "com.RegisterUserDto.checkUserName(_this.userName)",lang = "javascript",message = "用户名不能包含! @ # $ % & * \\ / ? ?特殊符号"),
@ScriptAssert(script = "_this.password.equals(_this.rePassword)",lang = "javascript",message = "确认密码与输入密码不一致")
})
下面三个属性是每一个JSR303注解中都存在的内容,大家经常使用JSR303的话,肯定也都不陌生。
String message() default "{org.hibernate.validator.constraints.ScriptAssert.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
message()
该注解判断为错误时的异常提示信息。
groups()
分组。
payload()
负载信息,比较少用。
上面三个属性也是自定义注解中必须存在的属性。
接下来我们在看看@ScriptAssert
注解中特有的属性。
String lang();
String script();
String alias() default "_this";
String reportOn() default "";
lang()
表示采用采用什么语言来执行脚本。lang = "javascript"
表示通过Java语言来执行script中脚本。
script()
指定特定的静态方法
来检验复杂的业务逻辑。
这里的指定的方法必须是静态的方法,否则会抛出异常。
Caused by: jdk.nashorn.internal.runtime.ECMAException: TypeError: com.crossoverjie.cim.route.api.validated.ValidatedLoginParameter.checkVersion is not a function
alias()
对象的别名,默认是_this
。
reportOn()
表明该验证是作用在那个方法上。这里起到作用就是在方法抛出异常的时候,可以具体到哪一个属性。因为@ScriptAssert是作用在整个对象上。所以整个属性还是非常有必要的。
以上便是所有的@ScriptAssert的属性介绍。
下面来看看如何使用:
- 首先自定义需要检验的方法
public class ValidatedLoginParameter {
/**
* 验证加密时,公钥加密过的秘钥不能为空
*
* @param encryptType
* @param key
* @return
*/
public static Boolean checkKey(Integer encryptType, String key) {
if (encryptType == 1) {
if (key == null) {
return false;
}
}
return true;
}
/**
* 验证加密时,公钥版本号不能为空
*
* @param encryptType
* @param version
* @return
*/
public static Boolean checkVersion(Integer encryptType, String version) {
if (encryptType == 1) {
if (version == null) {
return false;
}
}
return true;
}
}
- 在需要验证的类上添加注解
@Data
@ScriptAssert.List({
@ScriptAssert(lang = "javascript",
script = "com.crossoverjie.cim.route.api.validated.ValidatedLoginParameter.checkKey(_this.encryptType,_this.key)",
message = "密钥不能为空",
reportOn = "key",
groups = {Groups.Query.class}
),
@ScriptAssert(lang = "javascript",
script = "com.crossoverjie.cim.route.api.validated.ValidatedLoginParameter.checkVersion(_this.encryptType,_this.version)",
message = "公钥版本号不能为空",
reportOn = "version",
groups = {Groups.Query.class}
)
})
public class LoginReqVO {
@NotNull(message = "userId 不能为空", groups = {Groups.Query.class, Groups.Search.class})
@ApiModelProperty(required = true, value = "用户应用id", example = "123456")
private String userId;
@NotNull(message = "pwd 不能为空", groups = {Groups.Query.class, Groups.Search.class})
@ApiModelProperty(required = true, value = "用户密码", example = "200")
private String pwd;
@NotNull(message = "appId 不能为空", groups = {Groups.Query.class, Groups.Search.class})
@ApiModelProperty(required = true, value = "应用id", example = "1111")
private String appId;
@ApiModelProperty(value = "加密类型(0原始数据传输,1加密传输)默认0", example = "0")
private Integer encryptType;
@ApiModelProperty(value = "公钥加密过的秘钥(当encryptType为1是为必传字段)", example = "xxxx")
private String key;
@ApiModelProperty(value = "公钥版本号(当encryptType为1是为必传字段)", example = "1.0.0")
private String version;
}
这样就可以完成最开始所讲的需求了。当encryptType为1时验证key和version,当为0时,就不验证key和version。
3. 自定义注解
我们再来使用自定义注注解的方式来实现上述的功能:
- 声明自定义注解
@Target({ElementType.TYPE})
@Retention(RUNTIME)
@Documented
// 1. Marks an annotation as being a Bean Validation constraint
// 2. validatedBy = 具体校验类
@Constraint(validatedBy = { AssociationValidator.class})
public @interface Association {
//校验不通过的时候打印的信息
String message() default "属性值不能为null";
//校验组,用于分组校验
Class<?>[] groups default{};
//负载信息
Class<? extends Payload> [] payload() default{};
}
- 实现具体的校验类
public class AssociationValidator implements ConstraintValidator<Association, Object> {
private Association association;
@Override
public void initialize(Association constraintAnnotation) {
association = constraintAnnotation;
}
/**
* @param value 被注解的对象
* @param context
* @return true 校验通过 false 校验失败
*/
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
return association(value);
}
/**
* 校验方法
*/
private boolean association(Object value) {
Field encryptType = value.class.getDeclaredField("encryptType");
Field key = value.class.getDeclaredField("key");
Field version = value.class.getDeclaredField("version");
Integer type = (Integer)encryptType .get(value);
if(type == 1){
String keyStr = (String)key.get(value);
String versionStr = (String)version.get(value);
if(keyStr==null||versionStr==null){
return false;
}
}
return true;
}
}
- 使用
@Data
@Association
public class LoginReqVO {
@NotNull(message = "userId 不能为空", groups = {Groups.Query.class, Groups.Search.class})
@ApiModelProperty(required = true, value = "用户应用id", example = "123456")
private String userId;
@NotNull(message = "pwd 不能为空", groups = {Groups.Query.class, Groups.Search.class})
@ApiModelProperty(required = true, value = "用户密码", example = "200")
private String pwd;
@NotNull(message = "appId 不能为空", groups = {Groups.Query.class, Groups.Search.class})
@ApiModelProperty(required = true, value = "应用id", example = "1111")
private String appId;
@ApiModelProperty(value = "加密类型(0原始数据传输,1加密传输)默认0", example = "0")
private Integer encryptType;
@ApiModelProperty(value = "公钥加密过的秘钥(当encryptType为1是为必传字段)", example = "xxxx")
private String key;
@ApiModelProperty(value = "公钥版本号(当encryptType为1是为必传字段)", example = "1.0.0")
private String version;
}