SpringBoot 参数校验

本文详细介绍了在SpringBoot中如何利用JavaBeanValidationAPI进行参数校验,包括引入依赖、定义和使用内置与自定义校验注解,以及分组校验和嵌套校验的实现方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、常见的参数接收方式

在Spring Boot项目中,接收客户端请求参数的方式非常灵活,以下是主要的参数接收方式及代码示例:

  1. @RequestParam - 接收查询参数

    • 请求示例:
      GET /user?id=100 或 GET /search?keyword=spring&page=2
      
    • 接收示例:
      // 接收单个参数(可设置默认值)
      @GetMapping("/user")
      public String getUser(@RequestParam(name = "id", defaultValue = "1") Long userId) {
      	return "User ID: " + userId;
      }
      
      // 接收多个参数
      @GetMapping("/search")
      public String search(@RequestParam String keyword, 
                       	@RequestParam int page) {
      	return "Search: " + keyword + " Page: " + page;
      }
      
      // 接收Map(需指定参数名前缀)
      @GetMapping("/filter")
      public String filter(@RequestParam Map<String, String> params) {
      	return params.toString();
      }
      
  2. @PathVariable - 接收路径参数

    • 请求示例:
      GET /article/123/category/tech
      
    • 接收示例:
      @GetMapping("/article/{id}/category/{catId}")
      public String getArticle(
              @PathVariable("id") Long articleId,
              @PathVariable String catId) { // 变量名相同可省略参数
          return "Article: " + articleId + " Category: " + catId;
      }
      
  3. @RequestBody - 接收JSON/XML请求体

    • 请求示例:
      POST /createUser
      Content-Type: application/json
      {"name":"Alice","age":25,"email":"alice@example.com"}
      
    • 接收示例:
      @PostMapping("/createUser")
      public ResponseEntity<User> createUser(@RequestBody User user) {
          // 自动将JSON映射到User对象
          return ResponseEntity.ok(userService.save(user));
      }
      
  4. @ModelAttribute - 接收表单数据

    • 请求示例:
      POST /register
      Content-Type: application/x-www-form-urlencoded
      name=Bob&age=30&email=bob@example.com
      
    • 接收示例:
      @PostMapping("/register")
      public String register(@ModelAttribute UserForm form) {
          // 自动绑定表单字段到对象属性
          return "redirect:/success";
      }
      
  5. HttpServletRequest - 原生请求对象

    • 接收示例:
      @GetMapping("/info")
      public String getInfo(HttpServletRequest request) {
          String token = request.getHeader("Authorization");
          String clientIp = request.getRemoteAddr();
          return "IP: " + clientIp + " | Token: " + token;
      }
      

二、参数校验处理逻辑

在Spring Boot中,实现参数校验主要依赖于Java Bean Validation API(JSR 380),以及Spring框架对该API的集成支持。以下是具体步骤:

  1. 引入依赖:首先确保项目中引入了Spring Validation相关的依赖。如果使用的是Spring Boot 2.3.x之前的版本,spring-boot-starter-web会自动包含hibernate-validator。对于2.3.x及以后的版本,可能需要手动添加spring-boot-starter-validation依赖。
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
  1. 定义校验约束:使用Hibernate Validator提供的注解来标记你想要校验的字段。例如,@NotNull表示该字段不能为null,@Size(min=3, max=50)表示字符串长度必须在3到50个字符之间,@Email用于校验邮箱格式等。
public class User {
    @NotNull(message = "用户名不能为空")
    private String username;
    
    @Email(message = "邮箱格式不正确")
    private String email;
    
    // getter和setter方法
}
  1. 控制器层校验:在控制器方法的参数上使用@Valid注解,这样Spring MVC会自动触发参数校验。如果校验失败,会抛出MethodArgumentNotValidException异常。
