当前位置:Java -> 使用Spring Boot 进行验证
在构建Spring Boot应用程序时,您需要验证Web请求的输入,服务的输入等。在本博客中,您将学习如何为Spring Boot应用程序添加验证。享受吧!
为了验证输入,将使用Jakarta Bean Validation规范。Jakarta Bean Validation规范是一个Java规范,允许您通过注解验证输入、模型等。规范的实现之一是Hibernate Validator。使用Hibernate Validator并不意味着您也会使用Hibernate ORM(对象关系映射)。它只是Hibernate旗下的另一个项目。在Spring Boot中,您可以添加spring-boot-starter-validation依赖项,该依赖项使用Hibernate Validator进行验证。
在本博客的其余部分,您将创建一个基本的Spring Boot应用程序,并在控制器和服务中添加验证。
本博客使用的资源可以在GitHub上找到:GitHub。
本博客的先决条件包括:
在本博客中要构建的项目是一个基本的Spring Boot项目。该领域是一个具有id、firstName和lastName的Customer。
public class Customer {
private Long customerId;
private String firstName;
private String lastName;
...
}
通过Rest API,可以创建和检索客户。为了使API规范和源代码保持一致,您将使用openapi-generator-maven-plugin。首先,编写OpenAPI规范,插件将根据规范为您生成源代码。OpenAPI规范包含两个端点,一个用于创建客户(POST),一个用于检索客户(GET)。OpenAPI规范包含一些约束:
openapi: "3.1.0"
info:
title: API Customer
version: "1.0"
servers:
- url: https://localhost:8080
tags:
- name: Customer
description: Customer specific data.
paths:
/customer:
post:
tags:
- Customer
summary: Create Customer
operationId: createCustomer
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Customer'
responses:
'200':
description: OK
content:
'application/json':
schema:
$ref: '#/components/schemas/CustomerFullData'
/customer/{customerId}:
get:
tags:
- Customer
summary: Retrieve Customer
operationId: getCustomer
parameters:
- name: customerId
in: path
required: true
schema:
type: integer
format: int64
responses:
'200':
description: OK
content:
'application/json':
schema:
$ref: '#/components/schemas/CustomerFullData'
'404':
description: NOT FOUND
components:
schemas:
Customer:
type: object
properties:
firstName:
type: string
description: First name of the customer
minLength: 1
maxLength: 20
lastName:
type: string
description: Last name of the customer
minLength: 1
maxLength: 20
CustomerFullData:
allOf:
- $ref: '#/components/schemas/Customer'
- type: object
properties:
customerId:
type: integer
description: The ID of the customer
format: int64
description: Full data of the customer.
生成的代码会生成一个接口,CustomerController实现该接口。
@RestController
class CustomerController implements CustomerApi {
private final CustomerService customerService;
CustomerController(CustomerService customerService) {
this.customerService = customerService;
}
@Override
public ResponseEntity<CustomerFullData> createCustomer(Customer apiCustomer) {
com.mydeveloperplanet.myvalidationplanet.domain.Customer customer = new com.mydeveloperplanet.myvalidationplanet.domain.Customer();
customer.setFirstName(apiCustomer.getFirstName());
customer.setLastName(apiCustomer.getLastName());
return ResponseEntity.ok(domainToApi(customerService.createCustomer(customer)));
}
@Override
public ResponseEntity<CustomerFullData> getCustomer(Long customerId) {
com.mydeveloperplanet.myvalidationplanet.domain.Customer customer = customerService.getCustomer(customerId);
return ResponseEntity.ok(domainToApi(customer));
}
private CustomerFullData domainToApi(com.mydeveloperplanet.myvalidationplanet.domain.Customer customer) {
CustomerFullData cfd = new CustomerFullData();
cfd.setCustomerId(customer.getCustomerId());
cfd.setFirstName(customer.getFirstName());
cfd.setLastName(customer.getLastName());
return cfd;
}
}
CustomerService将客户放入Map中,不使用数据库或其他内容。
@Service
class CustomerService {
private final HashMap<Long, Customer> customers = new HashMap<>();
private Long index = 0L;
Customer createCustomer(Customer customer) {
customer.setCustomerId(index);
customers.put(index, customer);
index++;
return customer;
}
Customer getCustomer(Long customerId) {
if (customers.containsKey(customerId)) {
return customers.get(customerId);
} else {
return null;
}
}
}
构建应用程序并运行测试。
$ mvn clean verify
现在,控制器验证非常简单。只需将spring-boot-starter-validation依赖项添加到您的pom.xml中即可。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
仔细查看生成的CustomerApi接口,该接口位于target/generated-sources/openapi/src/main/java/com/mydeveloperplanet/myvalidationplanet/api/。
@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2024-03-30T09:31:30.793931181+01:00[Europe/Amsterdam]")
@Validated
@Tag(name = "Customer", description = "Customer specific data.")
public interface CustomerApi {
...
default ResponseEntity<CustomerFullData> createCustomer(
@Parameter(name = "Customer", description = "") @Valid @RequestBody(required = false) Customer customer
) {
...
}
...
default ResponseEntity<CustomerFullData> getCustomer(
@Parameter(name = "customerId", description = "", required = true, in = ParameterIn.PATH) @PathVariable("customerId") Long customerId
) {
...
}
...
}
很酷的一点是,基于OpenAPI规范,生成的代码中会自动放置正确的注释。您无需执行任何特殊操作即可为您的Rest API添加验证。
让我们测试一下验证是否正常工作。仅测试控制器,服务被模拟,并且您将使用@WebMvcTest注解以将测试切片到最小。
@WebMvcTest(controllers = CustomerController.class)
class CustomerControllerTest {
@MockBean
private CustomerService customerService;
@Autowired
private MockMvc mvc;
@Test
void whenCreateCustomerIsInvalid_thenReturnBadRequest() throws Exception {
String body = """
{
"firstName": "John",
"lastName": "John who has a very long last name"
}
""";
mvc.perform(post("/customer")
.contentType("application/json")
.content(body))
.andExpect(status().isBadRequest());
}
@Test
void whenCreateCustomerIsValid_thenReturnOk() throws Exception {
String body = """
{
"firstName": "John",
"lastName": "Doe"
}
""";
Customer customer = new Customer();
customer.setCustomerId(1L);
customer.setFirstName("John");
customer.setLastName("Doe");
when(customerService.createCustomer(any())).thenReturn(customer);
mvc.perform(post("/customer")
.contentType("application/json")
.content(body))
.andExpect(status().isOk())
.andExpect(jsonPath("firstName", equalTo("John")))
.andExpect(jsonPath("lastName", equalTo("Doe")))
.andExpect(jsonPath("customerId", equalTo(1)));
}
@Test
void whenGetCustomerIsInvalid_thenReturnBadRequest() throws Exception {
mvc.perform(get("/customer/abc"))
.andExpect(status().isBadRequest());
}
@Test
void whenGetCustomerIsValid_thenReturnOk() throws Exception {
Customer customer = new Customer();
customer.setCustomerId(1L);
customer.setFirstName("John");
customer.setLastName("Doe");
when(customerService.getCustomer(any())).thenReturn(customer);
mvc.perform(get("/customer/1"))
.andExpect(status().isOk());
}
}
向服务添加验证需要更多工作,但仍然非常简单。
将验证约束添加到模型中。验证约束的完整列表可以在Hibernate Validator文档中找到。
添加以下约束:
public class Customer {
private Long customerId;
@Size(min = 1, max = 20)
@NotEmpty
private String firstName;
@Size(min = 1, max = 20)
@NotEmpty
private String lastName;
...
}
为了在服务中启用验证,有两种方法。一种方法是在服务中注入Validator
并显式验证客户。这个Validator
由Spring Boot提供。如果发现违例,你可以创建一个错误消息。
@Service
class CustomerService {
private final HashMap<Long, Customer> customers = new HashMap<>();
private Long index = 0L;
private final Validator validator;
CustomerService(Validator validator) {
this.validator = validator;
}
Customer createCustomer(Customer customer) {
Set<ConstraintViolation<Customer>> violations = validator.validate(customer);
if (!violations.isEmpty()) {
StringBuilder sb = new StringBuilder();
for (ConstraintViolation<Customer> constraintViolation : violations) {
sb.append(constraintViolation.getMessage());
}
throw new ConstraintViolationException("Error occurred: " + sb, violations);
}
customer.setCustomerId(index);
customers.put(index, customer);
index++;
return customer;
}
...
}
为了测试服务的验证,你需要添加@SpringBootTest
注解。缺点是这将是一个代价高昂的测试,因为它会启动一个完整的Spring Boot应用程序。添加了两个测试:
lastName
创建客户时是否会抛出ConstraintViolationException
;@SpringBootTest
class CustomerServiceTest {
@Autowired
private CustomerService customerService;
@Test
void whenCreateCustomerIsInvalid_thenThrowsException() {
Customer customer = new Customer();
customer.setFirstName("John");
customer.setLastName("John who has a very long last name");
assertThrows(ConstraintViolationException.class, () -> {
customerService.createCustomer(customer);
});
}
@Test
void whenCreateCustomerIsValid_thenCustomerCreated() {
Customer customer = new Customer();
customer.setFirstName("John");
customer.setLastName("Doe");
Customer customerCreated = customerService.createCustomer(customer);
assertNotNull(customerCreated.getCustomerId());
}
}
添加验证的第二种方法与控制器使用的方法相同。你在类级别添加@Validated
注解,以及在要验证的参数上添加@Valid
注解。
@Service
@Validated
class CustomerValidatedService {
private final HashMap<Long, Customer> customers = new HashMap<>();
private Long index = 0L;
Customer createCustomer(@Valid Customer customer) {
customer.setCustomerId(index);
customers.put(index, customer);
index++;
return customer;
}
...
}
当标准验证器不足以满足你的需求时,你可以创建自己的验证器。让我们为荷兰邮政编码创建一个自定义验证器。荷兰邮政编码由4位数字后跟两个字符组成。
首先,你需要创建自己的约束注解。在这种情况下,你只需指定要使用的约束违例消息。
@Target({ FIELD })
@Retention(RUNTIME)
@Constraint(validatedBy = DutchZipcodeValidator.class)
@Documented
public @interface DutchZipcode {
String message() default "A Dutch zipcode must contain 4 digits followed by two letters";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
注解由类DutchZipcodeValidator
验证。这个类实现了ConstraintValidator
。isValid
方法用于实现检查并返回输入是否有效。在这种情况下,检查是通过正则表达式实现的。
public class DutchZipcodeValidator implements ConstraintValidator<DutchZipcode, String> {
@Override
public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
Pattern pattern = Pattern.compile("\\b\\d{4}\\s?[a-zA-Z]{2}\\b");
Matcher matcher = pattern.matcher(s);
return matcher.matches();
}
}
为了使用新的约束,你需要添加一个新的Address
领域实体,其中包含一个street
和一个zipcode
。zipcode
使用@DutchZipcode
进行注释。
可以通过基本单元测试来测试新的约束。
class ValidateDutchZipcodeTest {
@Test
void whenZipcodeIsValid_thenOk() {
Address address = new Address("street", "2845AA");
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
Set<ConstraintViolation<Address>> violations = validator.validate(address);
assertTrue(violations.isEmpty());
}
@Test
void whenZipcodeIsInvalid_thenNotOk() {
Address address = new Address("street", "2845");
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
Set<ConstraintViolation<Address>> violations = validator.validate(address);
assertFalse(violations.isEmpty());
}
}
如果你仔细定义你的OpenAPI规范并通过openapi-generator-maven-plugin
生成代码,向控制器添加验证几乎是免费的。通过有限的努力,你也可以向服务添加验证。Spring Boot使用的Hibernate Validator提供了相当多的约束供使用,如果必要,你也可以创建自己的自定义约束。
推荐阅读: 8.已知某个文件内包含一些电话号码,每个号码为 8 位数字,统计不同号码的个数。
本文链接: 使用Spring Boot 进行验证