概述
惯例交待下背景,为保证系统的稳定可靠运行,必须对输入的数据进行严格验证,防止一些非法的异常数据引发系统后续处理流程出错甚至崩溃。同时,对于验证失败的情况,需要输出明确的、友好的错误信息,降低系统的运维工作量。
今天要说的数据验证,特指使用MVC架构的web系统,技术栈是SSM,即对于从前端传到控制器层(SpringMVC)的请求参数或对象,进行的数据校验工作。前端的数据校验,由前端技术方案解决,不在讨论范围内。后端带业务逻辑的数据验证,如账号是否已存在,是服务层service该干的活,同样不在讨论范围内。
很明显,这是一个常见常用的典型场景,就不要重复造轮子了,应该考虑优先选择成熟稳定的功能组件。
数据校验是有规范的,JSR303是标准。JSR-349是其升级版本,添加了一些新特性,规定一些校验注解,位于javax.validation.constraints包下。注意,规范仅仅是规范,实现还得具体的功能组件来。
目前主流的功能验证组件是hibernate validator,需要注意的是,别一提hibernate就想到ORM框架,这里只是一个数据验证的功能组件。
这个功能组件可以直接使用,不过,贴心的Spring,在其基础上进行了二次开发与封装,称之为spring validation,整合进了整个Spring体系。
弄清楚JSR303/JSR-349,hibernate validation,spring validation之间的关系,那么,很明显,我们应该选择spring validation。
常用注解
以下注解均来自于javax.validation.constraints包。
非空验证
这是最常见的需求,即验证某个属性不能为空,如用户账号、部门名称等,对应的注解有三个,@NotNull 、@NotEmpty和@NotBlank。
这三个略有差异,@NotNull是验证不能为null,适用于任何类型;@NotEmpty 进了一步,不仅不能为null,也不能为空串,既适用于字符串,也适用于集合类对象;@NotBlank更进一步,不能为非空白字符(空白字符包括空格、回车、换行、tab等),只适用于字符串。
与之对应的有三个功能相反的注解,@Null 、@Empty和 @Blank,从数据验证的角度,适用的地方会极其苛刻,只能用在某些特定的场景了,实用性相当低。
范围类验证
@Min和@Max:验证最小值和最大值,含边界,适用于整数类型,如byte、short、int、long及其包装类型,以及BigDecimal和BigInteger,不适用于double和float。
@DecimalMin和@DecimalMax:名字上看上去专用于Decimal数据类型,实际跟上面@Min和@Max的适用范围完全一致。那既然有了@Max,为何还需要@DecimalMax?原因在于,如果某个数字特别大,超出了Long.maxValue(@Max注解value属性的类型是long),这时候,只能使用@DecimalMax。
@Size(min,max) 用于字符串和集合,验证长度或元素数在最小值与最大值范围之间。
@Digits (integer, fraction),两个参数代表整数位数和小数位数最大值,适用于整数类型,如byte、short、int、long及其包装类型,以及BigDecimal和BigInteger,此外也适用于字符串。这里适用于字符串,是要求这个字符串必须能转换为数值,否则验证肯定通不过。
日期类验证
@Past 适用于日期时间类型,要求必须是过去的时间
@Future 适用于日期时间类型,要求必须是将来的时间
与之相关的还有两个,包含当前日期,@PastOrPresent和@FutureOrPresent
某些场景还是能用上。
特定格式验证
@Email 验证电子邮件
正则表达式验证
@Pattern 自己来写正则,实用性可以。
其他验证
还有一些,实用性比较差,在此简单列一下,知道有即可,用的可能性不大。
@Negative 负数
@Positive 正数
@AssertTrue 必须为true,专用于布尔类型,包括boolean和Boolean
@AssertFalse 必须为false,专用于布尔类型,包括boolean和Boolean
如何使用
以下内容从实战角度说明如何来使用,需要注意的问题及坑点,不会面面俱到。
引入依赖
需要注意的是,当前网传的一些文章,说sping-boot-starer-web已经内置引用了Hibernate validator组件,实际是不准确的,在早期的SpringBoot版本,确实是引入了,但较新的SpringBoot版本,默认情况下是未引入的,例如我使用的2.3.0,仍需要引入以下包:
<!--数据验证-->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.18.Final</version>
</dependency>
如果不引入上面这个包,首先校验的注解无法识别,还需要引入下面的包。
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
但如果只引入了validation-api包,不引入hibernate-validator包,项目编译与运行并不会报错,但是数据验证并不会触发与生效。
附加注解
为要进行数据验证的对象的属性添加注解,在我的系统设计中,使用VO(视图对象)作为前后端交互的对象,因此,注解添加在VO对象上,如下图使用的@NotBlank,其中message参数用于指定数据验证失败后的提示信息,下文会用到。
@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
@ApiModel(value = "字典项对象")
public class DictionaryItemVO extends BaseVO {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "字典类型标识")
private String typeId;
@NotBlank(message = "名称不能为空")
@ApiModelProperty(value = "名称")
private String name;
@ApiModelProperty(value = "编码")
@NotBlank(message = "编码不能为空")
private String code;
@ApiModelProperty(value = "状态")
private String status;
@ApiModelProperty(value = "排序号")
private String orderNo;
}
进行验证
对前后端交互,使用SpringMVC的情况下,后端接收参数的时候,需要附加@Validated注解来修饰指定参数。
/**
* 新增
*/
@ApiOperation(value = "新增")
@PostMapping("/")
@SystemLog(value = "组织机构-新增")
@PreAuthorize("hasPermission(null,'system:organization:add')")
public ResponseEntity<Result> add(@Validated @RequestBody OrganizationVO vo) {
Organization entity = convert2Entity(vo);
organizationService.add(entity);
OrganizationVO newVO = convert2VO(entity);
return ResultUtil.success(newVO);
}
如上图,对于组织机构新增的方法,增加了注解@Validated,这点很关键,不加该注解,则不会进行实际的数据验证。
加了该注解后,会自动进行数据验证,验证失败抛出MethodArgumentNotValidException的异常,这时候,我们就可以结合全局异常处理,在附加了@RestControllerAdvice的全局类中捕获异常,拿到错误提示信息,并给予友好提示了。
/**
* 全局异常处理类
* @author wqliu
*/
@RestControllerAdvice
@Slf4j
public class ExceptionHandle {
//常见Http异常
/**
* 参数异常 400
*/
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public ResponseEntity<Result> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
String errorMessage = "";
//bean validator验证部分
BindingResult bindingResult = e.getBindingResult();
List<ObjectError> allErrors = bindingResult.getAllErrors();
if (allErrors.size() > 0) {
FieldError fieldError = (FieldError) allErrors.get(0);
errorMessage = fieldError.getDefaultMessage();
}
return logAndGenerateResult(e,HttpStatus.BAD_REQUEST,errorMessage);
}
/**
* 未授权,401
*/
@ExceptionHandler(SessionExpiredException.class)
public ResponseEntity<Result> handleSessionExpiredException(SessionExpiredException e) {
//会话超时为正常现象,虽然使用异常来处理,但不调用log.error
return ResultUtil.error(e.getMessage(),HttpStatus.UNAUTHORIZED);
}
……
}
进阶使用
分组验证
对于同一业务实体,我们可能需要在不同场景下进行不同的数据验证工作,例如,通过组织机构数据维护,表单录入需要验证机构类型,而通过excel批量导入,只需要验证下机构名称不为空即可,机构类型在导入模板中干脆就不提供,让系统管理员导入后在系统中手工调整,这种情况下就用到分组验证。
Spring Validation提供了相应的分组验证方面的支持。
@Data
@EqualsAndHashCode(callSuper = true)
@ApiModel(value = "Organization对象", description = "组织机构")
public class OrganizationVO extends BaseVO {
private static final long serialVersionUID = 1L;
/**
* 表单录入
*/
public interface FormInput{
}
/**
* excel导入
*/
public interface ExcelImport{
}
@ApiModelProperty(value = "父标识")
private String parentId;
@ApiModelProperty(value = "名称")
@NotBlank(message = "名称不能为空",groups = {tech.popsoft.platform.system.vo.OrganizationVO.FormInput.class, tech.popsoft.platform.system.vo.OrganizationVO.ExcelImport.class})
@ExcelProperty("名称")
private String name;
@ApiModelProperty(value = "类型")
@NotBlank(message = "请选择类型",groups = {tech.popsoft.platform.system.vo.OrganizationVO.FormInput.class})
@ExcelIgnore
private String type;
……
}
如上所示,在实体对象内部,以接口的方式,定义分组,然后在验证属性的注解上,如@NotBlank,附加groups属性,指定在分组,可指定多个。
需要注意的是,在controller接收数据的时候,需要在@Validated中指定分组名,如下所示。
public ResponseEntity<Result> add(@Validated(OrganizationVO.FormInput.class) @RequestBody OrganizationVO vo) {
Organization entity = convert2Entity(vo);
organizationService.add(entity);
OrganizationVO newVO = convert2VO(entity);
return ResultUtil.success(newVO);
}
服务层使用
上面说的都是与SpringMVC做了整合的情况下,如果服务层,也想使用数据验证框架,如对Controller层传过来的数据,或Service层之间的调用数据,使用dto对象,同样可以使用。给要验证的对象添加注解跟前面是完全一样的,就是进行验证,需要调用validator对象的方法,进行手工触发。
private void validate(ApiRequest apiRequest) {
Validator validator= Validation.byProvider(HibernateValidator.class).configure()
.failFast(true).buildValidatorFactory().getValidator();
Set<ConstraintViolation<ApiRequest>> set = validator.validate(apiRequest);
if(set.size()>0){
throw new ApiException("S00", set.iterator().next().getMessage());
}
}
以上实际是直接使用HibernateValidator的方式,理论上,使用Spring validation也是可行的,在服务层中通过@Autowired直接注入private Validator validator,然后调用validator.validate方法即可,但这个方法返回是void,通过第二个参数Errors来接收错误,怎么构造出来这个参数,尚未找到资料,留个悬念,日后解决了补充进来。