前言
做开发的应该都写过类似的校验方法,最简单的判断null,复杂一点的字符串正则校验。单纯从逻辑上说这类代码不复杂,但是有没有一种可能假如需要一次性判断多个,再加上判断失败后的处理逻辑,还有可能需要一些特殊处理,比如正则校验,等等,三个一下,写写也还行,五个以上,如果代码按行收费的话,那能够狠狠的赚了一笔。
大家脑海里的代码可能是这样
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @RestController public class SysController {
@GetMapping("by_id") Object getById(String id){ if (id == null) { return "msg: id为null"; } if (id.length() > 10){ return "id,长度过长"; }
return null; } }
|
这只是一个,实际开发中有些录入非常复杂的表单数据时可能有三四十个,上面我写的处理逻辑也不复杂,真实中可能有些需要使用异常提示,有些需要直接return返回信息,等等,复杂且难以维护。难以维护,到并不是不能维护,钱给的够多,维护,也还是能维护的,当然这个杠不能这样抬。🤣😂😂😂
介绍一个工具,应该说是一套工具,基于注解和反射。
看到这两个词,性能有所损耗是很正常的,比如我们开发全员Map,其实代码的运行速度来说,不慢,但是总所周知HASH(哈希)表是一种非常耗内存的数据结构,所以代码在本地跑的时候时不时的omm异常一下也是合符情理的一件事。😁😁,现在这硬件发展的环境,应该不缺内存这东西。
validation是什么
框架名字叫javax.validation,它提供了一系列的注解来进行对JavaBean的参数校验。通常用来处理Controller里面方法的参数的校验。就是暴露出去的API,内部的方法也可以用,但没必要,暴露出去的接口获取的数据是不可控的,所以才需要校验,内部的数据是可控的,当然内部方法可控的前提是源头的数据是可控的。
安装
springboot中有些博客中写的是这个依赖默认包含在starter-web中了,不清楚从哪个版本开始就不包含在其中了,就说现在的环境是springboot2.5.x系列的,validation默认已经不包含在starter-web中了,需要单独引入这个依赖。
1 2 3 4 5
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency>
|
在其他项目中可以引入这个依赖,并自己调整版本即可使用。
1 2 3 4 5
| <dependency> <groupId>org.hibernate.validator</groupId> <artifactId>hibernate-validator</artifactId> <version>6.0.2.Final</version> </dependency>
|
注解说明
验证注解 |
验证的数据类型 |
说明 |
@AssertFalse |
Boolean,boolean |
验证注解的元素值是false |
@AssertTrue |
Boolean,boolean |
验证注解的元素值是true |
@NotNull |
任意类型 |
验证注解的元素值不是null |
@Null |
任意类型 |
验证注解的元素值是null |
@Min(value=值) |
BigDecimal,BigInteger, byte,short, int, long,等任何Number或CharSequence(存储的是数字)子类型 |
验证注解的元素值大于等于@Min指定的value值 |
@Max(value=值) |
和@Min要求一样 |
验证注解的元素值小于等于@Max指定的value值 |
@DecimalMin(value=值) |
和@Min要求一样 |
验证注解的元素值大于等于@ DecimalMin指定的value值 |
@DecimalMax(value=值) |
和@Min要求一样 |
验证注解的元素值小于等于@ DecimalMax指定的value值 |
@Digits(integer=整数位数, fraction=小数位数) |
和@Min要求一样 |
验证注解的元素值的整数位数和小数位数上限 |
@Size(min=下限, max=上限) |
字符串、Collection、Map、数组等 |
验证注解的元素值的在min和max(包含)指定区间之内,如字符长度、集合大小 |
@Past |
java.util.Date,java.util.Calendar;Joda Time类库的日期类型 |
验证注解的元素值(日期类型)比当前时间早 |
@Future |
与@Past要求一样 |
验证注解的元素值(日期类型)比当前时间晚 |
@NotBlank |
CharSequence子类型 |
验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的首位空格 |
@Length(min=下限, max=上限) |
CharSequence子类型 |
验证注解的元素值长度在min和max区间内 |
@NotEmpty |
CharSequence子类型、Collection、Map、数组 |
验证注解的元素值不为null且不为空(字符串长度不为0、集合大小不为0) |
@Range(min=最小值, max=最大值) |
BigDecimal,BigInteger,CharSequence, byte, short, int, long等原子类型和包装类型 |
验证注解的元素值在最小值和最大值之间 |
@Email(regexp=正则表达式,flag=标志的模式) |
CharSequence子类型(如String) |
验证注解的元素值是Email,也可以通过regexp和flag指定自定义的email格式 |
@Pattern(regexp=正则表达式,flag=标志的模式) |
String,任何CharSequence的子类型 |
验证注解的元素值与指定的正则表达式匹配 |
@Valid |
任何非原子类型 |
指定递归验证关联的对象如用户对象中有个地址对象属性,如果想在验证用户对象时一起验证地址对象的话,在地址对象上加@Valid注解即可级联验证 |
这里只列出Hibernate Validator提供的大部分验证约束注解,如需了解更多请参考hibernate validator官方文档了解其他验证约束注解和进行自定义的验证约束注解定义。这块内容基本就是翻译文档,没啥好说的。
实际使用
1 2 3 4 5 6 7 8 9 10 11 12 13
| @PostMapping("/save/valid") public Object save(@RequestBody @Validated UserDTO userDTO) { userService.save(userDTO); return "成功"; }
@GetMapping("/get/id") public Object save(@NotBlank String id) { userService.save(userDTO); return "成功"; }
|
DTO 上的注解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| @Data public class UserDTO implements Serializable {
private static final long serialVersionUID = 1L;
@NotNull(message = "用户id不能为空") private Long userId; @NotBlank(message = "用户名不能为空") @Length(max = 20, message = "用户名不能超过20个字符") @Pattern(regexp = "^[\\u4E00-\\u9FA5A-Za-z0-9\\*]*$", message = "用户昵称限制:最多20字符,包含文字、字母和数字") private String username; @NotBlank(message = "手机号不能为空") @Pattern(regexp = "^[1][3,4,5,6,7,8,9][0-9]{9}$", message = "手机号格式有误") private String mobile;
private String sex;
@NotBlank(message = "联系邮箱不能为空") @Email(message = "邮箱格式不对") private String email;
private String password;
@Future(message = "时间必须是将来时间") private Date createTime;
}
|
全局异常处理
参数校验失败时会报异常,但是把异常直接显示给用户是非常不友好的,于是需要全局处理一下异常。
默认异常信息
1 2 3 4 5 6 7
| { "timestamp": "2022-01-01T04:41:23.164+00:00", "status": 500, "error": "Internal Server Error", "message": "", "path": "/by_id" }
|
全局异常拦截器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
| @RestControllerAdvice public class GlobalExceptionHandler {
private Logger logger = LoggerFactory.getLogger(getClass());
private static int DUPLICATE_KEY_CODE = 1001; private static int PARAM_FAIL_CODE = 1002; private static int VALIDATION_CODE = 1003;
@ExceptionHandler(BizException.class) public Object handleRRException(BizException e) { logger.error(e.getMessage(), e); return ""; }
@ExceptionHandler(MethodArgumentNotValidException.class) public Object handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { logger.error(e.getMessage(), e); return ""; }
@ExceptionHandler(ValidationException.class) public Object handleValidationException(ValidationException e) { logger.error(e.getMessage(), e); return ""; }
@ExceptionHandler(ConstraintViolationException.class) public Object handleConstraintViolationException(ConstraintViolationException e) { logger.error(e.getMessage(), e); return ""; }
@ExceptionHandler(NoHandlerFoundException.class) public Object handlerNoFoundException(Exception e) { logger.error(e.getMessage(), e); return ""; }
@ExceptionHandler(DuplicateKeyException.class) public Object handleDuplicateKeyException(DuplicateKeyException e) { logger.error(e.getMessage(), e); return ""; }
@ExceptionHandler(Exception.class) public Object handleException(Exception e) { logger.error(e.getMessage(), e); return ""; } }
|
自定义参数注解
如果默认的注解不够用,你还可以自定义注解,比如一个身份证号的验证
注解
1 2 3 4 5 6 7 8 9 10 11 12
| @Documented @Target({ElementType.PARAMETER, ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = IdentityCardNumberValidator.class) public @interface IdentityCardNumber {
String message() default "身份证号码不合法";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {}; }
|
自定义Validator
这里才是才是真正的处理逻辑,注解类似是一个徽章,在AOP处理处理时,是一个特殊的标识。
1 2 3 4 5 6 7 8 9 10 11
| public class IdentityCardNumberValidator implements ConstraintValidator<IdentityCardNumber, Object> {
@Override public void initialize(IdentityCardNumber identityCardNumber) { }
@Override public boolean isValid(Object o, ConstraintValidatorContext constraintValidatorContext) { return IdCardValidatorUtils.isValidate18Idcard(o.toString()); } }
|
使用
1 2 3
| @NotBlank(message = "身份证号不能为空") @IdentityCardNumber(message = "身份证信息有误,请核对后提交") private String clientCardNo;
|
使用groups的校验
就是分组校验,一个实体类作为不同的接口参数校验时,可能有不同的需求。定义两个接口。
1 2 3 4 5 6 7 8 9
| import javax.validation.groups.Default;
public interface Create extends Default { }
import javax.validation.groups.Default;
public interface Update extends Default{ }
|
再在需要校验的地方@Validated声明校验组
1 2 3 4 5
| @PostMapping("/update/groups") public RspDTO update(@RequestBody @Validated(Update.class) UserDTO userDTO) { userService.updateById(userDTO); return RspDTO.success(); }
|
DTO 改一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| @Data public class UserDTO implements Serializable {
private static final long serialVersionUID = 1L;
@NotNull(message = "用户id不能为空", groups = Update.class) private Long userId;
@NotBlank(message = "用户名不能为空") @Length(max = 20, message = "用户名不能超过20个字符", groups = {Create.class, Update.class}) @Pattern(regexp = "^[\\u4E00-\\u9FA5A-Za-z0-9\\*]*$", message = "用户昵称限制:最多20字符,包含文字、字母和数字") private String username;
@NotBlank(message = "手机号不能为空") @Pattern(regexp = "^[1][3,4,5,6,7,8,9][0-9]{9}$", message = "手机号格式有误", groups = {Create.class, Update.class}) private String mobile;
private String sex;
@NotBlank(message = "联系邮箱不能为空") @Email(message = "邮箱格式不对") private String email;
private String password;
@Future(message = "时间必须是将来时间", groups = {Create.class}) private Date createTime;
}
|
tips 注意:在声明分组的时候尽量加上 extend javax.validation.groups.Default
否则,在你声明@Validated(Update.class)
的时候,就会出现你在默认没添加groups = {}
的时候的校验组@Email(message = "邮箱格式不对")
,会不去校验,因为默认的校验组是groups = {Default.class}
restful风格用法
在多个参数校验,或者@RequestParam 形式时候,需要在controller上加注@Validated
1 2 3 4 5 6 7 8
| @GetMapping("/get") public RspDTO getUser(@RequestParam("userId") @NotNull(message = "用户id不能为空") Long userId) { User user = userService.selectById(userId); if (user == null) { return new RspDTO<User>().nonAbsent("用户不存在"); } return new RspDTO<User>().success(user); }
|
需要加上
1 2 3 4 5 6
| @RestController @RequestMapping("user/") @Validated public class UserController extends AbstractController { ········· }
|
说了一大通基础的东西,但是感觉其实挺全面了,各种用法都介绍了,不足就是 但是没有很深入,如异常的日志的记录,等等。