@PostMapping("/user")
public ResponseEntity<?> createUser(@Valid @RequestBody User user) {
    // 创建用户逻辑
    return ResponseEntity.ok().build();
}
  1. 全局异常处理:通过创建一个@ControllerAdvice类来统一处理校验异常,返回标准化的响应。
@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseBody
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Map<String, Object> handleValidationExceptions(
            MethodArgumentNotValidException ex) {
        Map<String, Object> errors = new HashMap<>();
        ex.getBindingResult().getAllErrors().forEach((error) -> {
            String fieldName = ((FieldError) error).getField();
            String errorMessage = error.getDefaultMessage();
            errors.put(fieldName, errorMessage);
        });
        return errors;
    }
}
  1. 自定义校验注解:如果内置注解无法满足需求,可以创建自定义注解和相应的校验器。

三、常用校验注解

JSR 380 (Bean Validation API) 提供了一系列基本校验注解,用于在Java Bean属性上执行不同类型的校验。下面是一些常用的校验注解及其说明:

注解描述说明
@NotNull验证指定的元素不为null
@Null验证指定的元素必须为null
@NotBlank验证字符串字段非null,且长度大于0,不只包含空白字符
@NotEmpty验证集合、数组、Map或字符串不为null,且不为空
@Size(min=, max=)验证字符串、集合、数组或Map的大小在特定范围内
@Length(min=, max=)验证注解的元素值长度是否在min和max区间内
@Range(min=, max=)验证注解的元素值在min和max之间
@Min验证数字类型的字段值必须大于等于指定的最小值
@Max验证数字类型的字段值必须小于等于指定的最大值
@DecimalMin验证数字类型的字段值必须大于等于指定的最小值,支持小数点
@DecimalMax验证数字类型的字段值必须小于等于指定的最大值,支持小数点
@Pattern验证字符串是否符合正则表达式的模式
@Future检查日期类型的字段值是否在当前时间之后
@Past检查日期类型的字段值是否在当前时间之前
@Email检查字符串字段是否是有效的电子邮件地址
@AssertFalse验证注解的元素值是false
@AssertTrue验证注解的元素值是true

四、自定义校验注解

在Spring Boot中创建自定义校验注解涉及以下几个步骤:

  1. 定义校验注解:使用@Constraint注解来定义你的自定义校验注解。你需要指定约束的类型、消息、错误代码和分组。
