写点什么

SpringBoot 如何优雅的进行参数校验

  • 2024-03-26
    福建
  • 本文字数:5946 字

    阅读完需:约 20 分钟

一、传统参数校验


虽然往事不堪回首,但还是得回忆一下我们传统参数校验的痛点。

下面是我们传统校验用户名和邮箱是否合法的代码

if (username == null || username.isEmpty()) {    throw new IllegalArgumentException("用户名不能为空");} if (isValidEmail(email)) {    throw new IllegalArgumentException("邮箱格式不正确");} public boolean isValidEmail(String email) {    String emailRegex = "^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$";    Pattern pattern = Pattern.compile(emailRegex);    Matcher matcher = pattern.matcher(email);    return matcher.matches();}
复制代码


这样的代码不仅冗长,而且难以维护,尤其是在多个地方重复使用时,容易出错。

面对上面的痛点,我们就得解放双手,利用框架来完成校验。

它只需要通过简单的注解来定义校验规则,让框架来帮助我们处理校验逻辑,让我们代码变得更加的优雅。


二、几个名词


问题①:JSR 是什么?

JSR(Java Specification Requests) 是一套 JavaBean 参数校验的标准,它定义了很多常用的校验注解。

我们可以直接将这些注解加在我们 JavaBean 的属性上面,这样就可以在需要校验的时候进行校验了,非常方便!


问题②:Bean Validation 是什么?

Bean Validation是一个抽象的框架,它定义了验证规则,而不会涉及具体的业务逻辑


问题③:Hibernate Validator 是什么?

Bean Validation的实现,目前最新版的 Hibernate Validator 6.x 是 Bean Validation 2.0(JSR 380)的参考实现


三、所需依赖


Spring boot 2.3 以前版本,Springbootspring-boot-starter-web默认内置了Hibernate-Validator

这些版本直接引入spring-boot-starter-web即可,后面的版本需要单独引入


在后面的测试中会用到lombokSpringBootwebtest等基础依赖,这里就不一一给出


四、注解及作用




看到这些注解后,大家可能会对【@NotNul@NotEmpty@NotBlank】这三个注解有点不理解,这里稍作解释

  • @NotNull:任何对象的 value 不能为 null。

  • @NotEmpty:集合对象的元素不为 0,即集合不为空,也可以用于字符串不为 null。

  • @NotBlank:只能用于字符串不为 null,并且字符串 trim()以后 length 要大于 0。


五、快速入门


5.1 新增加一个一个User 实体类

@Datapublic class User {    //姓名    @NotBlank(message = "用户名不能为空")  //注解确保姓名不为空    private  String name;     //性别    @NotBlank(message = "性别不能为空")   //注解确保性别不为空    private String sex;     //年龄    @NotNull(message = "年龄不能为空")  //注解确保年龄不为空    @Max(value = 120,message = "年龄不能大于120")  //注解确保年龄必须小于等于120    @Min(value = 18,message = "年龄不能小于18")   //注解确保年龄必须大于等于18    private  Integer age;     //邮箱    @Email(message = "邮箱格式不正确")    //注解确保邮箱格式正确    @NotBlank(message = "邮箱不能为空")    private String email;}
复制代码


上述代码说明:

  • @NotBlank: 此注解确保字符串不为空并且不能为空字符串,且去掉前后空格后的长度必须大于 0。它常用于字符串字段验证。message 属性用于指定提示信息;

  • @NotNull: 此注解确保整数类型不能为 null

  • @Min 和 @Max: 这两个注解用于验证数字值是否在指定的范围内。例如,在上面的示例中,我们想要确保 age 的值在 18 到 120 之间;

  • @Email: 此注解用于验证字符串值是否是有效的电子邮件地址格式。


5.2 Controller层参数校验


下图是controller层校验流程



@RestControllerpublic class ValidatorController {    //测试参数校验    @RequestMapping("/testValidator")    public ResponseEntity<String> testValidator(@Valid @RequestBody User  user, BindingResult bindingResult){         // 是否存在校验错误        if (bindingResult.hasErrors()) {            // 获取校验不通过字段的提示信息            String errorMsg = bindingResult.getFieldErrors()                    .stream()                    .map(FieldError::getDefaultMessage)                    .collect(Collectors.joining(", "));             return ResponseEntity.badRequest().body(errorMsg);        }        return ResponseEntity.ok("参数校验成功");    } }
复制代码


解释一下上面代码:

  • @Validated: 告诉 Spring 需要对 User 对象执行校验; 这个一定不要忘记加上

