如何在Spring Boot中优雅地进行参数校验

1. 前言

在平时的开发工作中,我们通常需要对接口进行参数格式验证。当参数个数较少(个数小于3)时,可以使用if ... else ...手动进行参数验证。当参数个数大于3个时,使用if ... else ...进行参数验证就会让代码显得臃肿,这个时候推荐使用注解来进行参数验证。

在Java中,注解(Annotation)是一种代码标记,通常用于提供元数据,这些元数据可以被编译器或运行时环境使用。这些注解通常用于框架和库中,以实现更加灵活和可配置的代码。

2. 常用注解描述

  1. @NotNull

    • 描述:标记一个值不能为null。
    • 示例:
    java 复制代码
    public class User {  
        @NotNull  
        private String name;  
        // ...  
    }
  2. @NotEmpty

    • 描述:标记一个集合(如List、Set等)不能为空。
    • 示例:
    java 复制代码
    public class User {  
        @NotEmpty  
        private List<String> interests;  
        // ...  
    }
  3. @NotBlank

    • 描述:标记一个字符串不能为空白(即null、空字符串或只包含空格)。
    • 示例:
    java 复制代码
    public class User {  
        @NotBlank  
        private String username;  
        // ...  
    }
  4. @Size

    • 描述:标记一个字符串或集合的大小必须在指定的范围内。
    • 示例:
    java 复制代码
    public class User {  
        @Size(min = 2, max = 50)  
        private String username;  
        // ...  
    }
  5. @Min@Max

    • 描述:标记一个数值必须在指定的最小值和最大值之间。
    • 示例:
    java 复制代码
    public class User {  
        @Min(18)
        @Max(60)  
        private int age;  
        // ...  
    }
  6. @DecimalMin@DecimalMax

    • 描述:标记一个浮点数或双精度数必须在指定的最小值和最大值之间。
    • 示例:
    java 复制代码
    public class User {  
        @DecimalMin("0.01") @DecimalMax("100.00")  
        private double discount;  
        // ...  
    }
  7. @Digits

    • 描述:标记一个整数或浮点数必须在指定的精度和总数值范围内。
    • 示例:
    java 复制代码
    public class User {  
        @Digits(integer = 3, fraction = 2) // 总长度为5,3位整数,2位小数。例如:"123.45" 是合法的。  
        private BigDecimal amount;  
        // ...  
    }
  8. @Pattern

    • 描述:标记一个字符串必须匹配指定的正则表达式。通常用于验证输入格式。例如电子邮件地址、电话号码格式等。@Pattern注解在javax.validation.constraints包中。@Pattern(regexp = "^\w{5,}$")表示长度在5-20之间,由字母、数字、下划线组成的字符串。@Pattern注解用于类字段上,例如用户密码字段。
    • 示例:
    java 复制代码
    public class User { 
        @Pattern(regexp = "^[a-zA-Z0-9]*$") 
        private String password; //... 
    } 
  9. @Email

    • 描述:标记一个字符串必须是一个有效的电子邮件地址。
    • 示例:
    java 复制代码
    @Email
    private String emailAddress;
  10. @AssertTrue@AssertFalse

    • 描述:标记一个布尔值必须为true或false。
    • 示例:
    java 复制代码
    @AssertTrue
    private boolean isValid;
    
    @AssertFalse
    private boolean isNotValid;
  11. @Future

    • 描述:标记一个日期必须是在未来某个时间点之后。
    • 示例:
    java 复制代码
    @Future
    private Date expiryDate;
  12. @Past

    • 描述:标记一个日期必须是在过去某个时间点之前。
    • 示例:
    java 复制代码
    @Past
    private Date purchaseDate;

这些注解通常与验证框架(如Hibernate Validator)一起使用,以在运行时验证对象的属性。

3. 注解使用

  1. 在项目的pom.xml文件中添加如下依赖:
xml 复制代码
<dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-web</artifactId>
 </dependency>
 <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-validation</artifactId>
 </dependency>
  1. 在实体类中使用上述注解,代码如下:

    java 复制代码
    package com.yyqq.demo.entity;
    
    import lombok.Data;
    import javax.validation.constraints.*;
    
    @Data
    public class User {
        @NotBlank(message = "用户姓名不能为空")
        private String name;
    
        @NotBlank(message = "密码不能为空")
        @Size(min = 6, message = "密码长度不能少于6位")
        private String password;
    
        @Min(value = 0, message = "年龄不能小于0岁")
        @Max(value = 150, message = "年龄不应超过150岁")
        private Integer age;
    
        @Pattern(regexp = "^((13[0-9])|(15[^4])|(18[0-9])|(17[0-9])|(147))\d{8}$", message = "手机号格式不正确")
        private String phone;
    }
  2. 控制器类使用验证,代码如下:

    java 复制代码
    import com.yyqq.demo.util.Result;
    import com.yyqq.demo.entity.User;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import javax.validation.Valid;
    
    @RestController
    @RequestMapping("/user")
    public class UserController {
    
        @PostMapping("/add")
        public Result add(@Valid @RequestBody User user) {
         	return Result.success(user);
        }
    }
  3. Result是封装结果的一个类,用于返回统一的结果,代码如下:

    java 复制代码
    package com.yyqq.demo.util;
    
    import lombok.Data;
    import java.io.Serializable;
    
    @Data
    public class Result<T> implements Serializable {
        private int code;
        private boolean success;
        private T data;
        private String msg;
    
        private Result(int code, T data, String msg) {
            this.code = code;
            this.data = data;
            this.msg = msg;
            this.success = code == 200;
        }
    
        public static <T> Result<T> sucess(T data) {
        	return new Result<>(200, data, null);
        }
    
        public static <T> Result<T> fail(String msg) {
        	return new Result<>(500, null, msg);
        }
    }
  4. 定义全局异常处理类,我们在全局异常处理类中使用ExceptionHandler捕获BindException异常,获取参数验证异常信息,最后返回统一的异常结果格式,代码如下:

    java 复制代码
    package com.yyqq.demo.util;
    
    import com.yyqq.demo.util.Result;
    import org.springframework.validation.BindException;
    import org.springframework.validation.BindingResult;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    import org.springframework.web.bind.annotation.RestControllerAdvice;
    
    @RestControllerAdvice
    public class GlobalExceptionHandler {
    
        @ExceptionHandler(BindException.class)
        public Result handleError(BindException e) {
            BindingResult bindingResult = e.getBindingResult();
            return Result.fail(bindingResult.getFieldError().getDefaultMessage());
        }
    }

