欢迎来到信息岛!
adminAdmin  2024-11-05 18:10 信息岛 显示边栏 |   抢沙发  6 
文章评分 0 次,平均分 0.0

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 验证值不大于该值
@Email 验证字符串是有效的电子邮件地址
@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;

}
「点点赞赏,手留余香」

还没有人赞赏,快来当第一个赞赏的人吧!

admin给Admin打赏
×
予人玫瑰,手有余香
  • 2
  • 5
  • 10
  • 20
  • 50
2
支付

声明:本文为原创文章,版权归所有,欢迎分享本文,转载请保留出处!

admin
Admin 关注:0    粉丝:0
这个人很懒,什么都没写

发表评论

表情 格式 链接 私密 签到
扫一扫二维码分享