在日常开发中,复杂的业务接口需要做大量的参数校验,在业务代码中掺杂这许多if else判断,用于检验参数的有效性,影响核心业务代码的可读性,基于JSR303
标准实现的hibernate Validator是进行参数校验的一个重要工具,通过注解的方式就替代很多校验工作,如常用的@NotBlank可以检验字符串的非空,@NotNull检验对象的非空,@Max校验字符最大长度,@Email校验邮箱合法性等,这些能满足日常开发中的大部分场景,又的时候由于业务的复杂性,还需要自定义校验器来增强校验,本文将进行自定义的校验器的探讨。
首先通过一个开发场引入,在管理系统中通常有用户号找回密码的功能,通常为了增加用户找回密码的安全性,会提供手机号+验证码、邮箱+验证的方式,很显然用邮箱找回和手机找回两种方式需要校验的参数大部分相同,但是也有不同的,那么怎样能够在调用手机号找回校验手机号相关参数,邮箱找回校验邮箱相关参数,常规做法只需要使用groups即可实现。
一、@Validated分组(groups)校验
1、定义手机、邮箱分组
/**检验手机号参数*/
public interface IPhone {
}
/**校验邮件参数*/
public class IEmail {
}
2、找回密码的收参VO
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@ToString
public class ForgotPasswordVO {
/**找回方式*/
@NotBlank(message = "找回方式不能为空",groups = {IPhone.class, IEmail.class})
private String findWay;
/**手机号
* 使用手机号找回值校验手机号
* */
@NotBlank(message = "手机号不能为空",groups = {IPhone.class})
@Pattern(regexp = "^1(3\\d{1}|4[57]|5[012356789]|6[6]|7[0135678]|8\\d{1})\\d{8}$" ,message = "非法手机号" ,groups = {IPhone.class})
private String phone;
/**邮箱
* 通过groups分组使用邮箱找回只校验邮箱
* */
@NotBlank(message = "邮箱不能为空",groups = { IEmail.class})
@Email(message = "非法邮箱",groups = { IEmail.class})
private String email;
/**验证码*/
@NotBlank(message = "验证码不能为空",groups = {IPhone.class, IEmail.class})
private String verifyCode;
/**新密码*/
@NotBlank(message = "新密码不能为空",groups = {IPhone.class, IEmail.class})
private String newPassword;
}
3、接口参数校验
@RestController
@RequestMapping("/user")
public class UserController {
@PostMapping("/findPwdByPhone")
public String findPwdByPhone(@Validated(IPhone.class) @RequestBody ForgotPasswordVO forgotPasswordVO){
return "使用手机号找回密码成功";
}
@PostMapping("/findPwdByEmail")
public String findPwdByEmail(@Validated(IEmail.class) @RequestBody ForgotPasswordVO forgotPasswordVO){
return "使用邮箱找回密码成功";
}
}
使用分组可以很方便的解决同一个实体在不同场景接收不同的参数进行校验,但是也存在一些小瑕疵,需要根据不同的场景提供不同的控制器接口,这有可能会造成代码的冗余。
二、自定义校验器枚举值校验
对于常量的校验,比如性别 1 男 2 女这类枚举值,如何通过检验器校验调用方的传参合法性,可定义一个枚举类的校验器进行校验。
1、自定义校验注解
/**
* 检验枚举
*/
@Target(value = { METHOD, FIELD})
@Retention(RetentionPolicy.RUNTIME)
/// 指定校验器
@Constraint(validatedBy = { EnumValidator.class })
public @interface EnumValid {
/**校验失败时的返回信息*/
String message() default "";
/**分组校验*/
Class<?>[] groups() default {};
/**负载*/
Class<? extends Payload> [] payload() default {};
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
@Documented
@interface List {
EnumValid[] value();
}
/**枚举*/
Class<? extends Payload> constant();
/**枚举编码*/
String enumValue() default "getValue";
}
2、自定义检验器
public class EnumValidator implements ConstraintValidator<EnumValid, Object> {
private EnumValid enumValid;
@Override
public void initialize(EnumValid enumValid) {
this.enumValid = enumValid;
}
@Override
public boolean isValid(Object targetObj, ConstraintValidatorContext context) {
if (Objects.isNull(targetObj)) {
return false;
}
Class<? extends Payload> enumConstant = enumValid.constant();
try {
Payload[] payloads = enumConstant.getEnumConstants();
Method method = enumConstant.getMethod(enumValid.enumValue());
/// 遍历枚举值,和传递的枚举值做对比
Optional<Payload> first = Arrays.stream(payloads).filter(e -> {
try {
return targetObj.equals(method.invoke(e));
} catch (IllegalAccessException | InvocationTargetException ex) {
return false;
}
}).findFirst();
return first.isPresent();
} catch (Exception e) {
e.fillInStackTrace();
}
return false;
}
}
3、枚举类
/**
* 找回方式
*/
public enum FindWayEnum implements Payload {
BY_WAY_OF_PHONE("10","通过手机号找回密码") ,
BY_WAY_OF_EMAIL("20","通过邮箱找回密码");
private final String value;
private final String name;
FindWayEnum(String value, String name) {
this.value = value;
this.name = name;
}
public String getValue() {
return value;
}
public String getName() {
return name;
}
}
4、校验目标字段
/**找回方式*/
@NotBlank(message = "找回方式不能为空",groups = {IPhone.class, IEmail.class})
@EnumValid( constant = FindWayEnum.class, message = "不合法的找回方式")
private String findWay;
当把findWay的至送参不是10和20的时候就会校验失败。