Spring Boot自定义验证器的创建指南
Spring Boot 的 Bean 验证功能做了很多工作来帮我们,但在某些特定情况下,我们需要为特定的业务逻辑设置自定义的验证规则。
例如,有一个内置的 @Email
验证,但我们还需要验证电话号码,。或者我们想检查实体是否已经在数据库中存在,而不是让服务层或仓储层来进行验证。这样更符合单一职责的设计理念。
Spring Boot 还提供了定义自定义验证规则的功能,这真是太幸运了。
搭建一个 Spring Boot 示例项目我将展示我在我的演示 Spring Modulith 项目中集成的一些自定义验证规则。源代码可以在 GitHub 上找到(https://github.com/des-felins/spring-modulith-demo)。如果你对构建模块化的 Spring Boot 应用程序感兴趣的话,欢迎阅读这篇文章系列(https://bell-sw.com/blog/what-is-spring-modulith-introduction-to-modular-monoliths/?utm_source=medium&utm_medium=post&utm_campaign=edelveismedium&utm_content=validators)。
你可以拉取代码库并试一试,看看自定义验证在包含模块、21点和DTO的项目中是如何工作的。
你也可以创建一个基础的Spring Boot应用,然后一起跟我编程。
需要的前提条件如下:
- 已经安装了 Java 21。因为如果 Spring 推荐了一个运行时环境,为什么不使用它呢?
- 使用 Spring Initializr 生成的示例 Spring Boot 应用程序。选择最新的稳定版本(例如:3.3),Java 21、Maven 和 jar。添加了几个依赖。
- 你喜欢的 IDE。
生成项目并在您的IDE中打开该项目。我们需要添加一个依赖项来验证电话号码:libphonenumber由Google开发:
<dependency>
<groupId>com.googlecode.libphonenumber</groupId>
<artifactId>libphonenumber</artifactId>
<version>8.13.39</version>
</dependency>
创建一个我们要用来实验的 Customer
实体,
@Entity
@Table(name = "customer")
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank
private String name;
@NotBlank
private String phoneNumber;
@NotBlank
@Email
private String 电子邮件;
//客户信息类
//构造器, Getter, Setter, equals, hashCode
}
CustomerRepository(客户库)将继承JpaRepository
这里有一个名为CustomerRepository的公共接口,它继承了JpaRepository<Customer, Long>。这个接口有一个方法叫findByPhoneNumber,该方法接收一个字符串参数,用于通过电话号码查找客户。
`CustomerService` 负责 `Customer` 的增删改查操作
@Service
@RequiredArgsConstructor
/** 客户服务类,主要负责处理与客户相关的数据操作 */
public class CustomerService {
/** 定义一个最终的客户仓库,用于存储客户数据 */
private final CustomerRepository repository;
/** 保存客户信息的方法,接收一个新客户对象并调用仓库中的保存方法 */
public Customer saveCustomer(Customer newCustomer) {
return repository.save(newCustomer);
}
}
最后,CustomerController
实现 RestController
,并作为用户和应用之间的桥梁,
@RestController
@RequestMapping("/api")
@Validated
public class CustomerController {
private final CustomerService service;
@Autowired
public CustomerController(CustomerService service) {
this.service = service;
}
@PostMapping("/customers")
@ResponseStatus(HttpStatus.CREATED)
public ResponseEntity<Customer> createCustomer(
@NotNull
@Valid
@RequestBody
Customer customer) {
return ResponseEntity.ofNullable(service.saveCustomer(customer));
}
}
请注意 @Validated
注解:自 Spring Boot 3.1 起,它需要以显示自定义的验证错误信息。
我们来创建两个验证器:一个用于检查输入的电话号码是否有效,另一个用于检查是否存在使用这个电话号码的客户。
正确手机号码的验证规则首先,创建一个名为 CorrectNumber
的 @interface
,来定义我们的自定义注解。
@Constraint(validatedBy = CorrectPhoneValidator.class)
@Target({ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface 有效电话 {
String message() default "请输入有效的电话号码。";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
这个 @interface
包含以下元注解:
@Constraint
声明了将实现我们接口并定义其行为的这个类,@Target
定义了我们自定义注解可以应用到哪些程序元素上,@Retention
定义了我们的注解在什么时候是可用的。
此外,界面还包括当约束被违背时的默认错误消息,constraint payload,和组属性。后两个应该默认为空数组。
下一步是定义一个具体实现 ConstraintValidator
接口的类,该类将接受我们的注解并验证相应元素,因此我们创建一个名为 CorrectPhoneValidator
的类:
public class 电话号码验证器 implements ConstraintValidator<电话号码, String> {
private final Logger 日志记录器 = LoggerFactory.getLogger(电话号码验证器.class);
@Override
public boolean 验证是否有效(String 电话号码, ConstraintValidatorContext 验证器上下文) {
PhoneNumberUtil 电话号码工具 = PhoneNumberUtil.getInstance();
Phonenumber.PhoneNumber 电话;
if (电话号码 == null) return true;
try {
电话 = 电话号码工具.parse(电话号码, Phonenumber.CountryCodeSource.UNSPECIFIED);
return 电话号码工具.isValidNumber(电话);
} catch (NumberParseException e) {
日志记录器.error(e.getMessage());
return false;
}
}
}
需要实现 ConstraintValidator
接口,其中必须包含 isValid
方法,在 isValid
方法中定义验证逻辑。在这种情况下,我们利用 libphonenumber 库提供的 PhoneNumberUtil
来验证作为字符串提供的电话号码的有效性。
这种方法会检查以正号(+)开头的电话号码,也可以通过提供国家代码来检查特定国家的电话号码。
现在您可以使用我们的新注解标注phoneNumber
字段。
@非空
@手机号码格式正确
private String 电话号码;
使用依赖注入来确保电话号码的唯一性,进行验证
ConstraintValidator
实现也可以包含 @Autowired
依赖。此外,不仅仅验证单个字段,还可以传入一个对象来验证相关字段。我们可以利用这功能,创建一个校验唯一电话号码(数据库中不存在的号码)的校验器。
首先,创建一个名为 UniquePhone
的 @interface
。这你应该清楚:
@Constraint(validatedBy = UniquePhoneValidator.class)
@Target({ ElementType.PARAMETER, ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface UniquePhone {
String message() default "这个电话号码已有客户使用。";
Class<?>[] groups() default {};
Class<? extends Payload> []payload() default {};
}
接下来,创建名为UniquePhoneValidator
的实现。这里,ConstraintValidator
指特定的验证器接口。
@RequiredArgsConstructor
public class UniquePhoneValidator implements ConstraintValidator<UniquePhone, Customer> {
// 独特电话验证器
private final CustomerRepository repository;
@Override
public boolean isValid(Customer customer, ConstraintValidatorContext constraintValidatorContext) {
// 空值条件:如果客户为空,则返回true
if (customer == null) return true;
return repository.findByPhoneNumber(customer.getPhoneNumber()).isEmpty();
}
}
在这里,我们将仓库注入并传入了一个 Customer 对象来检查是否有此电话号码的客户。
我们现在可以为控制器类中的 createCustomer
方法的参数进行标注。
@PostMapping("/customers")
@ResponseStatus(HttpStatus.CREATED)
public ResponseEntity<Customer> createCustomer(
@NotNull
@Valid
@RequestBody
@UniquePhone
Customer customer) {
// 创建一个新的客户
return ResponseEntity.ofNullable(service.saveCustomer(customer));
}
创建一个异常处理程序
我们的自定义验证器目前运作良好,但如果输入了无效数据,用户将无法看到清晰的错误提示信息。相反,用户会看到一个默认的错误页面,我们当然不希望这样,不是吗?
因此,我们可以创建一个 ErrorHandler
类来统一处理验证时出现的异常。
请注意,这里有两种异常情况需要处理。
- 当带有
@Valid
注解的参数验证失败时,会抛出 MethodArgumentNotValidException。 - 在这种情况下,当一个操作违反了对存储库结构施加的约束时,会抛出 ConstraintViolationException。例如,当用户尝试保存一个数据库中已存在的客户电话号码时,将会抛出此异常。
这意味着我们要处理两种类型的异常。
让我们创建一个 @RestControllerAdvice
注解的 ErrorHandler
类:
@RestControllerAdvice
public class ErrorHandler {
// 错误处理类,用于处理请求中的验证异常
// @ResponseStatus(HttpStatus.BAD_REQUEST)
// @ExceptionHandler(MethodArgumentNotValidException.class)
public Map<String, String> handleMethodArgumentNotValidExceptions(
MethodArgumentNotValidException ex) {
// 创建一个存储错误信息的Map
Map<String, String> errors = new HashMap<>();
// 遍历所有验证错误,将错误信息添加到Map中
ex.getBindingResult().getAllErrors().forEach((error) -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});
return errors;
}
// @ResponseStatus(HttpStatus.BAD_REQUEST)
// @ExceptionHandler(ConstraintViolationException.class)
public List<String> handleConstraintValidationExceptions(ConstraintViolationException ex) {
List<String> errors = new ArrayList<>();
// 遍历所有约束验证错误,将错误信息添加到列表中
for (ConstraintViolation<?> violation : ex.getConstraintViolations()) {
errors.add(violation.getMessage());
}
return errors;
}
}
handleMethodArgumentNotValidExceptions()
方法让我们能够获取一个映射,该映射以无效字段的名称为键,以错误信息为值。并将该映射以 JSON 格式返回给客户端。
handleConstraintValidationExceptions()
方法从所有相关的 ConstraintViolators 中获取所有错误消息的列表。
就这样!所有的约束都已设置好,异常都已捕获,消息都已发送!你可以运行你的小程序了,打开你最常用的API平台并检查一下一切是否都正常运作。
编程快乐!
共同学习,写下你的评论
评论加载中...
作者其他优质文章