Spring Boot 中的数据校验

1、概览

Spring Boot 通过 Hibernate Validator(Bean Validation 的实现)对数据验证提供了强大的支持。

本文将通过一个实际的 REST 应用带你了解如何在 Spring Boot 中校验数据。

2、Maven 依赖

pomx.ml 中添加 spring-boot-starter-webspring-boot-starter-jpaH2 database 依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency> 
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency> 
<dependency> 
    <groupId>com.h2database</groupId> 
    <artifactId>h2</artifactId>
    <version>2.1.214</version> 
    <scope>runtime</scope>
</dependency>

从 Boot 2.3 开始,还需要明确添加 spring-boot-starter-validation 依赖:

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

3、示例 Domain 类

定义一个 JPA 实体类,User

@Entity
public class User {
    
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;
    
    @NotBlank(message = "Name is mandatory")
    private String name;
    
    @NotBlank(message = "Email is mandatory")
    private String email;
    
    // 构造函数、Set、Set 方发省略
        
}

User 实体类很简单,它展示了如何使用 Bean Validation 的约束来限制 nameemail 字段。

为简单起见,这里仅使用 @NotBlank 约束来约束目标字段。此外,还使用 message 属性指定了错误信息。

因此,当 Spring Boot 验证类实例时,受约束字段必须不为 null,且去除其两边空白后长度必须大于零。

除了 @NotBlank 之外,Bean Validation 还提供了许多其他方便的约束。可以对受约束的类应用和组合不同的验证规则。如需了解更多信息,请阅读官方的 Bean Validation 文档

还要定义一个 UserRepository,用于把 User 保存到 H2 数据库:

@Repository
public interface UserRepository extends CrudRepository<User, Long> {}

4、REST Controller

创建 UserController

@RestController
public class UserController {

    @PostMapping("/users")
    ResponseEntity<String> addUser(@Valid @RequestBody User user) {
        // 存 储User
        return ResponseEntity.ok("User is valid");
    }

当 Spring Boot 发现一个参数注解了 @Valid 时,它会自动启动默认的 JSR 380 实现 - Hibernate Validator,并验证该参数。

当目标参数未能通过验证时,Spring Boot 会抛出一个 MethodArgumentNotValidException 异常。

5、@ExceptionHandler 注解

Spring Boot 能自动验证传递给 addUser() 方法的 User 对象,这确实很方便,但这一过程中缺少的是如何处理验证结果?

@ExceptionHandler 注解允许我们通过一个方法处理指定类型的异常。

因此,可以用它来处理验证错误:

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MethodArgumentNotValidException.class)
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;
}

如上,指定了 MethodArgumentNotValidException 异常作为要处理的异常。因此,当指定的 User 对象无效时,Spring Boot 将调用此方法。

该方法将每个无效字段的名称和验证后错误信息存储在 Map 中。然后,将 Map 以 JSON 表示形式发送回客户端,以便进一步处理。

6、测试 REST Controller

创建测试类,并注入 Mock UserRepository 接口实现、UserController 实例和 MockMvc 对象:

@RunWith(SpringRunner.class) 
@WebMvcTest
@AutoConfigureMockMvc
public class UserControllerIntegrationTest {

    @MockBean
    private UserRepository userRepository;
    
    @Autowired
    UserController userController;

    @Autowired
    private MockMvc mockMvc;

    //...
    
}

由于只测试 Web 层,因此使用 @WebMvcTest 注解。它允许我们使用 MockMvcRequestBuildersMockMvcResultMatchers 实现的静态方法集轻松测试请求和响应。

现在,在请求体中传递有效和无效的 User 对象来测试 addUser() 方法:

@Test
public void whenPostRequestToUsersAndValidUser_thenCorrectResponse() throws Exception {
    MediaType textPlainUtf8 = new MediaType(MediaType.TEXT_PLAIN, Charset.forName("UTF-8"));
    String user = "{\"name\": \"bob\", \"email\" : \"bob@domain.com\"}";
    mockMvc.perform(MockMvcRequestBuilders.post("/users")
      .content(user)
      .contentType(MediaType.APPLICATION_JSON_UTF8))
      .andExpect(MockMvcResultMatchers.status().isOk())
      .andExpect(MockMvcResultMatchers.content()
        .contentType(textPlainUtf8));
}

@Test
public void whenPostRequestToUsersAndInValidUser_thenCorrectResponse() throws Exception {
    String user = "{\"name\": \"\", \"email\" : \"bob@domain.com\"}";
    mockMvc.perform(MockMvcRequestBuilders.post("/users")
      .content(user)
      .contentType(MediaType.APPLICATION_JSON_UTF8))
      .andExpect(MockMvcResultMatchers.status().isBadRequest())
      .andExpect(MockMvcResultMatchers.jsonPath("$.name", Is.is("Name is mandatory")))
      .andExpect(MockMvcResultMatchers.content()
        .contentType(MediaType.APPLICATION_JSON_UTF8));
    }
}

也可以使用免费的 API 测试工具(如 Postman)来测试 REST Controller API。

7、运行示例应用

使用标准的 main() 方法运行示例项目:

@SpringBootApplication
public class Application {
    
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
    
    @Bean
    public CommandLineRunner run(UserRepository userRepository) throws Exception {
        return (String[] args) -> {
            User user1 = new User("Bob", "bob@domain.com");
            User user2 = new User("Jenny", "jenny@domain.com");
            userRepository.save(user1);
            userRepository.save(user2);
            userRepository.findAll().forEach(System.out::println);
        };
    }
}

你可以在控制台中看到打印出来的几个 User 对象。

使用有效的 User 对象向 http://localhost:8080/users 端点发出 POST 请求,将返回字符串 “User is valid”。

使用不含 nameemail 值的 User 对象进行 POST 请求,会返回以下响应:

{
  "name":"Name is mandatory",
  "email":"Email is mandatory"
}

8、总结

本文介绍了如何在 Spring Boot 中使用 @Valid 注解校验客户端提交的数据,以及如何处理校验失败的异常。


Ref:https://www.baeldung.com/spring-boot-bean-validation