作为一名码农,哪天工作不会遇到问题,但总是这边解决,那边忘记,过几天再遇到,再解决,恶性循环呢。摆脱恶性循环的第一步,用烂笔头来弥补自己的记忆。
也曾经使用过SSH框架,在使用SSH框架做后台验证的时候,并没有使用框架里自带的验证,而是纯手写的验证,那些验证呀,很多都是重复的,很是痛苦。后来使用spring cloud的框架,项目组的“恶习”并没有改掉,还是使用旧的验证模式。额,除了无语,更觉得抱歉,不能够说服项目主导人使用便利的验证方式。
写后台验证那么久,预想一下理想的验证应该是什么样子的呢?这也是今天所遇到的问题:
- 如何对一些不必输入字段,只做格式验证;
- 前台传递过来的数据是日期,怎么处理;
- 从mybatis查询出的数据,如果是日期,如何格式化为日期格式,而不去修改xml文件;
- 很懒,如何对一些经常用到的字段,做一个公共验证的方法或类;
- 还是懒,想用注解做验证,减少代码量,更减少和业务代码的耦合;
初次使用Spring Boot里面的验证,还需要先研究一下。Spring Boot里面都有什么验证呢?Spring Boot支持JSR-303验证规范,JSR是Java Specification Requests的缩写。JSR-303是Bean Validation 1.0 (JSR 303),说白了就是基于bean的验证,更多的解释参考JCP的官网。在默认情况下,Spring Boot会引入Hibernate Validator机制来支持JSR-303验证规范。
基于JSR-303的注解有哪些,上张图,以便日后查看。更多还需参考网址:https://www.ibm.com/developerworks/cn/java/j-lo-jsr303/index.html
Bean Validation 中的 constraint
表 1. Bean Validation 中内置的 constraint
Constraint | 详细信息 |
---|---|
@Null | 被注释的元素必须为 null |
@NotNull | 被注释的元素必须不为 null |
@AssertTrue | 被注释的元素必须为 true |
@AssertFalse | 被注释的元素必须为 false |
@Min(value) | 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 |
@Max(value) | 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 |
@DecimalMin(value) | 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 |
@DecimalMax(value) | 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 |
@Size(max, min) | 被注释的元素的大小必须在指定的范围内 |
@Digits (integer, fraction) | 被注释的元素必须是一个数字,其值必须在可接受的范围内 |
@Past | 被注释的元素必须是一个过去的日期 |
@Future | 被注释的元素必须是一个将来的日期 |
@Pattern(value) | 被注释的元素必须符合指定的正则表达式 |
表 2. Hibernate Validator 附加的 constraint
Constraint | 详细信息 |
---|---|
被注释的元素必须是电子邮箱地址 | |
@Length | 被注释的字符串的大小必须在指定的范围内 |
@NotEmpty | 被注释的字符串的必须非空 |
@Range | 被注释的元素必须在合适的范围内 |
JSR-303是基于Bean的验证,那就是需要在Bean上加注解喽,本次使用的Spring Boot版本是2.1.4.RELEASE,为什么强调版本,版本不一样,有些实现细节就存在差异。下面上代码。
第一步,在bean上增加注解,进行验证;
/**
* 实体类
* @author 程就人生
*
*/
public class Test {
private String userUid;
//用户名不为空,使用默认提示
@NotNull
private String userName;
//密码进行长度和格式的验证,个性化提示
@Size(min=6, max=15,message="密码长度必须在 6 ~ 15 字符之间!")
@Pattern(regexp="^[a-zA-Z0-9|_]+$",message="密码必须由字母、数字、下划线组成!")
private String userPwd;
//手机号码也用个性化提示,使用正则表达式进行匹配,非空时不验证
@Pattern(regexp="^1(3|4|5|7|8)\\d{9}$",message="手机号码格式错误!")
private String userMobile;
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
@DateTimeFormat(pattern="yyyy-MM-dd")
private Date userBirthday;
private Byte status;
private Date updateDate;
private String updateUser;
private Date createDate;
private String createUser;
public String getUserUid() {
return userUid;
}
public void setUserUid(String userUid) {
this.userUid = userUid;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getUserPwd() {
return userPwd;
}
public void setUserPwd(String userPwd) {
this.userPwd = userPwd;
}
public String getUserMobile() {
return userMobile;
}
public void setUserMobile(String userMobile) {
this.userMobile = userMobile;
}
public Byte getStatus() {
return status;
}
public void setStatus(Byte status) {
this.status = status;
}
public Date getUpdateDate() {
return updateDate;
}
public void setUpdateDate(Date updateDate) {
this.updateDate = updateDate;
}
public String getUpdateUser() {
return updateUser;
}
public void setUpdateUser(String updateUser) {
this.updateUser = updateUser;
}
public Date getCreateDate() {
return createDate;
}
public void setCreateDate(Date createDate) {
this.createDate = createDate;
}
public String getCreateUser() {
return createUser;
}
public void setCreateUser(String createUser) {
this.createUser = createUser;
}
public Date getUserBirthday() {
return userBirthday;
}
public void setUserBirthday(Date userBirthday) {
this.userBirthday = userBirthday;
}
}
说明:注解@Size是限定字段长度的,@Pattern是匹配正则表达式的,@DateTimeFormat是用来转换前台传递过来的日期,前台传递过来的日期必须是yyyy-MM-dd格式的字符串,后台才能正确接收,这几个参数都没有做非空验证,所以允许为null。
用mybatis从数据库查询出来的日期格式的数据是long型,如:1558504462000,想把它转换成年月日的形式,就用注解@JsonFormat,转换出来的时间总是少一天,后面加上timezone = "GMT+8"就可以了。
第二步,在Controller上绑定验证
import java.util.HashMap;
import java.util.Map;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import com.example.demo.bean.Test;
/**
* 测试验证
* @author 程就人生
*
*/
@RestController
public class ValidatorTestController {
/**
* 使用 @Validated 开启对象验证
* @param test
* @return
*/
@PostMapping("/validator")
public Object validatorObject(@Validated Test test, BindingResult br){
Map<String,Object> errorMap = new HashMap<String,Object>();
if(br.hasErrors()){
//对错误集合进行遍历,有的话,直接放入map集合中
br.getFieldErrors().forEach(p->{
errorMap.put(p.getField(), p.getDefaultMessage());
});
}
//返回错误信息
return errorMap;
}
}
说明:BindingResult必须紧跟在@Validated的后面,特别是有多个的时候,要一对一对的排列,不能乱了顺序。
乱了顺序,验证失败时,只会在后台抛异常,而在controller方法里获取步到。
第三步,进行测试,先输入非法的数据,在输入合法的数据,测试结果OK
如果想自定义验证方法,不希望在Bean里面加注解,怎么做呢?
第一步,自定义验证类,实现Validator 接口
import org.springframework.util.StringUtils;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
/**
* 自定义验证类
* @author 程就人生
*
*/
public class TestValidator implements Validator{
@Override
public boolean supports(Class<?> clazz) {
//对需要验证的类进行绑定
return Test.class.equals(clazz);
}
@Override
public void validate(Object target, Errors errors) {
if(target == null){
//TODO
return;
}
// 把校验信息注册到Error的实现类里,两种写法
// ValidationUtils.rejectIfEmpty(errors,"userMobile",null,"手机号码不能为空!!");
// 对对象进行强转
Test test = (Test) target;
// 手机号码的验证,不为空时的一些验证
if(StringUtils.isEmpty(test.getUserMobile())){
errors.rejectValue("userMobile", null, "手机号不能为空!");
}
//其他自定义验证
}
}
说明:重写接口里的两个方法,先对需要进行验证的实体进行绑定,这个类实现了Validator接口,重写接口里的validate,自定义验证方法。
第二步,在Controller添加initBinder方法进行绑定,其他不变
/**
* 验证处理,initBinder方法在参数转换之前执行(转换规则,格式化)
* @param webDataBinder
*/
@InitBinder
public void initBinder(WebDataBinder webDataBinder) {
webDataBinder.addValidators(new TestValidator());
}
说明:initBinder方法是参数转换之前执行,在执行具体的controller方法前。
第三步,进行测试,不输入手机号,测试结果OK
还有问题,在实体类上写正则表达式的时候,比如说手机号码的验证,可能有好几个类都需要进行手机号码格式的验证,每个类都写一次,也是很繁琐的,有没有更简单更公共的的方法呢?
当然有,使用注解,根据JSR-303规范,一个 constraint 通常由 annotation 和相应的 constraint validator 组成,一个annotation可以对那个多个constraint validator。先不管这么多,写一个验证手机号的@Mobile试一试吧。
第一步,编写注解类Mobile
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.CONSTRUCTOR;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.TYPE_USE;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
/**
* 验证手机号码的注解类
* @author 程就人生
* @date 2019年5月22日
* @Description
*
*/
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy=MobileValidator.class) //对应的验证实现类
public @interface Mobile {
//默认提示
String message() default "手机号码格式错误!";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
第二步,实现MobileValidator类
import java.util.regex.Pattern;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import com.alibaba.druid.util.StringUtils;
/**
* 验证手机号码的实现类
* @author 程就人生
* @date 2019年5月22日
* @Description
*
*/
public class MobileValidator implements ConstraintValidator<Mobile, String> {
//验证手机的正则表达式
private String mobileReg = "^1(3|4|5|7|8)\\d{9}$";
private Pattern mobilePattern = Pattern.compile(mobileReg);
public void initialize(Mobile mobile) {
}
public boolean isValid(String value, ConstraintValidatorContext arg1) {
//为空时,不进行验证
if (StringUtils.isEmpty(value))
return true;
//返回匹配结果
return mobilePattern.matcher(value).matches();
}
}
第三步,将Test类上验证手机号的正则表达式换成@Mobile注解
//手机号码也用个性化提示,使用正则表达式进行匹配,非空时不验证
//@Pattern(regexp="^1(3|4|5|7|8)\\d{9}$",message="手机号码格式错误!")
@Mobile
private String userMobile;
第四步,进行测试,输入非法的手机号,测试结果OK
总结,这里面使用的注解和一些方法都是来自org.springframework.validation.annotation的架包,又一次感觉到了Spring组件的强大。