Spring boot validation 后端数据检验

Spring Boot后端数据校验全解析

数据校验

spring boot版本:3.5.3

场景

现在有一个用户注册接口,控制器方法参数就是用户对象:

@Slf4j
@RestController
@RequestMapping("user")
public class UserController {

    @PostMapping("register")
    public Result<String> register(@RequestBody User user){


        log.info("插入数据库~");
        return Result.success();
    }
}

用户对象:

@Data
public class User {

    private String username;

    private String password;

    private String mobile;

    private String email;
    
}

此时没有任何校验,数据会直接写入数据库。接下来引入数据校验功能,校验user对象中的各个字段。

1.引入依赖:

spring boot将数据校验场景抽取到单独的starter中:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

这个依赖中引入了以下两个包:

  1. hibernate-validator-8.0.2.Final.jar,hibernate对JSR303规范的扩展实现
  2. jakarta.validation-api-3.0.2.jar,JSR303规范

2.在参数实体类的字段上,标注合适的注解

至于可以标注哪些注解,可以参考上面的两个包,示例:
常用的如:@Max@Min@NotNull@NotBlank等。

常用的如:@Length

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
import org.hibernate.validator.constraints.Length;

@Data
public class User {

    @NotEmpty(message = "用户名不可为空!")
    private String username;

    private String password;

    @Length(min = 11, max = 11)
    private String mobile;

    @Email
    private String email;

}

3.开启参数校验

在控制器方法参数前,添加注解:@Vaild 或者 @Validated

@Slf4j
@RestController
@RequestMapping("user")
public class UserController {

    @PostMapping("register")
    public Result<String> register(@Valid @RequestBody User user){

        log.info("插入数据库~");
        return Result.success();
    }
}

4.测试

Postman发送请求
不填任何参数:

填入正确的参数:

5.当参数未通过校验时,提示详细信息

方式一:BindingResult

在每一个需要校验的控制器方法参数后面,加一个BindingResult参数,这样的话,如果这个参数未通过校验,校验结果会封装到后面的BindingResult中。
如果控制器方法有多个需要校验的参数,那么每个参数之后都要加一个BindingResult。

@Slf4j
@RestController
@RequestMapping("user")
public class UserController {

    @PostMapping("register")
    public Result<String> register(@Valid @RequestBody User user, BindingResult bindingResult){
        
        log.info("插入数据库~");
        return Result.success();
    }
}

注意:当使用BindingResult后,必须手动处理异常!因为当参数未通过校验时,spring boot将不会抛出异常,而是将校验结果封装到BindingResult中。如果不手动处理封装结果,参数校验将形同虚设。

如果有多个控制器方法,每个控制器方法有多个需要校验的参数,就会很麻烦,还容易遗漏,因此,这种方式并不推荐。

手动处理BindingResult

@Slf4j
@RestController
@RequestMapping("user")
public class UserController {

    @PostMapping("register")
    public Result<String> register(@Valid @RequestBody User user, BindingResult bindingResult){
        // 判断是否有参数未通过校验
        if(bindingResult.hasErrors()){
            StringBuilder errorMessage = new StringBuilder();
            // 获取所有未通过校验的参数
            List<FieldError> fieldErrors = bindingResult.getFieldErrors();
            for (FieldError fieldError : fieldErrors) {
                String field = fieldError.getField();
                String defaultMessage = fieldError.getDefaultMessage();
                errorMessage.append(defaultMessage);
            }

            return Result.fail(errorMessage.toString());
        }

        log.info("插入数据库~");
        return Result.success();
    }
}

方式二:全局异常处理

当参数未通过校验时,会抛出:org.springframework.web.bind.MethodArgumentNotValidException

MethodArgumentNotValidException继承自BindException,而BindException又实现了BindingResult。

那么就可以加一个全局异常处理器,来处理这个异常。

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(exception = MethodArgumentNotValidException.class)
    public Result<String> handleValid(MethodArgumentNotValidException methodArgumentNotValidException){
        BindingResult bindingResult = methodArgumentNotValidException.getBindingResult();

        // 获取所有未通过校验的参数
        List<FieldError> fieldErrors = bindingResult.getFieldErrors();
        Map<String,String> map = new HashMap<>();
        for (FieldError fieldError : fieldErrors) {
            String field = fieldError.getField();
            String defaultMessage = fieldError.getDefaultMessage();
            map.put(field,defaultMessage);
        }

        return Result.fail(map.toString());
    }
}

6.进阶-自定义注解

现在要对密码做校验,密码必须包含大小写字母、数字和特殊字符,长度8-20位。类似的比较复杂的校验,就需要我们自定义校验注解。

校验密码的注解

其中@Constraint(validatedBy = {PasswordValidator.class}),指定具体校验逻辑的实现类。
不知道怎么写校验注解,可以参考原有的注解,比如@NotNull。

@Documented
@Constraint(validatedBy = {PasswordValidator.class})
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidPassword {
    String message() default "密码必须包含大小写字母、数字和特殊字符,长度8-20位";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

校验逻辑的实现类

校验类要实现jakarta.validation.ConstraintValidator接口。
第一个泛型是自定义的校验注解,第二个泛型是要校验的参数的类型。

public class PasswordValidator implements ConstraintValidator<ValidPassword, String> {
    // 密码规则:至少1个大写字母、1个小写字母、1个数字、1个特殊字符,长度8-20
    private static final String PASSWORD_PATTERN =
            "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{8,20}$";

    @Override
    public boolean isValid(String password, ConstraintValidatorContext context) {
        return Pattern.compile(PASSWORD_PATTERN).matcher(password).matches();
    }
}

使用自定义注解

这里password属性上标注了两个注解,@NotBlank对密码做非空字符串校验,@ValidPassword校验密码的格式,两个校验分开。

但是测试发现,两个校验注解的生效顺序是随机的,当密码为空字符串时,有时提示密码不可为空,有时提示格式错误:

提示密码不可为空:

提示格式错误:

@Data
public class User {

    @NotEmpty(message = "用户名不可为空!")
    private String username;

    @NotBlank(message = "密码不可为空!")
    @ValidPassword
    private String password;

    @Length(min = 11, max = 11)
    private String mobile;

    @Email
    private String email;

}

改进校验器

密码是空值时,则跳过,让@NotBlank校验。

public class PasswordValidator implements ConstraintValidator<ValidPassword, String> {
    // 密码规则:至少1个大写字母、1个小写字母、1个数字、1个特殊字符,长度8-20
    private static final String PASSWORD_PATTERN =
            "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{8,20}$";

    @Override
    public boolean isValid(String password, ConstraintValidatorContext context) {
        if(!StringUtils.hasText(password)){
            // 密码是空值时,则跳过,让@NotBlank校验
            return true;
        }
        return Pattern.compile(PASSWORD_PATTERN).matcher(password).matches();
    }
}

这样当密码为空字符串时,就可以稳定提示密码为空了。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值