Spring @Validated注解用法:完整示例与解析 – wiki基地

Spring @Validated 注解用法:完整示例与解析

在 Spring 框架中,数据校验是保证应用程序健壮性的重要一环。@Validated 注解是 Spring 提供的用于声明式验证的强大工具,它基于 JSR-303(Bean Validation)规范,可以轻松地对方法参数、方法返回值以及 JavaBean 属性进行校验。本文将深入探讨 @Validated 注解的用法、原理、与 @Valid 的区别,并通过多个示例来展示其在实际开发中的应用。

1. JSR-303(Bean Validation)简介

在深入 @Validated 之前,我们需要先了解 JSR-303。JSR-303 是 Java 平台的一项规范,也称为 Bean Validation。它定义了一套标准的校验 API,用于对 Java Bean 中的字段、方法参数和返回值进行校验。Bean Validation 提供了一组内置的约束注解(如 @NotNull, @Size, @Email 等),同时也支持自定义约束注解。

Hibernate Validator 是 Bean Validation 的参考实现,Spring 框架对 Bean Validation 提供了很好的集成支持。

2. @Validated 注解详解

@Validated 是 Spring 框架提供的注解,它是对 JSR-303 Bean Validation 的增强。@Validated 主要用于以下几个方面:

  • 方法参数校验: 可以对控制器(Controller)或服务(Service)类中的方法参数进行校验。
  • 方法返回值校验: 可以对方法返回值进行校验 (需要配合其他设置).
  • 嵌套校验: 可以触发嵌套对象的校验(需要与 @Valid 配合使用)。
  • 分组校验: 可以指定在特定场景下应用哪些校验规则(通过 groups 属性)。

@Validated 注解的属性:

  • value (或 groups): 这是一个 Class<?>[] 类型的属性,用于指定校验分组。 校验分组允许你定义不同的校验规则集合,并在不同的场景下应用不同的集合。例如,你可以定义一个用于创建对象的校验分组和一个用于更新对象的校验分组。

3. @Validated@Valid 的区别

@Valid 也是 JSR-303 规范中的注解,它和 @Validated 都可以用于触发校验,但它们之间存在一些关键的区别:

特性 @Validated @Valid
来源 Spring Framework JSR-303 (Bean Validation)
分组校验 支持 不支持
嵌套校验 需要与 @Valid 配合使用 可以单独使用
方法参数校验 支持 (Spring 增强) 不支持(仅用于字段、方法和类型)
方法返回值校验 支持 (Spring AOP 增强, 需要额外配置) 不支持
使用场景 控制器层参数校验、服务层方法校验、分组校验 JavaBean 属性校验、嵌套校验

总结:

  • @Validated 是 Spring 对 JSR-303 的扩展,提供了分组校验、方法参数/返回值校验等更强大的功能。
  • @Valid 是 JSR-303 标准注解,主要用于 JavaBean 的属性校验和嵌套校验。
  • 在 Spring 环境中,通常建议在控制器层使用 @Validated 进行方法参数校验,而在 JavaBean 中使用 @Valid 进行属性校验。
  • 对于嵌套校验, @Validated 可以标注在类级别或者方法参数上, 内部被校验对象需要使用@Valid.

4. @Validated 使用示例

4.1. 准备工作

首先,我们需要在项目中添加必要的依赖。如果你使用 Maven,可以在 pom.xml 文件中添加以下依赖:

“`xml

org.springframework.boot
spring-boot-starter-validation


org.springframework.boot
spring-boot-starter-web

“`

spring-boot-starter-validation 包含了 Hibernate Validator 的实现。spring-boot-starter-web 包含了 Spring MVC 的支持。

4.2. JavaBean 校验

我们先创建一个简单的 JavaBean,并使用 JSR-303 注解定义校验规则:

