SpringBoot 中的 Validation
jarkarta bean validation 是javaEE规范之一,是为了使代码更简洁
hibernate-validator bean validation 规范的最佳实现
Spring validation:spring validation对hibernate validation进行了二次封装,在springmvc模块中添加了自动校验,并将校验信息封装进了特定的类中
beanvalidation官网 beanvalidation.org/
hibernate-validator官网: hibernate.org/validator/
1、validation starter
<!--validation依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
2、约束注解
2.1 校验空值
@Null:验证对象是否为 null@NotNull:验证对象是否不为 null@NotEmpty:验证对象不为 null,且长度(数组、集合、字符串等)大于 0@NotBlank:验证字符串不为 null,且去除两端空白字符后长度大于 0
2.2 校验大小
@Size(min=, max=):验证对象(数组、集合、字符串等)长度是否在给定的范围之内@Min(value):验证数值(整数或浮点数)是否大于等于指定的最小值@Max(value):验证数值是否小于等于指定的最大值
2.3 校验布尔值
@AssertTrue:验证 Boolean 对象是否为 true@AssertFalse:验证 Boolean 对象是否为 false
2.4 校验日期和时间
@Past:验证 Date 和 Calendar 对象是否在当前时间之前@Future:验证 Date 和 Calendar 对象是否在当前时间之后@PastOrPresent:验证日期是否是过去或现在的时间@FutureOrPresent:验证日期是否是现在或将来的时间
2.5 正则表达式
@Pattern(regexp=, flags=):验证 String 对象是否符合正则表达式的规则
2.6 Hibernate Validation 拓展
@Length(min=, max=):验证字符串的大小是否在指定的范围内@Range(min=, max=):验证数值是否在合适的范围内@UniqueElements:校验集合中的值是否唯一,依赖于 equals 方法@ScriptAssert:利用脚本进行校验
3、分组校验
- 定义分组,
- 定义校验项时指定归属的分组
- 校验时指定要校验的分组
注意: 没指定的校验项默认属于Default分组,分组可以继承
@Data
public class Category {
@NotNull(groups = Update.class)
private Integer id;//主键ID
@NotEmpty
private String categoryName;//分类名称
@NotEmpty
private String categoryAlias;//分类别名
private Integer createUser;//创建人ID
@JsonFormat(pattern="yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;//创建时间
@JsonFormat(pattern="yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;//更新时间
//如果说某个校验项没有指定分组,默认属于Default分组
//分组之间可以继承, A extends B 那么A中拥有B中所有的校验项
public interface Add extends Default {
}
public interface Update extends Default{
}
}
@PutMapping
public Result update(@RequestBody @Validated(Category.Update.class) Category category){
categoryService.upadate(category);
return Result.success();
}
4、级联校验
即一个对象的私有成员是一个引用类型 需要给那个引用类型加上@Valid注解
UserInfo.java
package com.bean;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import java.time.LocalDateTime;
public class UserInfo {
private Long id;
private String name;
private Integer age;
@Valid
private Grade grade;
}
Grade.java
public class Grade {
@Min(60)
@Max(100)
private Integer Chinese;
@Min(60)
@Max(100)
private Integer English;
@Min(60)
@Max(100)
private Integer Math;
}
5、校验简单参数
要在类上加@Validated
@RequestMapping("/notice")
@RestController
// 必须加上该注解
@Validated
public class UserController {
// 路径变量
@GetMapping("{id}")
public Reponse<NoticeDTO> detail(@PathVariable("id") @Min(1L) Long noticeId) {
// 参数noticeId校验通过,执行后续业务逻辑
return Reponse.ok();
}
// 请求参数
@GetMapping("getByTitle")
public Result getByTitle(@RequestParam("title") @Length(min = 1, max = 20) String title) {
// 参数title校验通过,执行后续业务逻辑
return Result.ok();
}
}
6、自定义注解
在使用Spring Boot的请求参数校验时,有时候标准的校验注解无法满足特定的业务需求,这时可以通过自定义验证注解来定制校验规则。
创建自定义验证注解的一般步骤如下:
1、创建注解:创建一个新的注解,用于标记需要进行自定义验证的参数。
2、创建自定义校验器: 创建一个实现ConstraintValidator接口的自定义校验器类,用于实现具体的校验逻辑。校验器需要重写isValid()方法。
3、应用自定义注解: 在请求参数对象的字段上应用自定义注解。
4、控制器中使用: 在控制器中使用@Valid注解对参数进行验证。
定义注解,并用@Constraint绑定校验器
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
// 指定校验器
@Constraint(validatedBy = UniqueValidator.class)
public @interface Unique {
// 用于自定义验证信息
String message() default "字段存在重复";
// 指定集合中的待校验字段
String[] field();
// 指定分组
Class<?>[] groups() default {};
}
制作校验器
// 实现ConstraintValidator<T, R>接口,T为注解的类型,R为注解的字段类型
public class UniqueValidator implements ConstraintValidator<Unique, Collection<?>> {
private Unique unique;
@Override
public void initialize(Unique constraintAnnotation) {
this.unique = constraintAnnotation;
}
@Override
public boolean isValid(Collection collection, ConstraintValidatorContext constraintValidatorContext) {
if(){
return false //校验后未通过
} else{
return true //校验后通过
}
}
}
6.1 自定义注解验证
用户名唯一校验示例
本案例通过自定义验证注解实现注册功能中用户名唯一的校验。
首先,新建valid包,用于存放自定义注解的相关内容。
然后,在valid包下声明自定义注解UniqueUsername,代码如下:
import jakarta.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 用于验证用户名是否唯一的注解
*/
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface UniqueUsername {
String message() default "用户名已存在";
// 下面两个属性必须添加
Class<?>[] groups() default {}; // 用于分组校验
Class<? extends Payload>[] payload() default {}; // 用于给出验证失败的详细信息
}
接下来,在valid包下声明自定义校验器UniqueUsernameValidator,该类需要实现ConstraintValidator接口并实现isValid方法。需要特别注意,该类前需要添加@Component注解才能生效,@Component注解的作用,将在后续的课程中展开讲解。具体代码如下:
import cn.highedu.boot01.util.DBUtil;
import io.micrometer.common.util.StringUtils;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import org.springframework.stereotype.Component;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
@Component
public class UniqueUsernameValidator implements ConstraintValidator<UniqueUsername, String> {
@Override
public boolean isValid(String username, ConstraintValidatorContext constraintValidatorContext) {
// 用户名为空时不进行校验
if (StringUtils.isBlank(username)) {
return true;
}
//获取数据库连接
try (Connection conn = DBUtil.getConnection()){
//准备插入数据的SQL语句
String sql = "select * from user where username = ?";
//创建执行SQL语句的对象
PreparedStatement ps = conn.prepareStatement(sql);
//替换SQL语句中的?
ps.setString(1, username);
//执行SQL语句
ResultSet rs = ps.executeQuery();
if (rs.next()) { // 如果查询到了数据,说明用户名已存在
return false;
}
} catch (SQLException e) {
e.printStackTrace();
}
// 如果没有查询到数据,说明用户名不存在
return true;
}
}
自定义校验器开发完成后,需要在自定义注解前添加@Constraint注解,绑定自定义检验器:
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = UniqueUsernameValidator.class)
public @interface UniqueUsername {
}
接下来,在User类的username属性前添加该自定义注解:
public class User {
private Integer id;
@NotBlank(message = "用户名不能为空")
@Size(min = 4, max = 20, message = "用户名长度的范围为4~20")
@UniqueUsername
private String username;
控制器中, 通过BindingResult获取报错信息
@RequestMapping("/users/register")
@ResponseBody
public String register(@Valid @RequestBody User user,
BindingResult result) {
if (result.hasErrors()) {
//返回验证错误
return result.getFieldError().getDefaultMessage();
}
}
7、全局异常处理器
用于处理一些接口业务异常,比如用户输入参数校验失败等。
曾经只能用try-catch处理,但现在可以用全局异常处理器处理
可以通过@ControllerAdvice 注解配置一个全局异常处理类,来统一处理 controller 层中的异常(最后从contoller层抛出去的也可以),于此同时 controller 中可以不用再写 try/catch,这使得代码既整洁又便于维护。
@RestControllerAdvice + @ExceptionHandler
@RestControllerAdvice 组合
@ResponseBody
@ControllerAdvice 组合 定义该类为全局异常处理类
@Component
@ExceptionHandler 定义该方法为异常处理方法
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BindingException.class) //选择要处理的异常
public Result handleException(BindingException e){
e.printStackTrace();
log.error("出现了异常! {}", e);
BindingResult bindingResult = e.getBindingResult();
List<String> errors = new ArrayList<>();
bindingResult.getFieldErrors().stream().forEach(item -> {
errors.add(item.getDefaultMessage());
});
String errorMessage = StringUtils.join(errors, '|');
return ResponseResult.errorResult(AppHttpCodeEnum.SYSTEM_ERROR, errorMessage);
}
}
统一异常处理这个功能可以使用两个注解:@RestControllerAdvice 和 @ControllerAdvice。
这两个注解都可以用来定义全局异常处理器,但是它们的使用场景略有不同。@ControllerAdvice 注解通常用于处理传统的 MVC 应用程序中的异常,例如使用 Thymeleaf、JSP 或者 FreeMarker 等视图技术构建的 Web 应用程序。而 @RestControllerAdvice 注解则通常用于处理 RESTful 服务中的异常,并返回 JSON 格式的数据。
此外,@ControllerAdvice 注解中定义的异常处理方法可以返回 ModelAndView 对象,以实现跳转到错误页面的功能,而 @RestControllerAdvice 注解中定义的异常处理方法则通常返回响应体,返回 JSON 格式的数据
@Valid 和 @Validated
这两个注解是校验的入口,作用相似但用法上存在差异。
// 用于类/接口/枚举,方法以及参数
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Validated {
// 校验时启动的分组
Class<?>[] value() default {};
}
// 用于方法,字段,构造函数,参数,以及泛型类型
@Target({ METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
public @interface Valid {
// 未提供其他属性
}
- 作用范围不同:
@Validated无法作用在于字段,@Valid无法作用于类; - 注解中的属性不同:
@Validated中提供了指定校验分组的属性,而@Valid没有这个功能,因为@Valid不能进行分组校验。