  • BindingResult : 该类包含校验不通过时的异常信息,校验不通过时,我们通过这个对象来获取注解中 message="xxx"中的内容


注意:当注解校验不通过时,直接将异常信息返回给前端其实并不友好,我们可以将异常包装一下再丢给前端


5.3 测试校验结果


这里我们使用 postman 工具测试一下参数校验是否成功


① 入参正确情况

{    "name":"小凡",    "sex":"男",    "age":18,    "email":"xiezhr@qq.com"}
复制代码



入参不正确的情况

{    "name":null,    "sex":"",    "age":17,    "email":"xiezhrqq.com"}
复制代码



通过上面的入门小案例,你学会了么?

上面的返回结果看起来可能不是那么优雅,那么怎么封装统一返回结果呢,

传送门在此优雅的封装返回结果


六、单个参数校验


上面快速入门中我们说了实体参数校验,这小节,我们来看看单个参数的校验

6.1 controller 层校验代码


@RequestMapping("/testSingleParmaValidator")public ResponseEntity<String> testSingleParmaValidator(@NotBlank(message = "姓名不能为空") String name,                                                       @Min(value = 18,message = "年龄不能小于18")                                                       @Max(value = 120,message = "年龄不能大于120") Integer age                                                       ){     // 参数校验    return ResponseEntity.ok("参数校验成功");}
复制代码


6.2 全局异常捕获


当参数校验不通过会发生如下异常信息



这里我们不能像上面一样通过BindingResult 来获取异常信息,需要添加全局异常捕获校验失败异常,具体代码如下

@RestControllerAdvicepublic class GlobalExceptionHandler {    //处理ValidationException异常    @ExceptionHandler(ValidationException.class)    //返回状态码为400    @ResponseStatus(HttpStatus.BAD_REQUEST)    public ResponseEntity<String> handleValidationExceptions(ValidationException  ex) {        String  message = "";        //判断异常类型        if(ex instanceof ConstraintViolationException){            ConstraintViolationException exs = (ConstraintViolationException) ex;            //获取验证不通过的信息            Set<ConstraintViolation<?>> violations = exs.getConstraintViolations();            //遍历验证不通过的信息            for (ConstraintViolation<?> item : violations) {                //将验证不通过的信息拼接到message中                message+=item.getMessage()+",";            }        }        //返回错误信息        return ResponseEntity.badRequest().body(message);    } }
复制代码


6.3 测试校验结果


入参正确情况

http://localhost:8080/testSingleParmaValidator?name=小凡&age=18
复制代码



入参不正确情况

http://localhost:8080/testSingleParmaValidator?name=&age=17
复制代码



八、参数校验分组


在实际开发中,我们会遇到这样的情况:同一个实体类可能会在多个接口中使用,但每次的校验场景又不一样。

例如:新增用户和修改用户接口,参数都是User 实体,在新增用户的时候ID字段 可以为空,但name字段 不能为空

在修改用户的是由ID字段不能为空,这种时候就可以使用参数分组来实现。


8.1 定义验证分组接口


定义两个分组接口CreateUserGroup(用户创建组),UpdateUserGroup(用户更新组),

分别继承javax.validation.groups.Default,标识不同的业务场景


public interface CreateUserGroup extends Default {} public interface UpdateUserGroup extends Default {}
复制代码


注:继承 Default 并不是必须的。只是说,如果继承了Default,那么@Validated(value = Create.class)的校验范畴就为【Create】和【Default】;如果没继承 Default,那么@Validated(value = Create.class)的校验范畴只为【Create】,而@Validated(value = {Create.class, Default.class})的校验范畴才为【Create】和【Default】


8.2 分组校验的使用


① 在实体中添加groups 属性

@Datapublic class User {     //用户ID    @NotNull(message = "用户ID不能为空",groups = UpdateUserGroup.class)  //用户更新接口必须传递用户ID    private Integer id;     //姓名    @NotBlank(message = "用户名不能为空",groups = CreateUserGroup.class)  //用户创建接口必须传递用户名    private  String name;     //性别    @NotBlank(message = "性别不能为空")   //注解确保性别不为空    private String sex;     //年龄    @NotNull(message = "年龄不能为空")  //注解确保年龄不为空    @Max(value = 120,message = "年龄不能大于120")  //注解确保年龄必须小于等于120    @Min(value = 18,message = "年龄不能小于18")   //注解确保年龄必须大于等于18    private  Integer age;     //邮箱    @Email(message = "邮箱格式不正确")    //注解确保邮箱格式正确    @NotBlank(message = "邮箱不能为空")    private String email;}
复制代码


②在接口中使用分组

使用 @Validated 注解,并指定要执行的验证组。

//添加用户@PostMapping("/addUser")public ResponseEntity<User> addUser(@Validated(value= CreateUserGroup.class) @RequestBody User user){     return ResponseEntity.ok(user);}//更新用户@PutMapping("/updateUser")public ResponseEntity<User> updateUserUser(@Validated(value= UpdateUserGroup.class) @RequestBody User user){     return ResponseEntity.ok(user);}
复制代码


我们指定 create 接口指定 CreateUserGroup 分组,update 接口指定 UpdateUserGroup


8.3 测试一下接口


接口入参

{    "name":"小凡",    "sex":"男",    "age":18,    "email":"xiezhr@qq.com"}
复制代码


addUser接口添加用户,不需要 id,验证通过



updateUser接口修改用户,需要传入 id,校验不通过



九、嵌套对象校验


9.1 构造一个员工信息表

@Datapublic class Emp {    @NotBlank(message = "员工编号不能为空")    private  String empNo;    @NotBlank(message = "员工姓名不能为空")    private  String empName;    @NotBlank(message = "员工职位不能为空")    private  String job;    @Valid                  //这里必须使用@Valid注解    private  Dept dept;}
复制代码


@Datapublic class Dept {    @NotBlank(message = "部门编号不能为空")    private String  deptNo;    @NotBlank(message = "部门名称不能为空")    private String  deptName;}
复制代码


在这个示例中, Dept 类包含三个字段需要校验: deptNo 和``deptName字段,通过在Dept类中的每个字段上添加相应的校验注解,然后在Emp类中的dept字段上添加@Valid` 注解,可以实现对嵌套对象中多个字段进行参数校验。


9.2 嵌套对象的使用

@PostMapping("/emp")public ResponseEntity<String> createOrder(@Valid @RequestBody Emp emp) {    return ResponseEntity.ok("参数校验成功");}
复制代码


9.3 测试一下

① 正确入参情况

{    "empNo":"10001",    "empName":"小凡",    "job":"程序员",    "dept":{        "deptNo":"20001",        "deptName":"研发部111"    }}
复制代码



② 不正确入参情况

{    "empNo":"10001",    "empName":"",    "job":"程序员",    "dept":{        "deptNo":"20001",        "deptName":""    }}
复制代码



十、自定义参数校验


SpringBoot 提供的注解校验功能可以满足大多数的验证需求,但如果在系统中需要实现一些特殊的校验功能时,

我们可以根据规则自定义校验

下面我们来手把手教你自定义一个字符串校验,校验字符串必须为大写或小写


10.1 自定义注解类


我们要自定义验证功能,需要首先自定义注解,以便我们在实体类中使用它,代码如下

①定义一个枚举类 CaseMode :

public enum CaseMode {    UPPER,    LOWER;}
复制代码


②创建一个自定义的校验注解 @CheckCase

@Target({ElementType.FIELD})@Retention(RetentionPolicy.RUNTIME)@Constraint(validatedBy = CheckCaseValidator.class)public @interface CheckCase {    String message() default "字符串必须是大写或小写";     Class<?>[] groups() default {};     Class<? extends Payload>[] payload() default {};     CaseMode value();}
复制代码


10.2 自定义验证业务逻辑类

public class CheckCaseValidator implements ConstraintValidator<CheckCase, String> {     private CaseMode caseMode;     @Override    public void initialize(CheckCase constraintAnnotation) {        // 获取约束注解的值        this.caseMode = constraintAnnotation.value();    }     @Override    public boolean isValid(String value, ConstraintValidatorContext context) {        // 如果值为空,则返回true        if (value == null) {            return true;        }         // 根据caseMode的值,判断value是否需要转换大小写        if (caseMode == CaseMode.UPPER) {            return value.equals(value.toUpperCase());        } else {            return value.equals(value.toLowerCase());        }    }}
复制代码


10.3 自定义校验注解使用


①在Car实体类上添加注解

@Datapublic class Car {    //车牌号    @CheckCase(value = CaseMode.UPPER,message = "车牌号必须为大写")    private  String brand;    //颜色    @CheckCase(value = CaseMode.LOWER,message = "颜色必须为小写")    private  String color;}
复制代码


②在controller 中校验参数

@GetMapping ("/car")public ResponseEntity<String> validatorCar(@Valid @RequestBody Car car) {    return ResponseEntity.ok("参数校验成功");}
复制代码


10.4 测试一下


①入参正确情况

{    "brand":"云A.888888",    "color":"red"}
复制代码



②入参错误情况

{    "brand":"云a.888888",    "color":"RED"}
复制代码



文章转载自:xiezhr

原文链接:https://www.cnblogs.com/xiezhr/p/18093602

体验地址:http://www.jnpfsoft.com/?from=001

用户头像

还未添加个人签名 2023-06-19 加入

还未添加个人简介

评论

发布
暂无评论
SpringBoot如何优雅的进行参数校验_Java_不在线第一只蜗牛_InfoQ写作社区