“`java
import javax.validation.constraints.*;
import javax.validation.Valid;

public class User {

public interface Create {} // 分组 - 创建
public interface Update {} // 分组 - 更新
private Long id;

@NotBlank(message = "用户名不能为空", groups = {Create.class, Update.class})
@Size(min = 2, max = 20, message = "用户名长度必须在2到20个字符之间", groups = {Create.class, Update.class})
private String username;

@NotBlank(message = "密码不能为空", groups = {Create.class})
@Size(min = 6, message = "密码长度至少为6个字符", groups = {Create.class})
private String password;

@Email(message = "邮箱格式不正确", groups = {Create.class, Update.class})
private String email;

@NotNull(message = "年龄不能为空",groups = {Create.class, Update.class})
@Min(value = 18, message = "年龄必须大于等于18岁", groups = {Create.class, Update.class})
private Integer age;

@Valid //用于嵌套校验
private Address address;
// Getters and setters...
public Long getId() {  return id; }
public void setId(Long id) { this.id = id;}

public String getUsername() { return username; }
public void setUsername(String username) {  this.username = username;}

public String getPassword() { return password; }
public void setPassword(String password) { this.password = password;    }

public String getEmail() { return email;    }
public void setEmail(String email) {  this.email = email; }

public Integer getAge() { return age; }
public void setAge(Integer age) { this.age = age;}

public Address getAddress() {  return address; }
public void setAddress(Address address) { this.address = address; }

}

public class Address{
@NotBlank(message = “街道不能为空”)
private String street;

@NotBlank(message = "城市不能为空")
private String city;

@NotBlank(message = "邮编不能为空")
private String zipCode;
//getters and setters
public String getStreet() { return street; }
public void setStreet(String street) { this.street = street; }
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;}

}

“`

在这个示例中,我们使用了以下 JSR-303 注解:

  • @NotBlank: 字符串不能为空,且去除首尾空格后长度必须大于 0。
  • @Size: 字符串、集合或数组的长度必须在指定范围内。
  • @Email: 字符串必须是有效的邮箱地址。
  • @NotNull: 值不能为null.
  • @Min: 数值必须大于等于指定值。
  • @Valid: 用于Address属性, 触发嵌套校验.

我们还定义了两个校验分组:CreateUpdatepassword 字段只在创建用户时进行校验。

4.3. 控制器层方法参数校验

接下来,我们在控制器中使用 @Validated 注解来校验请求参数:

“`java
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import javax.validation.constraints.Min;

@RestController
@RequestMapping(“/users”)
@Validated // 可以放在类上,对所有方法生效
public class UserController {

@PostMapping("/create")
public String createUser(@RequestBody @Validated(User.Create.class) User user) {
    // ... 保存用户信息
    return "User created successfully!";
}

@PutMapping("/update/{id}")
public String updateUser(@PathVariable("id") @Min(1) Long id,
                         @RequestBody @Validated(User.Update.class) User user) {
    // ... 更新用户信息
    return "User updated successfully!";
}

@PostMapping("/createWithAddress")
public String createWithAddress(@RequestBody @Validated({User.Create.class}) User user) {
    //address 也会被校验
    return "User with address created!";
}

 @GetMapping("/details/{id}")
 public @ResponseBody @Valid User getUserDetails(@PathVariable("id") @Min(1) Long id){
    //模拟查询
    User user = new User();
    user.setId(id);
    user.setUsername("TestUser");
    user.setAge(20);  //如果返回的User对象的age或者username校验不通过, 则会报错.
    return user;
 }

}
“`

在这个示例中:

  • @Validated(User.Create.class): 在 createUser 方法中,我们使用 @Validated 注解并指定 User.Create 分组,表示只应用 User 类中标注了 Create 分组的校验规则。
  • @Validated(User.Update.class): 在 updateUser 方法中,我们指定 User.Update 分组,应用 User 类中标注了 Update 分组的校验规则。
  • @PathVariable("id") @Min(1) Long id: 对路径参数id进行校验, 最小值为1.
  • @Validated({User.Create.class}) on createWithAddress: 因为使用了@ValidUser类的address属性上, 所以Address对象也会被校验.
  • @ResponseBody @Valid User: 在getUserDetails 方法, 开启对返回值User的校验. 注意这里需要@Valid (JSR-303标准注解) 且需要额外配置.
  • @Validated 加在类上, 表示对该控制器中所有方法都进行校验.

4.4. 全局异常处理

为了优雅地处理校验失败的情况,我们可以创建一个全局异常处理器:

