一、概述
在现代软件开发中,模块化和可重用性是构建高效、可维护应用程序的关键。对于基于Spring Boot的应用程序来说,创建一个自定义校验注解并将其封装为一个启动器(starter),不仅可以提高代码的重用性和简化集成过程,还能确保项目之间的标准化和一致性。
二、封装自定义校验注解的意义
为什么将自定义校验注解封装为一个启动器?
将自定义校验逻辑封装成一个独立的启动器,可以带来诸多优势:
1. 模块化和重用性
通过将自定义校验逻辑打包成一个启动器,你可以轻松地将其作为Maven或Gradle依赖项添加到多个项目中。这不仅提高了代码的重用性,还简化了维护工作,使得相同的功能可以在不同的微服务或项目之间共享,而无需重复编写代码。
2. 易于集成
一个精心设计的启动器包含了所有必要的依赖关系、配置和自动配置类,使得其他开发者能够以最小的努力将你的校验功能集成到他们的项目中。只需一行依赖声明,即可获得完整的校验功能支持,这对于快速开发和迭代非常有利。
3. 标准化和一致性
在一个组织内,如果多个团队使用Spring Boot构建应用程序,创建一个标准的校验启动器可以帮助确保所有项目遵循相同的数据验证规则和标准。这样可以提高数据的一致性和可靠性,减少由于不同实现带来的潜在问题,从而提升整体项目的质量。
4. 可维护性和更新
当需要对校验逻辑进行修改或扩展时,只需要更新启动器中的代码,然后通知所有使用该启动器的项目进行版本升级。这种方式使得维护更加集中和高效,避免了跨多个项目进行相同更改的繁琐过程,节省了大量的时间和资源。
5. 简化配置
利用Spring Boot的自动配置特性,你可以在启动器中提供默认配置,从而减少使用者在自己的项目中配置这些细节的工作量。例如,可以预设一些常用的校验规则,用户可以根据需要轻松覆盖这些默认设置,灵活度更高。
6. 社区贡献和开源
如果你认为你的自定义校验逻辑可能对更广泛的开发者社区有用,可以选择将它作为一个开源启动器发布。这不仅可以帮助其他开发者解决问题,还可以从社区获取宝贵的反馈,进一步改进和完善你的库,形成良性循环。
总结而言,将自定义校验注解和校验器封装为一个启动器,不仅能提高代码的模块化和重用性,还能简化集成过程,确保标准化和一致性,同时方便维护和更新。此外,这也是一个很好的方式来分享你的工作,贡献给开源社区,共同推动技术的发展。
三、封装为启动器(Starter)
在Spring Boot中,自定义校验注解可以让你更加灵活地对数据进行验证。通过使用Hibernate Validator(JSR 380的实现),你可以创建自己的校验逻辑,并将其应用于你的实体类中。
封装自定义 校验注解
和 校验器
的 SpringBoot 场景启动器:validation-spring-boot-starter
。本文简述了封装方法和启用后的效果。
经过测试,本启动器不需要使用自动配置类(AutoConfiguration)。只要在需要校验的实体字段上添加注解,就能够正常启用校验功能。
1. POM
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.6</version>
</parent>
<artifactId>hello-validation-spring-boot-starter</artifactId>
<version>1.0.0</version>
<name>hello-validation-spring-boot-starter</name>
<description>参数校验启动器</description>
<properties>
<java.version>21</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
</dependencies>
</project>
2. 自定义校验注解和校验器
首先,你需要定义一个注解,这个注解将用于标记需要进行特定验证的数据字段。
package com.example.hello.validation.phone;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* 手机号码:是格式正确的手机号码。
* <p>
* {@code null} 或 {@code 空字符串},能够通过校验。
* <p>
*/
@Target({FIELD})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = MobilePhoneValidator.class)
public @interface MobilePhone {
String message() default "手机号码格式错误";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
接下来,你需要实现 ConstraintValidator<A,T>
接口,其中A是你创建的自定义注解类型,而T是被校验的数据类型。
package com.example.hello.validation.phone;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import java.util.regex.Pattern;
/**
* 手机号码格式校验器
*/
public class MobilePhoneValidator implements ConstraintValidator<MobilePhone, String> {
private static final Pattern PATTERN = Pattern.compile("^1[3-9]\\d{9}$");
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null || value.isEmpty()) {
return true;
}
return isMobilePhone(value);
}
/**
* 是手机号码
*/
private boolean isMobilePhone(CharSequence input) {
return PATTERN.matcher(input).matches();
}
}
3. 使用自定义校验注解
package com.example.hello_common.model.param;
import com.example.hello.validation.phone.MobilePhone;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
@Schema(description = "用户参数")
public class UserParam {
@NotBlank(message = "用户名不能为空")
@Schema(description = "用户名", example = "张三")
private String name;
@NotBlank(message = "手机号码不能为空")
@MobilePhone
@Schema(description = "手机号码", example = "18612345678")
private String mobilePhone;
private String email;
private String zipCode;
private String idCard;
}
4. 启用参数校验
控制器方法参数前添加 @Valid
。
package com.example.test.validation.web.user.controller;
import com.example.hello_common.model.param.UserParam;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
@RequestMapping("/users")
@Tag(name = "用户管理")
public class UserController {
@PostMapping
@Operation(summary = "新增用户")
public void addUser(@Valid @RequestBody UserParam userParam) {
log.info("新增用户。userParam={}", userParam);
}
}
5. 校验效果
接口返回
{
"timestamp": "2024-12-02T15:43:28.827+00:00",
"status": 400,
"error": "Bad Request",
"path": "/users"
}
报错日志
2024-12-02T23:43:28.824+08:00 WARN 10652 — [hello-test-validation] [nio-8080-exec-6] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public void com.example.test.validation.web.user.controller.UserController.addUser(com.example.hello_common.model.param.UserParam): [Field error in object ‘userParam’ on field ‘mobilePhone’: rejected value [123]; codes [MobilePhone.userParam.mobilePhone,MobilePhone.mobilePhone,MobilePhone.java.lang.String,MobilePhone]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [userParam.mobilePhone,mobilePhone]; arguments []; default message [mobilePhone]]; default message [手机号码格式错误]] ]
MethodArgumentNotValidException 和 BindException
6. 异常统一处理
package com.example.test.validation.core.exception;
import com.example.hello_common.response.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* 异常统一处理
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler
public Result<Void> handle(BindException bindException) {
log.info(bindException.getMessage());
String message = bindException.getFieldErrors().stream()
.map(FieldError::getDefaultMessage)
.filter(Objects::nonNull) // 防止 NullPointerException
.collect(Collectors.joining(","));
return Result.fail(message);
}
}
7. 返回统一的API响应格式
为了保持API的一致性,你可以创建一个通用的响应对象,用于封装成功和失败的响应信息。
package com.example.hello_common.response;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 响应封装实体
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> {
private Integer code;
private String message;
private T data;
public static <T> Result<T> success(T data) {
return new Result<>(ResultEnum.SUCCESS.getCode(), ResultEnum.SUCCESS.getMessage(), data);
}
public static <T> Result<T> fail(String message) {
return new Result<>(ResultEnum.FAIL.getCode(), message, null);
}
}