参数校验
1.1概述
本文使用 Hibernate Validator 框架对 RESTful API 接口的参数进行校验,确保数据入库的正确性。
例如,在用户注册时,校验手机号格式、密码强度等。如果校验失败,抛出
ConstraintViolationException 或相关异常,由 GlobalExceptionHandler 捕获,返回标准化的 CommonResult 响应,格式如下:
{
"code": 400,
"data": null,
"msg": "请求参数不正确:密码不能为空"
}
1.2 参数校验注解
Hibernate Validator 提供 20+ 个内置校验注解,文档将其分为常用和不常用两类:
常用注解
注解 | 功能 |
---|---|
@NotBlank | 用于字符串,确保非 null 且 trim() 后长度大于 0 |
@NotEmpty | 用于集合、字符串,确保非 null 且非空 |
@NotNull | 确保非 null |
@Pattern(value) | 符合指定正则表达式 |
@Max(value) | 值小于或等于指定值 |
@Min(value) | 值大于或等于指定值 |
@Range(min, max) | 值在指定范围内 |
@Size(max, min) | 集合、字符串等大小在范围内 |
@Length(max, min) | 字符串长度在范围内 |
@AssertTrue | 值为 true |
@AssertFalse | 值为 false |
符合邮箱格式 | |
@URL | 符合 URL 格式 |
不常用注解
注解 | 功能 |
---|---|
@Null | 必须为 null |
@DecimalMax(value) | 数字小于或等于指定值 |
@DecimalMin(value) | 数字大于或等于指定值 |
@Digits(integer, fraction) | 数字在指定位数范围内 |
@Positive | 正数 |
@PositiveOrZero | 正数或 0 |
@Negative | 负数 |
@NegativeOrZero | 负数或 0 |
@Future | 未来日期 |
@FutureOrPresent | 现在或未来日期 |
@Past | 过去日期 |
@PastOrPresent | 现在或过去日期 |
@SafeHtml | 安全的 HTML 内容 |
1.3 参数校验使用过程
文档提到,只需三步即可启用参数校验:
第零步:引入依赖
- 项目默认引入 spring-boot-starter-validation,无需手动添加:
xml
Copy
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency>
- 提供 Hibernate Validator 的核心功能。
第一步:在类上添加 @Validated
- 在需要校验的类(如 Controller 或 Service)上添加 @Validated 注解,启用校验:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency>
- 作用:通知 Spring 在方法调用时对参数进行校验。
- 注意:Service 层也需校验,因为 Service 可能被其他 Service 调用,参数可能不正确。
第二步:添加校验注解
分为两种情况:
情况一:Bean 类型参数
- 在方法参数上添加 @Valid,在 Bean 属性上添加校验注解:
// Controller 示例 @Validated @RestController public class AuthController { // ... } // Service 示例(实现类) @Service @Validated public class AdminAuthServiceImpl implements AdminAuthService { // ... }
- 解析:
- @Valid:触发对 AuthLoginReqVO 对象的属性校验。
- @NotEmpty:确保字段非 null 且非空字符串。
- @Length:限制字符串长度在 4-16 位。
- @Pattern:确保用户名只包含字母和数字。
- 如果校验失败,抛出 MethodArgumentNotValidException,由 GlobalExceptionHandler 处理。
情况二:普通类型参数
- 直接在方法参数上添加校验注解:
// Controller 示例 @Validated @RestController public class DictDataController { @GetMapping("/get") public CommonResult<DictDataRespVO> getDictData(@RequestParam("id") @NotNull(message = "编号不能为空") Long id) { // ... } } // Service 接口示例 public interface DictDataService { DictDataDO getDictData(@NotNull(message = "编号不能为空") Long id); }
- 解析:
- @NotNull:确保 id 非 null。
- 校验失败抛出 ConstraintViolationException,由 GlobalExceptionHandler 的 constraintViolationExceptionHandler 捕获:
@ExceptionHandler(value = ConstraintViolationException.class) public CommonResult<?> constraintViolationExceptionHandler(ConstraintViolationException ex) { log.warn("[constraintViolationExceptionHandler]", ex); ConstraintViolation<?> constraintViolation = ex.getConstraintViolations().iterator().next(); return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", constraintViolation.getMessage())); }
第三步:测试校验效果
- 启动项目,调用 API(如 /login),故意漏填参数(如 username),检查响应:
{ "code": 400, "data": null, "msg": "请求参数不正确:登录账号不能为空" }
- 验证:确认 GlobalExceptionHandler 正确捕获异常并返回标准响应。
1.4 自定义校验注解
当内置注解不足以满足需求时,可自定义校验注解。文档以 @Mobile 注解为例:
第一步:定义 @Mobile 注解
@Target({
ElementType.METHOD,
ElementType.FIELD,
ElementType.ANNOTATION_TYPE,
ElementType.CONSTRUCTOR,
ElementType.PARAMETER,
ElementType.TYPE_USE
})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(
validatedBy = MobileValidator.class
)
public @interface Mobile {
String message() default "手机号格式不正确";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
- 解析:
- @Target:指定注解可用于字段、方法、参数等。
- @Retention:运行时保留,供校验器读取。
- @Constraint:绑定校验器 MobileValidator。
- message:自定义错误提示。
第二步:实现 MobileValidator
public class MobileValidator implements ConstraintValidator<Mobile, String> {
@Override
public void initialize(Mobile annotation) {
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
// 如果手机号为空,默认不校验,即校验通过
if (StrUtil.isEmpty(value)) {
return true;
}
// 校验手机
return ValidationUtils.isMobile(value);
}
}
- 解析:
- 实现 ConstraintValidator<Mobile, String>,校验字符串类型的手机号。
- isValid:检查是否符合手机号格式(通过 ValidationUtils.isMobile,推测为正则匹配)。
- 允许空值通过,符合业务需求。
第三步:使用 @Mobile
@Data
public class AppAuthLoginReqVO {
@NotEmpty(message = "手机号不能为空")
@Mobile // 应用自定义注解
private String mobile;
}
- 解析:
- @Mobile 校验 mobile 是否符合手机号格式。
- 校验失败抛出 ConstraintViolationException,由 GlobalExceptionHandler 处理。
1.5 校验异常处理
- 异常类型:
- Bean 参数校验失败:抛出 MethodArgumentNotValidException 或 BindException。
- 普通参数校验失败:抛出 ConstraintViolationException。
- 处理流程:
- GlobalExceptionHandler 捕获这些异常,转换为 CommonResult:
@ExceptionHandler(MethodArgumentNotValidException.class) public CommonResult<?> methodArgumentNotValidExceptionExceptionHandler(MethodArgumentNotValidException ex) { log.warn("[methodArgumentNotValidExceptionExceptionHandler]", ex); String errorMessage = ex.getBindingResult().getFieldError().getDefaultMessage(); return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", errorMessage)); }
- 返回 400 状态码和用户友好的错误消息。
- GlobalExceptionHandler 捕获这些异常,转换为 CommonResult:
1.6 WebSocket 场景
- 校验方式:WebSocket 握手请求(HTTP GET)通过查询参数传递数据,可使用普通参数校验:
@Validated
@RestController
public class WebSocketController {
@GetMapping("/ws/connect")
public CommonResult<?> connect(@RequestParam("token") @NotEmpty(message = "token 不能为空") String token) {
// ...
}
}
- 异常处理:同 RESTful API,由 GlobalExceptionHandler 处理,返回 CommonResult。
- 注意:WebSocket 消息(非握手)通常不直接使用 Hibernate Validator,需手动校验或在 Service 层处理。
二、时间传参
2.1 概述
项目对时间参数的传递和响应有明确规范,根据请求类型(Query 或 Request Body)使用不同格式,响应通常以 Long 时间戳为主。以下分 Query、Request Body 和 Response Body 三部分说明。
2.2 Query 时间传参
适用于 GET 请求或 POST 的 form-data 请求。
后端代码
- 使用 @DateTimeFormat 指定时间格式:
@Data
public class JobLogPageReqVO {
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") // 指定格式
private LocalDateTime beginTime;
}
@Data
public class UserPageReqVO {
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") // 数组形式
private LocalDateTime[] createTime;
}
- 解析:
- @DateTimeFormat:将字符串(如 2025-05-14 09:47:00)解析为 LocalDateTime。
- pattern:定义格式为 yyyy-MM-dd HH:mm:ss,与前端一致。
- 数组形式支持时间范围查询(如开始和结束时间)。
前端代码
- 前端传递格式为 yyyy-MM-dd HH:mm:ss:
- 示例(views/infra/job/logger/index.vue):
-
// 单个时间传参 beginTime: '2025-05-14 09:47:00'
- 示例(views/system/user/index.vue):
// 多个时间传参(范围) createTime: ['2025-05-14 00:00:00', '2025-05-14 23:59:59']
- 解析:前端通过查询参数(如 ?beginTime=2025-05-14+09:47:00)或 form-data 提交。
校验
- 可添加校验注解:
@NotNull(message = "开始时间不能为空") @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime beginTime;
- 格式错误抛出 HttpMessageNotReadableException,由 GlobalExceptionHandler 处理。
2.3 Request Body 时间传参
适用于 POST、PUT 请求的 JSON 格式。
后端代码
- 使用 @RequestBody 接收 LocalDateTime:
@Data public class TenantCreateReqVO { @NotNull(message = "过期时间不能为空") private LocalDateTime expireTime; } @PostMapping("/create") public CommonResult<?> createTenant(@RequestBody TenantCreateReqVO reqVO) { // ... }
- 解析:
- 不需显式 @DateTimeFormat,因为 JSON 使用 Long 时间戳。
- LocalDateTime 通过自定义反序列化器处理。
前端代码
- 传递 Long 时间戳:
- 示例(views/system/tenant/TenantForm.vue):
expireTime: 1744558020000 // 对应 2025-05-14 09:47:00
- 示例(views/system/tenant/TenantForm.vue):
- 解析:前端将时间转换为毫秒时间戳,符合后端预期。
2.4 Response Body 时间响应
- LocalDateTime 字段序列化为 Long 时间戳:
@Data public class TenantRespVO { private LocalDateTime createTime; }
- 响应示例:
{ "code": 0, "data": { "createTime": 1744558020000 }, "msg": "success" }
2.5 自定义 JSON 时间格式
作用范围(前端 POST/PUT 请求发送 JSON 数据时,包含 LocalDateTime
字段,后端返回包含 LocalDateTime
的 Java 对象时),上文Request Body 时间传参 和 Response Body 时间响应 的处理主要依赖于 自定义 JSON 时间格式,而 Query 时间传参 则依赖 @DateTimeFormat 注解来解析字符串格式的时间。
为什么使用 Long 时间戳?
- 原因:
- Long 时间戳是标准格式,无格式歧义(如 yyyy-MM-dd vs yyyy/MM/dd)。
- 前端可通过 format 方法灵活展示任意格式,规范性强。
- 实现:
- 使用自定义序列化器和反序列化器:
/** * 基于时间戳的 LocalDateTime 序列化器 * 用于将 Java 8 的 LocalDateTime 对象序列化为 Unix 时间戳(毫秒级) */ public class TimestampLocalDateTimeSerializer extends JsonSerializer<LocalDateTime> { // 单例实例,避免重复创建 public static final TimestampLocalDateTimeSerializer INSTANCE = new TimestampLocalDateTimeSerializer(); /** * 将 LocalDateTime 对象序列化为时间戳(毫秒) * * @param value 待序列化的 LocalDateTime 对象 * @param gen JSON 生成器,用于输出 JSON 内容 * @param serializers 序列化器提供程序,可用于获取上下文信息 * @throws IOException 当 JSON 生成过程中发生 I/O 错误时抛出 */ @Override public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException { // 1. 将 LocalDateTime 转换为系统默认时区的 ZonedDateTime // LocalDateTime 本身不带时区信息,需要通过 ZoneId.systemDefault() 获取系统默认时区 // 例如:2023-01-01T12:00:00 + 系统时区(如 Asia/Shanghai) = 2023-01-01T12:00:00+08:00[Asia/Shanghai] ZonedDateTime zonedDateTime = value.atZone(ZoneId.systemDefault()); // 2. 将 ZonedDateTime 转换为 Instant(时间线上的一个点,UTC 时间) // 例如:2023-01-01T12:00:00+08:00[Asia/Shanghai] -> 2023-01-01T04:00:00Z Instant instant = zonedDateTime.toInstant(); // 3. 将 Instant 转换为 Unix 时间戳(自 1970-01-01T00:00:00Z 以来的毫秒数) // 例如:2023-01-01T04:00:00Z -> 1672545600000 long timestamp = instant.toEpochMilli(); // 4. 将时间戳写入 JSON 输出流 gen.writeNumber(timestamp); } } /** * 基于时间戳的 LocalDateTime 反序列化器 */ public class TimestampLocalDateTimeDeserializer extends JsonDeserializer<LocalDateTime> { public static final TimestampLocalDateTimeDeserializer INSTANCE = new TimestampLocalDateTimeDeserializer(); @Override public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { // 将 Long 时间戳,转换为 LocalDateTime 对象 return LocalDateTime.ofInstant(Instant.ofEpochMilli(p.getValueAsLong()), ZoneId.systemDefault()); } }
- 配置在 YudaoJacksonAutoConfiguration 中:
@Bean public ObjectMapper objectMapper() { ObjectMapper mapper = new ObjectMapper(); SimpleModule module = new SimpleModule(); module.addSerializer(LocalDateTime.class, new TimestampLocalDateTimeSerializer()); module.addDeserializer(LocalDateTime.class, new TimestampLocalDateTimeDeserializer()); mapper.registerModule(module); return mapper; }
- 使用自定义序列化器和反序列化器:
全局配置时间格式
- 配置 LocalDateTimeSerializer 和 LocalDateTimeDeserializer:
@Bean public ObjectMapper objectMapper() { ObjectMapper mapper = new ObjectMapper(); SimpleModule module = new SimpleModule(); module.addSerializer(LocalDateTime.class, new TimestampLocalDateTimeSerializer()); module.addDeserializer(LocalDateTime.class, new TimestampLocalDateTimeDeserializer()); mapper.registerModule(module); return mapper; }
- 效果:所有 LocalDateTime 字段以 yyyy-MM-dd HH:mm:ss 格式序列化。
局部配置时间格式
- 使用 @JsonFormat:
@Data public class UserRespVO { @JsonSerialize(using = LocalDateTimeSerializer.class) @JsonDeserialize(using = LocalDateTimeDeserializer.class) @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime createTime; }
- 效果:仅 createTime 字段使用指定格式。