1 引言
1.1 场景
最近的这个项目是互联网项目,网络用户需要填写表单信息保存提交。页面输入信息需要进行数据格式校验,从而避免无效数据被保存或者提交。这些检查工作包括必填项检查、数值检查、长度检查、身份证号码、手机号码检查等工作。如果将这些字段校验和业务逻辑混合一起写,则会干扰原有逻辑,而且不容易维护。下面即将要介绍的是后端api对表单数据的验证处理技术。
1.2 名词或技术介绍
(1) JSR
JSR:Java Specification Requests的缩写,意思是Java 规范提案。是指向JCP(Java Community Process)提出新增一个标准化技术规范的正式请求。任何人都可以提交JSR,以向Java平台增添新的API和服务。JSR已成为Java界的一个重要标准。
Bean Validation 是一个运行时的数据验证框架,在验证之后验证的错误信息会被马上返回。BeanValidation就是这个规范。
提到JSR,相信有小伙伴想去看下到底是个啥。可以看到规范从JSR 303 到 JSR 380,目前最新规范是Bean Validation 2.0。这是地址:
https://jcp.org/en/jsr/summary?id=bean+validation
(2) javax.validation.api
Java 在2009年的 JAVAEE 6 中发布了 JSR303以及javax下的validation包内容。
这项工作的主要目标是为java应用程序开发人员提供 基于java对象的 约束(constraints)声明 和 对约束的验证工具(validator),以及约束元数据存储库和查询API,以及默认实现。
Java8开始,Java EE改名为Jakarta EE,注意javax.validation相关的api在jakarta.validation的包下。所以大家看不同的版本的时候,会发现以前的版本包在javax.validation包下。javase的支持还在jcp,Java EE改名JakartaEE,JakartaEE的官网及其支持的项目:
https://jakarta.ee/
Bean Validation 2.0规范及默认实现的地址:
https://beanvalidation.org/2.0/spec/#whatsnew
(3) hibernate-validator
Hibernate-Validator框架 提供了 JSR 303 规范中所有内置 constraint 的实现,除此之外还有一些附加的 constraint。注意:此处的Hibernate 不是 Hibernate ORM没有任何关系,hibernate-validator是Hibernate 基金会下的项目之一。
hibernate-validator的介绍地址:
https://docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single/
源码地址:
https://github.com/hibernate/hibernate-validator
2 常用注解
下面列出常用约束,每个约束都有参数 message,groups 和 payload。这是 Bean Validation 规范的要求。
请注意,这里没有区分哪些是默认注解,哪些是hibernate-validator的注解,因为我们实际开发中,肯定会同时用两个包,大家在可以直接看得到使用注解属于哪个包。
(1)2个标识注解
@Valid(规范、常用)
标记用于验证级联的属性、方法参数或方法返回类型。
在验证属性、方法参数或方法返回类型时,将验证在对象及其属性上定义的约束。
此行为是递归应用的。
@Validated(spring)
spring 提供的扩展注解,可以方便的用于分组校验
其中,message 是提示消息,groups 可以根据情况来分组。
(2)22个约束注解
以下每一个注解都可以在相同元素上定义多个。
@AssertFalse
检查元素是否为 false,支持数据类型:boolean、Boolean
@AssertTrue
检查元素是否为 true,支持数据类型:boolean、Boolean
@DecimalMax(value=, inclusive=)
inclusive:boolean,默认 true,表示是否包含,是否等于
value:当 inclusive=false 时,检查带注解的值是否小于指定的最大值。当 inclusive=true 检查该值是否小于或等于指定的最大值。参数值是根据 bigdecimal 字符串表示的最大值。
支持数据类型:BigDecimal、BigInteger、CharSequence、(byte、short、int、long 和其封装类)
@DecimalMin(value=, inclusive=)
支持数据类型:BigDecimal、BigInteger、CharSequence、(byte、short、int、long 和其封装类)
inclusive:boolean,默认 true,表示是否包含,是否等于
value:
当 inclusive=false 时,检查带注解的值是否大于指定的最大值。当 inclusive=true 检查该值是否大于或等于指定的最大值。参数值是根据 bigdecimal 字符串表示的最小值。
@Digits(integer=, fraction=)
检查值是否为最多包含 integer 位整数和 fraction 位小数的数字
支持的数据类型:
BigDecimal, BigInteger, CharSequence, byte, short, int, long 、原生类型的封装类、任何 Number 子类。
@Email
检查指定的字符序列是否为有效的电子邮件地址。可选参数 regexp 和 flags 允许指定电子邮件必须匹配的附加正则表达式(包括正则表达式标志)。
支持的数据类型:CharSequence
@Max(value=)
检查值是否小于或等于指定的最大值
支持的数据类型:
BigDecimal, BigInteger, byte, short, int, long, 原生类型的封装类, CharSequence 的任意子类(字符序列表示的数字), Number 的任意子类, javax.money.MonetaryAmount 的任意子类
@Min(value=)
检查值是否大于或等于指定的最大值
支持的数据类型:
BigDecimal, BigInteger, byte, short, int, long, 原生类型的封装类, CharSequence 的任意子类(字符序列表示的数字), Number 的任意子类, javax.money.MonetaryAmount 的任意子类
@NotBlank
检查字符序列是否为空,以及去空格后的长度是否大于 0。与 @NotEmpty 的不同之处在于,此约束只能应用于字符序列,并且忽略尾随空格。
支持数据类型:CharSequence
@NotNull
检查值是否不为 null
支持数据类型:任何类型
@NotEmpty
检查元素是否为 null 或 空
支持数据类型:CharSequence, Collection, Map, arrays
@Size(min=, max=)
检查元素个数是否在 min(含)和 max(含)之间
支持数据类型:CharSequence,Collection,Map, arrays
@Negative
检查元素是否严格为负数。零值被认为无效。
支持数据类型:
BigDecimal, BigInteger, byte, short, int, long, 原生类型的封装类, CharSequence 的任意子类(字符序列表示的数字), Number 的任意子类, javax.money.MonetaryAmount 的任意子类
@NegativeOrZero
检查元素是否为负或零。
支持数据类型:
BigDecimal, BigInteger, byte, short, int, long, 原生类型的封装类, CharSequence 的任意子类(字符序列表示的数字), Number 的任意子类, javax.money.MonetaryAmount 的任意子类
@Positive
检查元素是否严格为正。零值被视为无效。
支持数据类型:
BigDecimal, BigInteger, byte, short, int, long, 原生类型的封装类, CharSequence 的任意子类(字符序列表示的数字), Number 的任意子类, javax.money.MonetaryAmount 的任意子类
@PositiveOrZero
检查元素是否为正或零。
支持数据类型:
BigDecimal, BigInteger, byte, short, int, long, 原生类型的封装类, CharSequence 的任意子类(字符序列表示的数字), Number 的任意子类, javax.money.MonetaryAmount 的任意子类
@Null
检查值是否为 null
支持数据类型:任何类型
@Future
检查日期是否在未来
支持的数据类型:
java.util.Date, java.util.Calendar, java.time.Instant, java.time.LocalDate, java.time.LocalDateTime, java.time.LocalTime, java.time.MonthDay, java.time.OffsetDateTime, java.time.OffsetTime, java.time.Year, java.time.YearMonth, java.time.ZonedDateTime, java.time.chrono.HijrahDate, java.time.chrono.JapaneseDate, java.time.chrono.MinguoDate, java.time.chrono.ThaiBuddhistDate
如果 Joda Time API 在类路径中,ReadablePartial 和ReadableInstant 的任何实现类
@FutureOrPresent
检查日期是现在或将来
支持数据类型:同@Future
@Past
检查日期是否在过去
支持数据类型:同@Future
@PastOrPresent
检查日期是否在过去或现在
支持数据类型:同@Future
@Pattern(regex=, flags=)
根据给定的 flag 匹配,检查字符串是否与正则表达式 regex 匹配
支持数据类型:CharSequence
3 代码实战
接下来在springboot项目中,结合springAOP和AspectJ,实现通过注解的方式进行数据格式验证。
3.1 Jar包引入
需要引入jakata.validation-api,以及hibernate-validator的jar包,但是springboot项目中,spring-boot-starter-web 包中已经引入了 hibernate-validator 6.0.17.RELEASE了。
3.2 对需要验证的字段进行约束
级联验证用valid,复杂验证用正则表达式
给一个正则表达式的地址:https://www.w3cschool.cn/zhengzebiaodashi/regexp-metachar.html
package com.galen.demo.validator.domain;
import org.hibernate.validator.constraints.Length;
//javax.validation.constraints包下所支持的一些约束类型
import javax.validation.Valid;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Past;
import javax.validation.constraints.Pattern;
import java.util.Date;
/**
* 员工信息
* @author galen
* @date 2020/2/26
* */
public class Employee {
@NotNull(message = "姓名必填!")
@Length(max = 20, message = "姓名过长!")
private String name;
@NotNull(message = "工牌必填!")
@Pattern(regexp = "^\\d{10}",message = "请输入10位数字工牌!")//长度10,0-9
private String badgeCode;
@Pattern(regexp = "^[1-2]",message = "性别参数错误!")
@NotNull(message = "性别必填!")
private String gender;
@Past(message = "无效的出生日期!")
private Date birthDate;
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
@Email
private String email;
@NotNull(message = "身份证号码必填!")
@Pattern(regexp = "^[1-9]\\d{5}(18|19|([23]\\d))\\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\\d{3}[0-9Xx]$"
,message = "身份证号码格式错误")
private String idCardNumber;
@Valid
private Salary salary;
public Salary getSalary() {
return salary;
}
public void setSalary(Salary salary) {
this.salary = salary;
}
public String getPhoneNumber() {
return phoneNumber;
}
public void setPhoneNumber(String phoneNumber) {
this.phoneNumber = phoneNumber;
}
@NotNull(message = "联系电话必填!")
@Pattern(regexp = "^[1]+\\d{10}$"
,message = "电话号码格式错误")
private String phoneNumber;
public String getIdCardNumber() {
return idCardNumber;
}
public void setIdCardNumber(String idCardNumber) {
this.idCardNumber = idCardNumber;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getBadgeCode() {
return badgeCode;
}
public void setBadgeCode(String badgeCode) {
this.badgeCode = badgeCode;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
public Date getBirthDate() {
return birthDate;
}
public void setBirthDate(Date birthDate) {
this.birthDate = birthDate;
}
@Override
public String toString() {
return "Employee{" +
"name='" + name + '\'' +
", badgeCode='" + badgeCode + '\'' +
", gender=" + gender +
", birthDate=" + birthDate +
'}';
}
}
3.3 添加自定义注解
package com.galen.demo.validator.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author galen
* @date 2020/2/26
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BeanValidation {
String handler() default "default";
}
3.4 定义自定义注解的处理类
/**
* 定义环绕通知,在指定注解处(切点)拦截,获取切点对象参数,以及获取BeanValidation的自定义参数
* */
@Around("@annotation(beanValidation)")
public final Object validateParamByAnnotation(ProceedingJoinPoint ponit, BeanValidation beanValidation) throws Throwable {
log.info("========================== start bean validation in :" + ponit.getSignature().getDeclaringTypeName() + "==========================");
String errorMsg = packErrorMsg(ponit.getArgs());
if (errorMsg != null && errorMsg.length() > 0) {
String handlerName = beanValidation.handler();
if ("default".equals(handlerName)) {
return R.error(errorMsg);
}
return customHandlerInvoke(handlerName, errorMsg);
}
return ponit.proceed();
}
3.5 在需要验证的方法上加注解
package com.galen.demo.validator.controller;
import com.galen.demo.validator.annotation.BeanValidation;
import com.galen.demo.validator.domain.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.*;
import java.util.List;
import java.util.Set;
/**
* 用户信息接口
* @author galen
* @date 2020/2/26
* */
@RequestMapping(path = "/employee")
@RestController
public class EmployController {
private static String lineSeparator = System.lineSeparator();
/**
* 注解实现
* 使用@Valid 注解 实体, 并传入参数bindResult以获取校验结果信息
* @param employee
* @param bindingResult
* @return
*/
@PostMapping("/bindingResult")
public Object addEmployee(@RequestBody @Valid Employee employee, BindingResult bindingResult){
if (bindingResult.hasErrors()){
//校验结果以集合的形式返回,当然也可以获取单个。具体可以查看bindResult的API文档
List<FieldError> fieldErrorList = bindingResult.getFieldErrors();
//StringBuilder组装异常信息
StringBuilder builder = new StringBuilder();
//遍历拼装
fieldErrorList.forEach(error -> {
builder.append(error.getDefaultMessage() + lineSeparator);
});
builder.insert(0,"use @Valid n BingdingResult :" +lineSeparator);
return builder.toString();
}
//TODO there can invoke service layer method to do someting
return "添加职员信息成功:" + employee.toString();
}
//Spring boot 已帮我们把 validation 的关键对象的实例装载如 IOC 容器中
@Autowired
private ValidatorFactory autowiredValidatorFactory;
@Autowired
private Validator autowiredValidator;
/**
* 调用validator实现
* @param employee
* @return
*/
@PostMapping("/validator")
public Object addEmployee(@RequestBody Employee employee){
System.out.println("这里将导入 由 Springboot 的 IOC 容器中获取的 校验器工厂和 校验器类");
System.out.println("validator工厂类:"+ autowiredValidatorFactory.toString());
System.out.println("validator类:"+ autowiredValidator.toString());
/**
* 下述的工厂类和校验器类也可以使用上述由IOC容器中获取的对象实例代替
*/
//实例化一个 validator工厂
ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
//获取validator实例
Validator validator = validatorFactory.getValidator();
//调用调用,得到校验结果信息 Set
Set<ConstraintViolation<Employee>> constraintViolationSet = validator.validate(employee);
//StringBuilder组装异常信息
StringBuilder builder = new StringBuilder();
//遍历拼装
constraintViolationSet.forEach(violationInfo -> {
builder.append(violationInfo.getMessage() + lineSeparator);
});
if (builder.toString().length() > 0){
builder.insert(0,"use validator :" +lineSeparator);
return builder.toString();
}
return "添加职员信息成功:" + employee.toString();
}
/**
* 调用BeanValidation 切面实现
* @param employee
* @return
*/
@PostMapping("/validateByAspect")
@BeanValidation
public Object addEmployeeByAspect(@RequestBody Employee employee1){
return "添加职员信息成功" ;
}
}
3.6 效果演示
最后附上源码地址和参考地址
本文实战源码地址:https://gitee.com/muziye/validator
相关参考地址:
正则表达式:https://www.w3cschool.cn/zhengzebiaodashi/regexp-metachar.html
身份证正则:https://blog.csdn.net/loa_loa/article/details/81737086
表单验证通用切面:https://blog.csdn.net/weixin_43740223/article/details/100889250
验证拓展:自定义验证+分组验证:
https://www.cnblogs.com/beiyan/p/5946345.html
https://blog.csdn.net/Mr_rain/article/details/78247857
对hibernate注解进一步说明
https://blog.csdn.net/zalan01408980/article/details/79059434
https://www.cnblogs.com/lw5946/p/11574987.html
https://beanvalidation.org/
https://github.com/hibernate/hibernate-validator