@Documented
@Constraint(validatedBy = MyConstraintValidator.class)
@Target({ ElementType.METHOD, ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface MyConstraint {
    String message() default "自定义校验信息";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}
  1. 实现校验器:创建一个实现ConstraintValidator接口的类。这个类包含initialize和isValid方法。isValid方法用于实际的校验逻辑。
public class MyConstraintValidator implements ConstraintValidator<MyConstraint, Object> {
    
    @Override
    public void initialize(MyConstraint constraintAnnotation) {
        // 初始化代码,如果需要可以从注解中读取配置信息
    }

    @Override
    public boolean isValid(Object value, ConstraintValidatorContext context) {
        // 这里编写具体的校验逻辑
        if (value == null) {
            return true; // 或者返回false,取决于你希望null值是否通过校验
        }
        // 例如,检查value是否为特定的字符串
        return "expectedValue".equals(value.toString());
    }
}
  1. 在实体或DTO中使用自定义注解:在需要校验的字段上应用你的自定义注解。
public class User {
    
    @MyConstraint(message = "用户名不符合预期")
    private String username;
    
    // 其他字段和方法
}

五、@Validated与@Valid区别

@Validated和@Valid都是用于Java Bean验证的注解,它们之间的主要区别在于功能和使用场景:

  1. 分组校验(Groups):
    • @Validated支持校验组,允许开发者根据不同的场景定义一组校验注解,并在运行时指定要执行哪组校验。
    • @Valid不支持校验组,它会触发所有的校验注解。
  2. 嵌套校验:
    • @Validated不支持嵌套校验。
    • @Valid支持嵌套校验。
  3. 使用范围:
    • @Validated 一般用在或者方法参数,但不能用于字段
    • @Valid 可以用在方法参数字段构造器参数上。
  4. 集成方式:
    • @Validated是Spring Framework提供的扩展注解,它在JSR 303/JSR 349/J憝 380的基础上增加了额外的特性,比如对校验组的支持,因此它的使用通常与Spring应用程序结合得更加紧密。
    • @Valid是JSR 303/JSR 349/JSR 380规范的核心注解之一,大多数遵循该规范的框架都支持@Valid。
  5. 默认行为:
    • @Validated允许通过设置ignoreEmpty属性来忽略那些值为空的非空约束注解。
    • @Valid在校验时不会忽略非空的约束注解,即使它们的值是空的也会触发校验。

六、@Valid 和 @Validated 不同场景的使用案例

@Valid和@Validated在不同场景下的使用示例,主要包含以下:

  • 场景1:Controller层校验对象(使用@Valid)
  • 场景2:分组校验(使用@Validated)
  • 场景3:Service层方法参数校验(使用@Validated)
  • 场景4:嵌套校验(使用@Valid)
  • 场景5:集合内元素校验(使用@Valid)
  • 场景6:自定义校验注解
6.1 DTO 对象定义
class UserDTO {
    @NotBlank(message = "用户名不能为空")
    private String username;

    @Email(message = "邮箱格式不正确")
    private String email;

    @Min(value = 18, message = "年龄必须大于18岁")
    private int age;

    @Valid // 必须添加以触发嵌套校验
    private AddressDTO address;
    
    @Valid // 校验集合内的每个元素
    private List<@Valid OrderItemDTO> recentOrders = new ArrayList<>();

    // Getters and setters
    public String getUsername() { return username; }
    public void setUsername(String username) { this.username = username; }
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
    public int getAge() { return age; }
    public void setAge(int age) { this.age = age; }
    public AddressDTO getAddress() { return address; }
    public void setAddress(AddressDTO address) { this.address = address; }
    public List<OrderItemDTO> getRecentOrders() { return recentOrders; }
    public void setRecentOrders(List<OrderItemDTO> recentOrders) { this.recentOrders = recentOrders; }
}

class AddressDTO {
    @NotBlank(message = "城市不能为空")
    private String city;
    
    @Pattern(regexp = "\\d{6}", message = "邮编必须是6位数字")
    private String zipCode;

    // Getters and setters
    public String getCity() { return city; }
    public void setCity(String city) { this.city = city; }
    public String getZipCode() { return zipCode; }
    public void setZipCode(String zipCode) { this.zipCode = zipCode; }
}

class OrderItemDTO {
    @NotBlank(message = "商品名称不能为空")
    private String productName;
    
    @Min(value = 1, message = "数量至少为1")
    private int quantity;
    
    @DecimalMin(value = "0.01", message = "价格必须大于0")
    private BigDecimal price;

    // Getters and setters
    public String getProductName() { return productName; }
    public void setProductName(String productName) { this.productName = productName; }
    public int getQuantity() { return quantity; }
    public void setQuantity(int quantity) { this.quantity = quantity; }
    public BigDecimal getPrice() { return price; }
    public void setPrice(BigDecimal price) { this.price = price; }
}

======================== 分组校验接口 ========================
interface BasicValidation {}
interface AdvancedValidation {}

class ProductDTO {
    @Null(groups = BasicValidation.class, message = "ID必须为空")
    @NotNull(groups = AdvancedValidation.class, message = "ID不能为空")
    private Long id;

    @NotBlank(groups = BasicValidation.class, message = "名称不能为空")
    private String name;
    
    @DecimalMin(value = "0.01", groups = AdvancedValidation.class, message = "价格必须大于0")
    private BigDecimal price;

    // Getters and setters
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public BigDecimal getPrice() { return price; }
    public void setPrice(BigDecimal price) { this.price = price; }
}

======================== 自定义校验注解 ========================
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneValidator.class)
@interface ValidPhone {
    String message() default "手机号格式不正确";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

class PhoneValidator implements ConstraintValidator<ValidPhone, String> {
    private static final Pattern PHONE_PATTERN = Pattern.compile("^1[3-9]\\d{9}$");
    
    @Override
    public boolean isValid(String phone, ConstraintValidatorContext context) {
        return phone != null && PHONE_PATTERN.matcher(phone).matches();
    }
}

class ContactDTO {
    @NotBlank(message = "姓名不能为空")
    private String name;
    
    @ValidPhone(message = "请输入有效的手机号")
    private String phone;

    // Getters and setters
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public String getPhone() { return phone; }
    public void setPhone(String phone) { this.phone = phone; }
}
6.2 示例:

================================== Controller 层 ==============================

@RestController
@RequestMapping("/api")
public class ValidationController {
    
    // 场景1: 基础对象校验 (@Valid)
    @PostMapping("/users")
    public ResponseEntity<?> createUser(@RequestBody @Valid UserDTO userDTO, 
                                        BindingResult result) {
        if (result.hasErrors()) {
            return ResponseEntity.badRequest().body(
                result.getFieldErrors().stream()
                    .map(e -> e.getField() + ": " + e.getDefaultMessage())
                    .collect(Collectors.toList())
            );
        }
        return ResponseEntity.ok("用户创建成功");
    }
    
    // 场景2: 分组校验 (@Validated)
    @PostMapping("/products/basic")
    public ResponseEntity<?> createBasicProduct(
            @RequestBody @Validated(BasicValidation.class) ProductDTO product) {
        return ResponseEntity.ok("基础产品创建成功");
    }
    
    @PutMapping("/products/advanced")
    public ResponseEntity<?> updateProduct(
            @RequestBody @Validated(AdvancedValidation.class) ProductDTO product) {
        return ResponseEntity.ok("产品更新成功");
    }
    
    // 场景6: 自定义校验
    @PostMapping("/contact")
    public ResponseEntity<?> addContact(@RequestBody @Valid ContactDTO contact) {
        return ResponseEntity.ok("联系方式添加成功");
    }
}

================================== Service 层 ==============================

@Service
@Validated // 必须添加类级别注解
public class BusinessService {
    
    // 场景4: Service层基础类型参数校验
    public void processPayment(
            @NotBlank(message = "订单ID不能为空") String orderId,
            @DecimalMin(value = "0.01", message = "金额必须大于0") BigDecimal amount) {
        System.out.println("处理订单: " + orderId + ", 金额: " + amount);
    }
    
    // 场景5: Service层分组校验
    public void updateProductPrice(
            @NotNull(groups = AdvancedValidation.class, message = "产品ID不能为空") Long productId,
            @DecimalMin(value = "0.01", groups = AdvancedValidation.class, 
                        message = "价格必须大于0") BigDecimal newPrice) {
        System.out.println("更新产品价格: " + productId + " -> " + newPrice);
    }
}

七、分组校验

在Spring Boot中进行参数分组校验,可以通过定义校验组接口和在注解中指定这些校验组来实现。下面是一个具体的例子:

  1. 首先,定义校验组接口:
public interface DefaultGroup {
}

public interface AdvancedGroup extends DefaultGroup {
}
  1. 然后,在参数实体类中使用这些校验组,通过groups来指定校验注解所属的组。
public class User {

    @NotNull(groups = DefaultGroup.class)
    private String username;

    @NotNull(groups = AdvancedGroup.class)
    private String password;

    // Getter and Setter methods
}
  1. 最后,在控制器方法中使用@Validated注解,并指定校验组。这里只能使用@Validated注解,使用@Valid注解会报错
@RestController
public class UserController {

    @PostMapping("/user")
    public String createUser(@Validated({DefaultGroup.class}) @RequestBody User user) {
        // 业务逻辑处理
        return "User created successfully";
    }
    
	@PostMapping("/advancedUser")
    public String createAdvancedUser(@Validated({AdvancedGroup.class}) @RequestBody User user) {
        // 业务逻辑处理
        return "User created successfully";
    }
}
在上面的例子中,createUser方法仅应用了DefaultGroup校验组,因此只有username字段会被校验。
而createAdvancedUser方法应用了AdvancedGroup校验组,而在定义AdvancedGroup组的时候继承了DefaultGroup组,
这就相当于同时应用了两个组,所以password和username字段都会被校验。
如果不同的组之间不存在继承关系,又想同时多个组一起校验,
那就使用@Validated({@Validated({AdvancedGroup.class}), AdvancedGroup.class})指定多个组就好。

八、嵌套校验

嵌套校验指的是接收参数的实体里面还嵌套了其他实体对象,需要连同其他的实体对象中的参数一起校验。以下是一个示例来说明如何对嵌套对象进行校验;

  1. 首先,假设我们有两个实体类Parent和Child用来接收参数,其中Parent类包含一个Child类型的属性:
public class Parent {
    @Min(value = 1)
    private Integer id;
    
    private String name;
    
    private Child child;

    // 省略getter和setter方法
}

public class Child {
    @Min(value = 1)
    private Integer id;
    
    @NotNull(message = "Child name cannot be null")
    private String name;

    // 省略getter和setter方法
}
这里Parent 类的id字段做最小值的校验,Child 类的id以及nanme字段都做了校验。
  1. 然后,在控制器方法中对Parent对象及其嵌套的Child对象进行参数校验。其实这里如果只是像上面第一步那样设置接收参数的实体类的话,无论使用@Valid或@Validated注解都无法对Child 对象的属性进行校验,想要达到嵌套校验的效果还需要对第一步接收参数的实体稍作改动
@RestController
public class ParentController {

    @PostMapping("/parent")
    public String createParent(@Valid @RequestBody Parent parent) {
        // 此处可以进行业务逻辑处理,如果校验失败,则会抛出异常
        // ...
        
        return "Parent created successfully";
    }
}
  1. 对第一步定义的Parent 类进行改造,往嵌套的Child对象上添加@Valid注解,只有在需要嵌套校验的对象上添加@Valid注解,才会在接口进行参数校验时,连同子对象一起校验。由于字段属性上只能使用@Valid注解,无法使用@Validated注解,这就是为什么@Validated注解不支持嵌套校验,@Valid注解支持的原因
public class Parent {
    @Min(value = 1)
    private Integer id;
    
    private String name;
    
    @Valid // 实现嵌套校验的关键就在这个注解上
    private Child child;

    // 省略getter和setter方法
}
  1. 第三步修改完后,现在控制器方法中无论使用@Valid或@Validated注解,在调用这个接口的时候,Parent 以及Child 中有注解校验的字段都会进行校验。

九、最佳实践

  1. Controller层对象校验
    • 在方法参数前使用@Valid 或 @Validated,推荐使用@Valid。
    • 结合 BindingResult 处理错误信息。
  2. Service层校验
    • 必须在类上添加 @Validated。
    • 直接在方法参数上使用约束注解。
  3. 分组校验
    • 定义不同的校验规则接口。
    • 使用 @Validated 指定校验分组。
  4. 嵌套对象校验
    • 在嵌套对象字段上添加 @Valid。
    • 集合元素校验使用 List<@Valid T>。
  5. 自定义校验
    • 实现 ConstraintValidator 接口。
    • 创建自定义注解。
  6. 全局异常处理
    • 统一处理 MethodArgumentNotValidException。
    • 统一处理 ConstraintViolationException。
  7. 校验性能优化
    • 避免过度复杂的校验逻辑。
    • 合理使用分组校验减少不必要的检查。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值