“`java
import org.springframework.http.HttpStatus;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.validation.ConstraintViolationException;
import java.util.HashMap;
import java.util.Map;

@RestControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Map<String, String> handleValidationExceptions(MethodArgumentNotValidException ex) {
    Map<String, String> errors = new HashMap<>();
    ex.getBindingResult().getAllErrors().forEach((error) -> {
        String fieldName = ((FieldError) error).getField();
        String errorMessage = error.getDefaultMessage();
        errors.put(fieldName, errorMessage);
    });
    return errors;
}

@ExceptionHandler(ConstraintViolationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Map<String, String> handleConstraintViolationException(ConstraintViolationException ex) {
    Map<String, String> errors = new HashMap<>();
    ex.getConstraintViolations().forEach(violation -> {
        String fieldPath = violation.getPropertyPath().toString();
        String message = violation.getMessage();
        errors.put(fieldPath, message);
    });
    return errors;
}

 @ExceptionHandler(Exception.class)
 @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
 public String handleAllException(Exception ex){
    return "An unexpected error occurred: "+ ex.getMessage();
 }

}
“`

  • @RestControllerAdvice: 这是一个组合注解,等同于 @ControllerAdvice + @ResponseBody
  • @ExceptionHandler(MethodArgumentNotValidException.class): 处理 MethodArgumentNotValidException 异常,该异常通常在 @RequestBody 参数校验失败时抛出。
  • @ExceptionHandler(ConstraintViolationException.class): 处理 ConstraintViolationException 异常,该异常通常在方法参数或返回值校验失败时抛出。
  • @ResponseStatus(HttpStatus.BAD_REQUEST): 将响应状态码设置为 400(Bad Request)。

4.5 方法返回值校验配置 (MethodValidationPostProcessor)

要开启方法返回值校验, 需要配置MethodValidationPostProcessor.

“`java

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;

@Configuration
public class ValidationConfig {
@Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
return new MethodValidationPostProcessor();
}
}
“`

4.6. 服务层方法校验

除了控制器层,我们也可以在服务层使用 @Validated 进行方法参数校验:

“`java
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import javax.validation.Valid;
import javax.validation.constraints.Min;
@Service
@Validated
public class UserService {

public void createUser(@Valid User user) {
    // ... 保存用户信息
}
public void updateUser( @Min(1) Long id, @Valid User user){
    // ...
}

}
“`

这里和控制器层类似,@Validated 可以放在类上,对所有方法生效;也可以放在方法参数上,对特定参数进行校验。

5. 自定义校验注解

除了使用 JSR-303 内置的校验注解,我们还可以自定义校验注解来满足特定的业务需求。

示例:自定义一个校验手机号格式的注解

  1. 创建注解:

“`java
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;

@Documented
@Constraint(validatedBy = PhoneNumberValidator.class)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface PhoneNumber {
String message() default “Invalid phone number”;

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

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

}
“`

  1. 创建校验器:

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

public class PhoneNumberValidator implements ConstraintValidator {

private static final String PHONE_NUMBER_PATTERN = "^1[3-9]\\d{9}$";

@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
    if (value == null) {
        return true; // 允许为空
    }
    return Pattern.matches(PHONE_NUMBER_PATTERN, value);
}

}
“`

  1. 使用自定义注解:

“`java
public class User {
// … 其他字段

@PhoneNumber(message = "手机号格式不正确", groups = {User.Create.class, User.Update.class})
private String phone;

// ... Getters and setters
public String getPhone() { return phone; }
public void setPhone(String phone) { this.phone = phone; }

}
“`

6. 总结

@Validated 注解是 Spring 框架中进行数据校验的强大工具,它基于 JSR-303(Bean Validation)规范,提供了灵活、便捷的校验方式。通过本文的介绍,你应该已经掌握了 @Validated 注解的用法、原理、与 @Valid 的区别,以及如何在实际开发中应用它。记住以下几点:

  • @Validated 是 Spring 对 JSR-303 的扩展,支持分组校验和方法参数/返回值校验。
  • @Valid 是 JSR-303 标准注解,用于 JavaBean 属性校验和嵌套校验。
  • 在控制器层使用 @Validated 进行方法参数校验,在 JavaBean 中使用 @Valid 进行属性校验。
  • 使用全局异常处理器优雅地处理校验失败的情况。
  • 可以自定义校验注解来满足特定的业务需求。

希望本文能帮助你更好地理解和使用 Spring 的 @Validated 注解,写出更健壮、更可靠的代码。

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部