4. 使用分组验证

  1. 用于插入记录时的分组验证,代码如下:
java 复制代码
package com.yyqq.demo.interceptor;
import javax.validation.groups.Default;

public interface Insert extends Default {
}
  1. 用于更新记录时的分组验证,代码如下:
java 复制代码
package com.yyqq.demo.interceptor;
import javax.validation.groups.Default;

public interface Update extends Default {
}
  1. 在实体类中进行分组标记,代码如下:
java 复制代码
package com.yyqq.demo.entity;

import lombok.Data;
import javax.validation.constraints.*;

@Data
public class User {
    @NotBlank(groups = {Insert.class, Update.class})
    @NotBlank(message = "用户姓名不能为空")
    private String name;

    @NotBlank(message = "密码不能为空")
    @Size(min = 6, message = "密码长度不能少于6位")
    private String password;

    @Min(value = 0, message = "年龄不能小于0岁")
    @Max(value = 150, message = "年龄不应超过150岁")
    private Integer age;

    @NotBlank(groups = {Insert.class, Update.class})
    @Pattern(regexp = "^((13[0-9])|(15[^4])|(18[0-9])|(17[0-9])|(147))\\d{8}$", message = "手机号格式不正确")
    private String phone;
}
  1. 控制器类使用分组验证
java 复制代码
package com.yyqq.demo.controller;

import com.yyqq.demo.util.Result;
import com.yyqq.demo.entity.User;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.Valid;

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

    @PostMapping("/add")
    public Result add(@Validated(Insert.class) @RequestBody User user) {
     	return Result.success(user);
    }
    
    @PostMapping("/update")
    public Result update(@Validated(Update.class) @RequestBody User user) {
     	return Result.success(user);
    }
}

5. 自定义验证注解

除了框架自带的注解,平时的工作中可能需要我们自定义验证注解处理特定的业务需求。

  1. 定义注解
java 复制代码
package com.yyqq.demo.validate;

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Documented
@Retention(RUNTIME)
@Constraint(validatedBy = {PhoneValidator.class})
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
public @interface Phone {

    String message() default "手机号格式错误";

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

    Class<? extends Payload>[] payload() default {};
}
  1. 定义验证器类
java 复制代码
package com.yyqq.demo.validate;

import javax.validation.ConstraintValidatorContext;
import javax.validation.ConstraintValidator;
import java.util.regex.Pattern;

public class PhoneValidator implements ConstraintValidator<Phone, String> {
    private static final String REGEX = "^((13[0-9])|(15[^4])|(18[0-9])|(17[0-9])|(147))\\d{8}$";

    @Override
    public boolean isValid(String s, ConstraintValidatorContext context) {
        boolean result = false;
        try {
            result = Pattern.matches(REGEX, s);
        } catch (Exception e) {
            System.out.println("验证手机号格式时发生异常,异常信息:" + e);
        }
        return result;
    }
}
  1. 实体类使用注解
java 复制代码
package com.yyqq.demo.validate;

public class User {

    //其他属性...

    //  @Pattern(regexp = "^((13[0-9])|(15[^4])|(18[0-9])|(17[0-9])|(147))\\d{8}$", message = "手机号格式不正确")
    @Phone
    private String phone;
}

6. @Valid与@Validated的区别

用于参数校验的注解通常有两个:@Valid@Validated。它们的区别有如下几点:

区别 @Valid @Validated
来源 @Valid是Java标准注解 @Validated是Spring框架定义的注解。
是否支持分组验证 不支持 支持
使用位置 构造函数、方法、方法参数、成员属性 类、方法、方法参数,不能用于成员属性
是否支持嵌套校验 支持 不支持
相关推荐
用户83562907805110 小时前
Python 实现 PDF 文件加密与解密方法
后端·python
用户83562907805110 小时前
使用 Python 冻结与拆分 Excel 窗格教程
后端·python
你好潘先生18 小时前
别再记命令了,用 yeero do 说句人话就能跑脚本,而且不烧 token
服务器·python·命令行
Agent_大师19 小时前
WebSocket 行情重连成功,K线缺口不会自动消失
python
荣码19 小时前
LLM结构化输出:让AI返回JSON而不是废话,我踩了4个坑
java·python
copyer_xyf19 小时前
FastAPI 如何连接 MySQL
后端·python
apocelipes1 天前
常用编程语言和库的正则表达式性能对比
c语言·c++·python·性能优化·golang·开发工具和环境
用户8356290780511 天前
使用 Python 在 PDF 中创建与管理书签
后端·python
MeixianAgent2 天前
Python 回测数据入口怎么验?历史 K 线入库前先做 5 个检查
后端·python
咕白m6252 天前
用 Python 实现一键批量查找与替换 Excel 数据
后端·python