日常开发中,我们时常需要提供可靠的 API 接口,此时对于请求的入参就需要校验,以保证最终数据入库的正确性,这就成了必不可少的活。例如说,用户注册时,会校验手机格式的正确性、邮箱格式的正确性、密码非弱密码等。
但是如果使用 if-else 这种代码去校验, 那么需要校验的地方有很多情况下,代码量就会变的十分臃肿,若是一个类请求参数校验字段又多的化,相信各位小伙伴对不会开心,这么干肯定不合适,代码也不优雅,那么如何解决这个问题呢?
答案就是下面要介绍的 validation
validation 技术在Java中运用最早在2009 年,Java 官方提出了 Bean Validation 规范,而后经历了JSR303、JSR349、JSR380 三次标准的更迭,发展到了 2.0 。
Bean Validation 和 我们以前学习过的 JPA 一样,只提供规范,不提供具体的实现。因此实际使用过程,常用的是 hibernate 的校验组件:org.hibernate.hibernate-validator
通常情况下,在javax.validation.constraints 包下,定义了一系列的约束(constraint)注解,一共 22 个注解,快速略过即可。如下:
空和非空检查
@NotBlank
:只能用于字符串不为 null ,并且字符串 .trim() 以后 length 要大于 0 。@NotEmpty
:集合对象的元素不为 0 ,即集合不为空 。@NotNull
:不能为 null 。@Null
:必须为 null 。数值检查
@DecimalMax(value)
:被注释的元素必须是一个数字,其值必须小于等于指定的最大值。@DecimalMin(value)
:被注释的元素必须是一个数字,其值必须大于等于指定的最小值。@Digits(integer, fraction)
:被注释的元素必须是一个数字,其值必须在可接受的范围内。@Positive
:判断正数。@PositiveOrZero
:判断正数或 0 。@Max(value)
:该字段的值只能小于或等于该值。@Min(value)
:该字段的值只能大于或等于该值。@Negative
:判断负数。@NegativeOrZero
:判断负数或 0 。Boolean 值检查
@AssertFalse
:被注释的元素必须为 true 。@AssertTrue
:被注释的元素必须为 false 。长度检查
@Size(max, min)
:检查字段的 size 是否在 min 和 max 之间,可以是字符串、数组、集合、Map 等。日期检查
@Future
:被注释的元素必须是一个将来的日期。@FutureOrPresent
:判断日期是否是将来或现在日期。@Past
:检查该字段的日期是在过去。@PastOrPresent
:判断日期是否是过去或现在日期。其它检查
@Email
:被注释的元素必须是电子邮箱地址。@Pattern(value)
:被注释的元素必须符合指定的正则表达式。Hibernate Validator 附加的约束注解,在org.hibernate.validator.constraints 包下,定义了一系列的约束(constraint)注解。常见的如示。
@Range(min=, max=)
:被注释的元素必须在合适的范围内。@Length(min=, max=)
:被注释的字符串的大小必须在指定的范围内。@URL(protocol=,host=,port=,regexp=,flags=)
:被注释的字符串必须是一个有效的 URL 。@SafeHtml
:判断提交的 HTML 是否安全。例如说,不能包含 javascript 脚本等等。其他的就不一一列举了,有感兴趣的小伙伴可以去源码包看看。
@Valid
和 @Validated
@Valid
注解,是 Bean Validation 所定义,可以添加在普通方法、构造方法、方法参数、方法返回、成员变量上,表示它们需要进行约束校验。
@Validated
注解,是 Spring Validation 锁定义,可以添加在类、方法参数、普通方法上,表示它们需要进行约束校验。同时,@Validated
有 value 属性,支持分组校验。
对于初学者来说,很容易搞混 @Valid
和 @Validated
注解。
① 声明式校验:Spring Validation 仅对 @Validated 注解,实现声明式校验。
② 分组校验:Bean Validation 提供的 @Valid
注解,因为没有分组校验的属性,所以无法提供分组校验。此时,我们只能使用 @Validated
注解。
③ 嵌套校验:相比来说,@Valid
注解的地方,多了【成员变量】。这就导致,如果有嵌套对象的时候,只能使用@Valid
注解。
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.2.1.RELEASEversion>
<relativePath/>
parent>
<modelVersion>4.0.0modelVersion>
<groupId>com.raosgroupId>
<artifactId>validation-demoartifactId>
<version>1.0-SNAPSHOTversion>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<scope>compilescope>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
project>
spring-boot-starter-web 依赖里,已经默认引入 hibernate-validator 依赖,所以本示例使用的是 Hibernate Validator 作为 Bean Validation 的实现框架。
@Data
public class SysUser {
private Long userId;
/**
* 账号
*/
@NotBlank(message = "用户名不能为空")
@Size(min = 6, message = "用户名长度不能小于6个字符")
private String username;
/**
* 密码
*/
@NotEmpty(message = "密码不能为空")
@Size(min = 8, message = "密码长度不能小于8个字符")
private String password;
/**
* 手机号
*/
@NotBlank(message = "手机号不能为空")
@Size(min = 11, max = 11, message = "手机号长度不对")
private String mobile;
}
@RestController
@RequestMapping("/user")
public class SysUserController {
@PostMapping("/add")
public R addUser(@RequestBody @Valid SysUser sysUser) {
System.out.println("走到这里说明校验成功");
System.out.println(sysUser);
return R.ok(R.SUCCESS_MSG);
}
}
public class R extends HashMap<String, Object> {
private static final long serialVersionUID = 1L;
public static final String SUCCESS_MSG = "操作成功!";
public static final String FAIL_MSG = "操作失败!";
public R() {
this.put((String) "code", 0);
}
public static R error() {
return error(500, "未知异常,请联系管理员");
}
public static R error(String msg) {
return error(500, msg);
}
public static R error(int code, String msg) {
R r = new R();
r.put((String) "code", code);
r.put((String) "msg", msg);
return r;
}
public static R ok(String msg) {
R r = new R();
r.put((String) "msg", msg);
return r;
}
public static R ok(Object object) {
R r = new R();
r.put("result", object);
return r;
}
public static R ok(int code, String msg) {
R r = new R();
r.put((String) "code", code);
r.put((String) "msg", msg);
return r;
}
public static R ok(Map<String, Object> map) {
R r = new R();
r.putAll(map);
return r;
}
public static R ok() {
return new R();
}
public R put(String key, Object value) {
super.put(key, value);
return this;
}
}
public class RRException extends RuntimeException {
private static final long serialVersionUID = 1L;
private String msg;
private int code = 500;
public RRException(String msg) {
super(msg);
this.msg = msg;
}
public RRException(String msg, Throwable e) {
super(msg, e);
this.msg = msg;
}
public RRException(String msg, int code) {
super(msg);
this.msg = msg;
this.code = code;
}
public RRException(String msg, int code, Throwable e) {
super(msg, e);
this.msg = msg;
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
}
当访问/user/add
这个post接口时,如果参数不符合Model中定义的话,程序中就回抛出400异常状态码,并提示错误信息,如下所示。
{
"timestamp": "2021-05-20T01:08:28.831+0000",
"status": 400,
"error": "Bad Request",
"errors": [
{
"codes": [ "Size.sysUser.mobile", "Size.mobile", "Size.java.lang.String", "Size" ],
"arguments": [ { "codes": [ "sysUser.mobile", "mobile" ], "arguments": null, "defaultMessage": "mobile", "code": "mobile" },
11,
11
],
"defaultMessage": "手机号长度不对",
"objectName": "sysUser",
"field": "mobile",
"rejectedValue": "155833013",
"bindingFailure": false,
"code": "Size"
}
],
"message": "Validation failed for object='sysUser'. Error count: 1",
"path": "/user/add"
}
虽然 JSR303 和 Hibernate Validtor 已经提供了很多校验注解,但是当面对复杂参数校验时,还是不能满足我们的要求,这时候我们就需要 自定义校验注解。
下面以“List数组中不能含有null元素”为实例自定义校验注解
1、注解定义如示。
@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.FIELD})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = ListNotHaveNullValidatorImpl.class)//此处指定了注解的实现类
public @interface ListNotHaveNull {
/**
* 添加value属性,可以作为校验时的条件,若不需要,可去掉此处定义
*/
int value() default 0;
String message() default "List集合中不能含有null元素";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
/**
* 定义List,为了让Bean的一个属性上可以添加多套规则
*/
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@interface List {
ListNotHaveNull[] value();
}
}
2、编写自定义校验实现类
@Service
public class ListNotHaveNullValidatorImpl implements ConstraintValidator<ListNotHaveNull, List> {
private int value;
@Override
public void initialize(ListNotHaveNull constraintAnnotation) {
//传入value 值,可以在校验中使用
this.value = constraintAnnotation.value();
}
public boolean isValid(List list, ConstraintValidatorContext constraintValidatorContext) {
for (Object object : list) {
if (object == null) {
//如果List集合中含有Null元素,校验失败
return false;
} else if (object instanceof String) {
String value = object.toString();
if (value.trim().length() == 0){
return false;
}
}
}
return true;
}
}
3、model中添加注解:
@Data
public class SysRole {
private Long roleId;
@NotBlank(message = "角色名不能为空")
private String name;
@NotEmpty(message = "资源列表不能为空")
@ListNotHaveNull(message = "List 中不能含有null元素")
@Valid
private List<String> paths;
}
4、编写前端控制器
@PostMapping("/addRole")
public R addRole(@RequestBody @Valid SysRole sysRole) {
System.out.println("走到这里说明校验成功");
System.out.println(sysRole);
return R.ok(R.SUCCESS_MSG);
}
使用方法同 “简单校验”,在在需要校验的Model上面加上@Valid
即可。
public class ValidatorUtils {
private ValidatorUtils() { }
private static Validator validator;
static {
validator = Validation.buildDefaultValidatorFactory().getValidator();
}
/**
* 校验对象
*
* @param object 待校验对象
* @param groups 待校验的组
* @throws RRException 校验不通过,则报RRException异常
*/
public static void validateEntity(Object object, Class<?>... groups) throws RRException {
Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups);
if (!constraintViolations.isEmpty()) {
Iterator<ConstraintViolation<Object>> iterator = constraintViolations.iterator();
StringBuilder msg = new StringBuilder();
while (iterator.hasNext()) {
ConstraintViolation<Object> constraint = iterator.next();
msg.append(constraint.getMessage()).append(',');
}
throw new RRException(msg.toString().substring(0,msg.toString().lastIndexOf(',')));
}
}
}
使用方式,在接收到前端传递的参数后,使用ValidatorUtils.validateEntity(【参数名】);
即可校验,支持分组校验,分组需要定义分组接口。
以上待续。