SpringBoot之数据校验
数据校验是什么
数据校验就是数据合法性检查
示例
- 用户名不能为空
- 密码长度不够
- 两次密码不一样
- 邮箱格式不正确
- 手机格式不正确
...
JSR是什么
JSR是Java Specification Requests的缩写,意思是Java 规范提案。关于数据校验这块,最新的是JSR380。
JSR-380是 J2EE 的一个规范,用于校验实体属性,它是JSR-303的升级版,在 Spring Boot 中可以基于它优雅实现参数校验。
为什么要使用
在没有使用JSR-380之前,我们一般都会将参数校验硬编码在controller类中
拼式校验
Order.java
public class User {
private String username;
private String password
private String matchPassword;
}
Controller
@PostMapping("/add")
public String add(@RequestBody User user) {
if(user.getUsername()==null)
return "用户名不能为空";
if(user.getPassword()==null)
return "密码不能为空";
if(user.getMatchPassword()==null)
return "确认密码不能为空";
return "sucess";
}
从这个简单的方法入参校验至少能发现如下问题:
1、需要写大量的代码来进行参数验证。(这种代码多了就算垃圾代码)
2、需要通过注释来知道每个入参的约束是什么(否则别人咋看得懂)
3、每个程序员做参数验证的方式不一样,参数验证不通过抛出的异常也不一样(后期几乎没法维护)
如上会导致代码冗余和一些管理的问题(代码量越大,管理起来维护起来就越困难),比如说语义的一致性等。为了避免这样的情况发生,最好是将验证逻辑与相应的域模型(领域模型的概念)进行绑定,JavaEE提供的思路
Bean Validation
Bean Validation简介
最新的是JSR380,也就是Bean Validation 2.0。
为了解决上述问题,Bean Validation 为 JavaBean 验证定义了相应的元数据模型和 API。默认的元数据是各种Java Annotations,当然也支持xml方式并且你也可以扩展
Bean Validation的主页:http://beanvalidation.org
Bean Validation的参考实现:https://github.com/hibernate/hibernate-validator
Bean Validation是一个通过配置注解来验证参数的框架,它包含两部分Bean Validation API(规范)和Hibernate Validator(实现)。
Bean Validation是Java定义的一套基于注解/xml的数据校验规范,目前已经从JSR 303的1.0版本升级到JSR 349的1.1版本,再到JSR 380的2.0版本,已经经历了三个版本。最新版本Jakarta Bean Validation 3.0也在2020的10 月发布了 。
Bean Validation 2.0的新特性
因为2.0推出的时间确实不算长,so此处我把一些重要的关注点列举如下:
1、对Java的最低版本要求是Java 8
2、支持容器的校验,通过TYPE_USE类型的注解实现对容器内容的约束:List<@Email String>
3、支持日期/时间的校验,@Past和@Future
4、拓展元数据(新增注解):@Email,@NotEmpty,@NotBlank,@Positive, @PositiveOrZero,@Negative,@NegativeOrZero,@PastOrPresent和@FutureOrPresent
像@Email、@NotEmpty、@NotBlank之前是Hibernate额外提供的,2.0标准后hibernate自动退位让贤并且标注为过期了
5、Bean Validation 2.0的唯一实现为Hibernate Validator
6、对于Hibernate Validator,它自己也扩展了一些注解支持
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>8.0.1.Final</version>
</dependency>
常用校验注解
注解 | 描述 |
---|---|
@NotNull | 验证值不为 null |
@AssertTrue | 验证值为 true |
@Size | 验证值的长度介于 min 和 max 之间, 可应用于 String、Collection、Map 和数组类型 |
@Min | 验证值不小于该值 |
@Max | 验证值不大于该值 |
验证字符串是有效的电子邮件地址 | |
@NotEmpty | 验证值不为 null 或空,可应用于 String、Collection、Map 和数组类型 |
@NotBlank | 验证字符串不为 null 并且不是空白字符 |
@Positive | 验证数字为正数 |
@PositiveOrZero | 验证数字为正数(包括 0) |
@Negative | 验证数字为负数 |
@NegativeOrZero | 验证数字为负数(包括 0) |
@Past | 验证日期值是过去 |
@PastOrPresent | 验证日期值是过去(包括现在) |
@Future | 验证日期值是未来 |
@FutureOrPresent | 验证日期值是未来(包括现在) |
Spring Boot支持
Spring Boot支持JSR-380验证框架,默认实现是Hibernate Validator,项目中只要在Java Bean上放一些校验注解,就可以实现校验支持,杜绝通篇 if else 参数判断,而且这个校验是支持group的概念的,对于不同的group生效的校验不一样。这个很有用,因为对于增删改查等不同的操作,需要执行的校验本来就是不一样的。
在boot中可以使用spring-boot-starter-validation
传递依赖了hibernate-validation
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
JSR-380体验
老版本
老版本需要在Controller中判断
public String register(@RequestBody User user){
if(StringUtils.isBlank(user.getName())){
return Result.error("用户名不能为空");
}
// ...
}
而使用JSR-380只需要通过添加对应的注解即可实现校验
@Data
public class User{
@NotBlank
private String username;
private String password;
}
public String register(@Valid @RequestBody User user){
// ...
}
这样看起来代码是不是清爽了很多,只需要在需要校验的字段上加上对应的校验注解,然后对需要校验的地方加上@Valid注解,然后框架就会帮我们完成校验。
注意:记得要在验证POJO类前加上@Valid注解噢
JSR方式
完善 User
@Data
public class User{
@NotBlank
@Size(min = 4,max = 50,message = "用户名长度必须在4~50之间")
private String username;
@NotBlank
@Size(min = 8,max = 20,message = "密码长度必须在8~20之间")
private String password;
@NotBlank
@Size(min = 8,max = 20,message = "确认密码长度必须在8~20之间")
private String matchPassword;
//@Email(message = "邮箱格式不正确")
@Pattern(regexp = "^[a-zA-Z0-9_.-]+@[a-zA-Z0-9-]+(\\.[a-zA-Z0-9-]+)*\.[a-zA-Z0-9]{2,6}$",message = "邮箱格式不正确")
private String email;
@Min(value = 18,message = "最小年龄为18")
@Max(value = 58,message = "最大年龄为58")
private Integer age;
}
测试json数据
{
"username":"admin",
"password":12345678,
"matchPassword":12345678,
"email":"aa@aa.com",
"name":"admin",
"age":18
}
异常处理
@ExceptionHandler(BindException.class)
public AjaxResult handleIBindException(BindException ex) {
Map<String, String> errorMap = new HashMap<>();
// 处理字段级错误(FieldError)
for (FieldError fieldError : ex.getFieldErrors()) {
errorMap.put(fieldError.getField(), fieldError.getDefaultMessage());
}
// 处理类级错误(ObjectError),避免重复添加字段级错误
for (ObjectError objectError : ex.getGlobalErrors()) {
if (!(objectError instanceof FieldError)) {
errorMap.put(objectError.getObjectName(), objectError.getDefaultMessage());
}
}
// 返回包含错误属性名和消息的 AjaxResult
return AjaxResult.error("参数绑定错误", errorMap);
}
问题思考
1、新增不需要有id,修改必须要有id,对于这样的POJO类怎么校验呢?到底是加哪个注解
解决方案:
- 使用两个不同POJO类,新增使用UserAddDTO.java,修改使用UserEditDTO.java
- 使用分组校验
2、确认密码没有对应的注解,还有其它业务型的判断,怎么校验呢?比如两次密码一致性、数据字典校验等。
解决方案:
使用自定义校验注解来完成自己的业务
分组校验
先看@Validated/@Valid注解区别
1、提供商
@Valid使用的是jakarta包。
@Validated使用的是springframework包。
说明:
java的JSR303声明了@Valid这类接口,而Hibernate-validator对其进行了实现
@Validation对@Valid进行了二次封装,在使用上并没有区别,但在分组、注解位置、嵌套验证等功能上有所不同,这里主要就这几种情况进行说明。
2、注解位置不一样
@Validated:用在类型、方法和方法参数上。但不能用于成员属性(field)
@Valid:可以用在方法、构造函数、方法参数和成员属性(field)上
3、支持分组校验
@Validated:提供分组功能,可以在参数验证时,根据不同的分组采用不同的验证机制
@Valid:没有分组功能
分组校验场景
主要是用于对参数校验的一个分组,我们在对数据做不同操作的时候,比如更新的时候主键字段不能为空,新增的进修主键字段必须为空。
分组校验示例
AddGroup.java
public interface AddGroup {
}
UpdateGroup.java
public interface UpdateGroup {
}
修改User.java,加上id属性,每个校验规则后加上对应的组
@Data
@ConfirmPwd(groups = {AddGroup.class,UpdateGroup.class})
public class User {
@NotNull(message = "用户ID不能为空",groups = {UpdateGroup.class})
@Null(message = "用户ID必须为null",groups = {AddGroup.class})
private Integer id;
@NotBlank(message = "用户名不能为空",groups = {AddGroup.class,UpdateGroup.class})
private String username;
@NotBlank(message = "密码不能为空",groups = {AddGroup.class,UpdateGroup.class})
@Size(min = 8,max = 20,message = "密码长度必须在8~20之间")
private String password;
@Size(min = 8,max = 20,message = "确认密码长度必须在8~20之间",groups = {AddGroup.class,AddGroup.class,UpdateGroup.class})
private String matchPassword;
//@Email(message = "邮箱格式不正确")
@Pattern(regexp = "^[a-zA-Z0-9_.-]+@[a-zA-Z0-9-]+(\\.[a-zA-Z0-9-]+)*\.[a-zA-Z0-9]{2,6}$",message = "邮箱格式不正确",groups = {AddGroup.class,AddGroup.class,UpdateGroup.class})
private String email;
@Min(value = 18,message = "最小年龄为18",groups = {AddGroup.class,AddGroup.class,UpdateGroup.class})
@Max(value = 58,message = "最大年龄为58",groups = {AddGroup.class,AddGroup.class,UpdateGroup.class})
private Integer age;
注意:如果校验时需要分组,声明时并没有指定分组,则对应校验规则不生效
BrandController.java
@RestController
@RequestMapping("/user")
public class UserController {
@PostMapping
public AjaxResult add(@Validated({AddGroup.class}) @RequestBody User user) {
return AjaxResult.success(user);
}
@PutMapping
public AjaxResult update(@Validated({UpdateGroup.class}) @RequestBody User user) {
return AjaxResult.success(user);
}
}
测试json
{
"id": "1001",
"username":"张三",
"password":12345678,
"matchPassword":12345671,
"email":"aa@aa.com",
"name":"admin",
"age":18
}
自定义校验器
当需要根据自己业务实现的一系列的校验,因此可以使用自定义校验注解进行校验。使用时,只需要在需要添加校验的字段上加上自定义的注解即可。例如:数据字典校验等。
参数说明
1、@Target():注解的作用目标,可选项有
- TYPE(接口、类、枚举、注解)
-
FILELD(字段、枚举的常量)
-
METHOD(方法)
-
PARAMETER(方法参数)
-
CONSTRUCTOR(构造函数)
-
LOCAL_VARIABLE(局部变量)
-
ANNOTATION_TYPE(注解)
- PACKAGE(包)
2、@Retention(): 注解保留级别
- RetentionPolicy.SOURCE:只在源代码级别保留,编译时就会被忽略,在class字节码文件中不包含。
- RetentionPolicy.CLASS:编译时被保留,默认的保留策略,在class文件中存在,但JVM将会忽略,运行时无法获得。
- RetentionPolicy.RUNTIME:被JVM保留,所以他们能在运行时被JVM或其他使用反射机制的代码所读取和使用。
3、@Document:说明该注解将被包含在javadoc中
4、@Constraint(validateBy = {MobileValidator.class}):javax.validation 下的注解类,用来指定自定义校验器
5、required()被校验值是否可以为空
6、message()校验不通过时的返回信息
7、groups()用于分组校验
8、payload()数据载荷
邮箱校验器【练手】
ValidEmail.java
@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = EmailValidator.class)
@Documented
public @interface ValidEmail {
String message() default "邮箱格式不正确!!!";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
EmailValidator.java
public class EmailValidator implements ConstraintValidator<ValidEmail,String> {
public final String EMAIL_PATTERN="^[a-zA-Z0-9_.-]+@[a-zA-Z0-9-]+(\\.[a-zA-Z0-9-]+)*\.[a-zA-Z0-9]{2,6}$";
@Override
public void initialize(ValidEmail constraintAnnotation) { }
@Override
public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
return EMAIL_PATTERN.matches(s);
}
}
说明:
1、实现ConstraintValidator接口,ConstraintValidator接口是泛型接口
public interface ConstraintValidator<A extends Annotation, T>
此处 A 代表此校验器支持的注解类,T 代表需要被校验的值的类型(此处数据字典的值为String),因此这里是
ConstraintValidator<ValidEmail,String>
2、实现ConstraintValidator接口的两个方法
public void initialize(ValidEmail constraintAnnotation)
public boolean isValid(String value, ConstraintValidatorContext context)
第一个是初始化方法,第二个是实现校验的逻辑,这里使用校验工具类进行校验
使用
@ValidEmail(message = "邮箱格式又不正确了",groups = {AddGroup.class,AddGroup.class,UpdateGroup.class})
private String email;
密码的高级验证器
pom.xml
<dependency>
<groupId>org.passay</groupId>
<artifactId>passay</artifactId>
<version>1.6.2</version>
</dependency>
ValidPassword.java
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PasswordConstraintValidator.class)
@Target({ElementType.TYPE, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
public @interface ValidPassword {
String message() default "Invalid Password";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
PasswordConstraintValidator.java
public class PasswordConstraintValidator implements ConstraintValidator<ValidPassword, String> {
@Override
public void initialize(ValidPassword constraintAnnotation) {}
@Override
public boolean isValid(String password, ConstraintValidatorContext constraintValidatorContext) {
val validator = new PasswordValidator(Arrays.asList(
//密码长度为8到30位
new LengthRule(8, 30),
//至少有一个英文的大写字母
new CharacterRule(EnglishCharacterData.UpperCase, 1),
//至少有一个英文的小写字母
new CharacterRule(EnglishCharacterData.LowerCase, 1),
//至少有一个英文的特殊字符
new CharacterRule(EnglishCharacterData.Special, 1),
//不允许有5个连续的英文字母
new IllegalSequenceRule(EnglishSequenceData.Alphabetical, 5, false),
//不允许有5个连续的数字
new IllegalSequenceRule(EnglishSequenceData.Numerical, 5, false),
//不允许有5个键盘连续的字母
new IllegalSequenceRule(EnglishSequenceData.USQwerty, 5, false),
//需要有空格
new WhitespaceRule()
));
val result = validator.validate(new PasswordData(password));
return result.isValid();
}
}
使用
@NotBlank
@Size(min = 8,max = 20,message = "密码长度必须在8~20之间")
@ValidPassword
private String password;
两次密码一致性校验器
ConfirmPwd.java
@Documented
@Target({ElementType.FIELD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {ConfirmPwdValidator.class})
public @interface ConfirmPwd {
String message() default "两次输入的密码不一致";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
ConfirmPwdValidator.java
public class ConfirmPwdValidator implements ConstraintValidator<ConfirmPwd, User> {
@Override
public void initialize(ConfirmPwd constraintAnnotation) {
ConstraintValidator.super.initialize(constraintAnnotation);
}
@Override
public boolean isValid(User user, ConstraintValidatorContext constraintValidatorContext) {
return user.getPassword().equalsIgnoreCase(user.getMatchPassword());
}
}
使用
@Size(min = 8,max = 20,message = "确认密码长度必须在8~20之间",groups = {AddGroup.class,AddGroup.class,UpdateGroup.class})
@ConfirmPwd(groups = {AddGroup.class,UpdateGroup.class})
private String matchPassword;
数据字典校验器
DictValue.java
@Documented
@Constraint(validatedBy = { DictValueConstraintValidator.class })
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface DictValue {
// 这里可以直接写出错误字符串,下面的写法是从配置中读取
String message() default "{net.wanho.wanlimall.common.valid.DictValue.message}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
int[] vals() default { };
}
properties
配置文件的名称是固定的=>ValidationMessages
ValidationMessages.properties
net.wanho.wanlimall.common.valid.DictValue.message=字典数据的值不正确
数据字典校验器类
public class DictValueConstraintValidator implements ConstraintValidator<DictValue,Integer> {
private Set<Integer> set = new HashSet<>();
/**
* 初始化方法
* @param constraintAnnotation
*/
@Override
public void initialize(DictValue constraintAnnotation) {
int[] vals = constraintAnnotation.vals();
for (int val : vals) {
set.add(val);
}
}
/**
* 判断是否效验成功
* @param value 需要效验的值
* @param context
* @return
*/
@Override
public boolean isValid(Integer value, ConstraintValidatorContext context) {
//判断是否有包含的值
boolean contains = set.contains(value);
return contains;
}
}
使用
@CustomValid(message = "字典的值不对", groups = {AddGroup.class}, handler = DictValid.class ,vals = {0,1})
private Integer showStatus;
校验框架封装
CustomValid.java
@Documented
@Target({ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
// 自定义校验处理器
@Constraint(validatedBy = CustomValid.CustomValidHandler.class)
public @interface CustomValid {
// 校验提示信息
String message() default "校验未通过";
// 设置校验分组信息
Class<?>[] groups() default {};
// 设置校验的负载 - 原数据
Class<? extends Payload>[] payload() default {};
Class<? extends WanhoValid> handler();
int[] vals() default { };
class CustomValidHandler implements HibernateConstraintValidator<CustomValid, Object> {
private CustomValid customValid;
@Override
public void initialize(CustomValid customValid) {
this.customValid=customValid;
}
@Override
public boolean isValid(Object value, ConstraintValidatorContext constraintValidatorContext) {
if(value!=null){
Class<? extends WanhoValid> handlerClass = customValid.handler();
// 获取自定义的验证器
WanhoValid wanhoValid = SpringUtils.getBean(handlerClass);
if(wanhoValid==null) return true;
// 返回验证的结果
return wanhoValid.isValid(customValid,value);
}
return true;
}
}
}
WanhoValid.java
public interface WanhoValid {
public boolean isValid(CustomValid customValid, Object value);
}
DictValid.java
@Component
public class DictValid implements WanhoValid {
@Override
public boolean isValid(CustomValid customValid, Object value) {
int[] vals = customValid.vals();
Set<Integer> set = Arrays.stream(vals).boxed().collect(Collectors.toSet());
return set.contains(value);
}
}
Brand.java
public class Brand implements Serializable {
@CustomValid(message = "字典的值不对", groups = {AddGroup.class}, handler = DictValid.class ,vals = {0,1})
private Integer showStatus;
}
还没有人赞赏,快来当第一个赞赏的人吧!
- 2¥
- 5¥
- 10¥
- 20¥
- 50¥
声明:本文为原创文章,版权归信息岛所有,欢迎分享本文,转载请保留出处!