1 引言
平时在业务开发过程中 controller 层的参数校验有时会存在下面这样的判断
public String add(UserVO userVO) {
if(userVO.getAge() == null){
return "年龄不能为空";
}
if(userVO.getAge() > 120){
return "年龄不能超过120";
}
if(userVO.getName().isEmpty()){
return "用户名不能为空";
}
// 省略一堆参数校验...
return "OK";
}
业务代码还没开始写呢,光参数校验就写了一堆判断。这样写虽然没什么错,但是给人的感觉就是:不优雅,不专业。
其实java给我们提供了一组规范:JSR-303,spring又实现并扩展了这个规范,让我们能够优雅无侵入的实现参数的校验
2 JSR-303 规范
2.1 JSR:Java 规范提案
JSR即为Java Specification Requests的缩写,是Java 规范提案,常常定义一组规则,有的规则有默认的实现,有的则需要用户自己去实现。
2.2 JSR-303:Bean Validation规范
JSR-303 是JAVA EE 6 中的一项子规范,叫做Bean Validation,为Bean验证定义了元数据模型和API,默认的元数据模型是通过Annotations注解来描述的,支持扩展。
官方参考的默认实现是Hibernate Validator,当然用户也可以自己扩展,比如常用的Spring JSR-303
注意:这只是一组规范,对应到代码就是一组接口,不能直接使用
这组规范在javax所提供的jar:jakarta.validation-api-xxx.jar
中,要想使用,则需要导入并实现这组规范
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<version>2.0.2</version>
</dependency>
2.3 JSR-303 基本的约束原理
一个 constraint
通常由 annotation
和相应的 constraint validator
组成,它们是一对多的关系。
也就是说可以有多个 constraint validator 对应一个 annotation。
在运行时,Bean Validation 框架本身会根据被注释元素的类型来选择合适的 constraint validator 对数据进行验证。
- constraint:一个约束
- annotation:注解
- constraint validator:约束校验器(可有多个)
有些时候,在用户的应用中需要一些更复杂的 constraint。Bean Validation 提供扩展 constraint 的机制,可以通过两种方法去实现。
- 一种是组合现有的 constraint 来生成一个更复杂的 constraint
- 另外一种是开发一个全新的 constraint。
2.4 JSR-303 中内置的约束
内置的约束注解如下图:
3 JSR-303规范的实现
3.1 Hibernate Validator
这是官方参考的默认实现,Hibernate JSR-303导入并实现了
jakarta.validation-api
,并对JSR-303默认的约束做了扩展,注意此实现与 Hibernate ORM 没有任何关系。
3.1.1 Hibernate Validator 附加的 constraint
Constraint | 详细信息 |
---|---|
被注释的元素必须是电子邮箱地址 | |
@Length | 被注释的字符串的大小必须在指定的范围内 |
@NotEmpty | 被注释的字符串的必须非空 |
@Range | 被注释的元素必须在合适的范围内 |
3.1.2 导入依赖
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-el</artifactId>
<version>9.0.63</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.2.3.Final</version>
<scope>compile</scope>
</dependency>
3.1.3 简单使用
package myValid;
import lombok.Data;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotNull;
import java.util.Iterator;
import java.util.Set;
@Data
public class User {
@NotNull(message = "email不可以为空")
@Email(message = "email不合法")
private String email;
public static void main(String[] args) {
final User user = new User();
user.setEmail("xxxxxx.com");
//调用JSR303验证工具,校验参数
Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
Set<ConstraintViolation<User>> violations = validator.validate(user);
Iterator<ConstraintViolation<User>> iter = violations.iterator();
if (iter.hasNext()) {
final ConstraintViolation<User> violation = iter.next();
// email:email不合法
System.out.println(violation.getPropertyPath() + ":" + violation.getMessage());
// TODO 可以进行指定约束异常的抛出
}
}
}
3.2 Spring JSR-303
3.2.1 官方说明
JSR-303的javax.validation的变体。有效,支持验证组的规范。为方便使用Spring的JSR-303支持而设计,但不是JSR-303特有的。
可以与Spring MVC处理程序方法参数一起使用。通过org.springframework.validation.SmartValidator的验证提示概念支持,验证组类充当提示对象。
也可以与方法级验证一起使用,表示特定类应该在方法级进行验证(充当相应验证拦截器的切入点),但也可以在带注释的类中为方法级验证指定验证组。在方法级别应用此注释允许覆盖特定方法的验证组,但不作为切入点;然而,类级注释对于触发特定bean的方法验证是必要的。也可以用作自定义原型注释或自定义组特定的验证注释上的元注释。
3.2.2 通俗解释
- 实现了JSR-303标准
- 支持分组验证(特有)
- 可以与Spring MVC收参的方法一起使用
4 Spring Validator的使用
4.1 导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
点进入发现Spring Validator
包含了Hibernate Validator
,所以,spring环境下,我们直接使用Spring Validator
会非常非常方便
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.7.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-el</artifactId>
<version>9.0.63</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.2.3.Final</version>
<scope>compile</scope>
</dependency>
</dependencies>
4.2 常用的约束说明
验证注解 | 验证的数据类型 | 说明 |
---|---|---|
@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注解即可级联验证 |
4.3 重点:使用技巧
一般情况下,我们都是校验方法上面的形参,既然是形参,就分两种情况:
- 一种是校验形参本身,这种情况需要在当前类上面添加
@Validated
注解,同时使用@NotNull
等注解对该形参进行校验 - 另一种则是形参内部属性的校验,这种情况可以在当前类上面添加
@Validated
注解,也可以不加,但是一定要在该形参上添加@Validated
注解 - 未通过校验时会抛出
org.springframework.web.bind.MethodArgumentNotValidException
、org.springframework.validation.BindException
等异常,我们捕获统一返回即可
下面进行举例说明,比如说我们有一个DocumentTransController
@RestController
@RequestMapping("/documentTrans")
public class DocumentTransController {
private final DocumentTransService documentTransService;
public DocumentTransController(DocumentTransService documentTransService) {
this.documentTransService = documentTransService;
}
/**
* 接收翻译请求(接收文件)
* @return
*/
@RequestMapping(value = "/documentTransByFile", method = RequestMethod.POST)
public String transDocumentByFile(MultipartFile file,DocumentInfoBo documentInfoBo) {
return documentTransService.documentTrans(file,documentInfoBo);
}
}
@Data
public class DocumentInfoBo {
/**
* 所属用户id
*/
@NotNull(message = "用户id不能为空")
private Long userId;
private UserInfo userInfo;
@Data
static class UserInfo {
@NotNull(message = "用户名不能为空")
private String username;
private String phone;
}
}
4.3.1 校验方法形参本身
我现在想要校验transDocumentByFile()
的file不能为空,则需要在DocumentTransController
类上添加@Validated
注解,同时在(MultipartFile file)
前面添加@NotNull
注解,如下:
@RestController
@RequestMapping("/documentTrans")
@Validated
public class DocumentTransController {
private final DocumentTransService documentTransService;
public DocumentTransController(DocumentTransService documentTransService) {
this.documentTransService = documentTransService;
}
/**
* 接收翻译请求(接收文件)
* @return
*/
@RequestMapping(value = "/documentTransByFile", method = RequestMethod.POST)
public String transDocumentByFile(@NotNull MultipartFile file, DocumentInfoBo documentInfoBo) {
return documentTransService.documentTrans(file,documentInfoBo);
}
}
4.3.2 校验方法形参内部属性
在4.3.1的基础上,我现在想要校验DocumentInfoBo
对象内的userId
属性不能为空,则需要在(DocumentInfoBo documentInfoBo)
前面添加@Validated
注解
@RestController
@RequestMapping("/documentTrans")
@Validated
public class DocumentTransController {
private final DocumentTransService documentTransService;
public DocumentTransController(DocumentTransService documentTransService) {
this.documentTransService = documentTransService;
}
/**
* 接收翻译请求(接收文件)
* @return
*/
@RequestMapping(value = "/documentTransByFile", method = RequestMethod.POST)
public String transDocumentByFile(@NotNull MultipartFile file,@Validated DocumentInfoBo documentInfoBo) {
return documentTransService.documentTrans(file,documentInfoBo);
}
}
4.3.3 嵌套验证
现在我想在4.3.1和4.3.2基础上,添加对userInfo属性的校验,则需要在userInfo上面添加 @Valid @NotNull(message = "userInfo不能为空")
即可
@Data
public class DocumentInfoBo {
/**
* 所属用户id
*/
@NotNull(message = "用户id不能为空")
private Long userId;
@Valid
@NotNull(message = "userInfo不能为空")
private UserInfo userInfo;
@Data
static class UserInfo {
@NotNull(message = "用户名不能为空")
private String username;
private String phone;
}
}
4.3.4 分组参数校验
第一步、定义一个校验组类,声明四个接口对应不同场景校验
public class ValidationGroups {
public interface Select {
}
public interface Insert {
}
public interface Update {
}
public interface Detail {
}
public interface Delete{
}
}
第二步、在实体类具体属性添加校验规则及校验分组
@Data
public class Person {
@NotNull(message = "personId不能为null",groups = Select.class)
private Integer personId;
@NotEmpty(message = "name不能为空",groups = Insert.class)
private String name;
private Integer age;
}
第三步、在控制层创建查询和新增接口,添加@Validated注解,指定分组,可多个。
@GetMapping("/select")
public Object select(@Validated({ValidationGroups.Select.class}) Person person) {
return person;
}
@GetMapping("/insert")
public Object insert(@Validated(ValidationGroups.Insert.class) Person person) {
return person;
}
4.4 重点:注意事项
Spring Validator不光可以用在Controller层,同样也可以作用于service、或者其他bean
4.5 重点:@Validated和@Valid的区别
很多时候@Validated和@Valid的界限分的并没有那么清楚,很多场景@Validated和@Valid效果也等同,那么他们到底有什么区别呢?什么时候该用哪个注解呢?
在检验Controller的入参是否符合规范时,使用@Validated或者@Valid在基本验证功能上没有太多区别。但是在分组、注解地方、嵌套验证等功能上两个有所不同:
@Validated | @Valid | |
---|---|---|
分组 | 提供分组功能,可在入参验证时,根据不同的分组采用不同的验证机制。 | 无分组功能 |
可注解位置 | 可以用在类型、方法和方法参数上。但是不能用在成员属性上 | 可以用在方法、构造函数、方法参数和成员属性上(两者是否能用于成员属性上直接影响能否提供嵌套验证的功能) |
嵌套验证 | 用在方法入参上无法单独提供嵌套验证功能。不能用在成员属性上。也无法提供框架进行嵌套验证。能配合嵌套验证注解@Valid进行嵌套验证。 | 用在方法入参上无法单独提供嵌套验证功能。能够用在成员属性上,提示验证框架进行嵌套验证。能配合嵌套验证注解@Valid进行嵌套验证。 |
@Valid
被注释的元素是一个对象,需要检查此对象的所有字段值@Validated
被注解的元素是一个对象或者一个类,需要检查此对象的所有字段值
使用建议:常规情况使用@Validated
注解,嵌套验证时使用@Valid
注解
4.6 校验异常统一处理示例
异常对应:
- 使用form data方式调用接口,校验异常抛出 BindException
- 使用 json 请求体调用接口,校验异常抛出 MethodArgumentNotValidException
- 单个参数校验异常抛出ConstraintViolationException
@RestControllerAdvice
public class ExceptionHandle {
private final Logger logger = LoggerFactory.getLogger(getClass());
@ExceptionHandler(value = Exception.class)
public JSONResult<Object> handle(Exception e) {
logger.error(e.getMessage(), e);
if (e instanceof MyException) {
MyException myException = (MyException) e;
return JSONResult.error(myException.getCode(),myException.getMessage());
} else {
return JSONResult.error(ResultEnum.UNKNOWN_ERROR.getCode(), e.getMessage());
}
}
/**
* 方法参数校验
* 由于spring捕获异常的问题,导致此异常的编码格式无法修改为UTF-8,
* 故不使用RestControlle的ResponseBody,自己实现httpServletResponse的IO。
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public JSONResult<Object> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
final BindingResult bindingResult = e.getBindingResult();
List<FieldError> fieldErrors = bindingResult.getFieldErrors();
String msg = ResultEnum.VALIDATION_ERROR.getMsg();
if(!fieldErrors.isEmpty()){
// 返回错误信息
msg = fieldErrors.get(0).getField()+":"+fieldErrors.get(0).getDefaultMessage();
}
logger.error(e.getMessage(), e);
return JSONResult.error(ResultEnum.VALIDATION_ERROR.getCode(), msg.isEmpty()?e.getCause().getMessage():msg);
}
/**
* 捕获校验异常
* @param e
* @return
*/
@ExceptionHandler(ValidationException.class)
public JSONResult<Object> handleValidationException(ValidationException e) {
logger.error(e.getMessage(), e);
return JSONResult.error(ResultEnum.VALIDATION_ERROR.getCode(), e.getCause().getMessage());
}
/**
* 捕获校验绑定异常
* @param e
* @return
*/
@ExceptionHandler(BindException.class)
public JSONResult<Object> handleBindException(BindException e) {
logger.error(e.getMessage(), e);
List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
String msg = "";
if(!fieldErrors.isEmpty()){
// 返回错误信息
msg = fieldErrors.get(0).getDefaultMessage();
}
return JSONResult.error(ResultEnum.VALIDATION_ERROR.getCode(), msg.isEmpty()?e.getCause().getMessage():msg);
}
/**
* 捕获违反约束异常
* @param e
* @return
*/
@ExceptionHandler(ConstraintViolationException.class)
public JSONResult<Object> handConstraintViolationException(ConstraintViolationException e) {
final Iterator<ConstraintViolation<?>> iterator = e.getConstraintViolations().iterator();
while (iterator.hasNext()) {
final ConstraintViolation<?> constraintViolation = iterator.next();
final String errorMsg = constraintViolation.getMessage();
logger.error(e.getMessage(), e);
return JSONResult.error(ResultEnum.VALIDATION_ERROR.getCode(), errorMsg);
}
return JSONResult.error(ResultEnum.VALIDATION_ERROR.getCode(), e.getMessage());
}
5 扩展:注解失效的原因排查
1、是否忘记导入依赖
在2.3.0版本之前spring-boot-starter-web是默认集成validation依赖的,但是在2.3.0开始就去掉了该依赖,所以需要自己添加。
2、如果要校验对象本身,则需要在所在类上面添加@Validated注解
3、涉及嵌套验证,是否忘记添加@Valid注解
6 使用ConstraintValidator自定义注解和校验器
https://blog.csdn.net/weixin_43702146/article/details/125657418