手记

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平台并检查一下一切是否都正常运作。

编程快乐!

0人推荐
随时随地看视频
慕课网APP