java每日精进 5.14【参数校验】

参数校验

1.1概述

本文使用 Hibernate Validator 框架对 RESTful API 接口的参数进行校验,确保数据入库的正确性。

例如,在用户注册时,校验手机号格式、密码强度等。如果校验失败,抛出

ConstraintViolationException 或相关异常,由 GlobalExceptionHandler 捕获,返回标准化的 CommonResult 响应,格式如下:

java 复制代码
{
  "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
@Email 符合邮箱格式
@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 注解,启用校验:

    java 复制代码
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
  • 作用:通知 Spring 在方法调用时对参数进行校验。

  • 注意:Service 层也需校验,因为 Service 可能被其他 Service 调用,参数可能不正确。

第二步:添加校验注解

分为两种情况:

情况一:Bean 类型参数

  • 在方法参数上添加 @Valid,在 Bean 属性上添加校验注解:

    java 复制代码
    // Controller 示例
    @Validated
    @RestController
    public class AuthController {
        // ...
    }
    
    // Service 示例(实现类)
    @Service
    @Validated
    public class AdminAuthServiceImpl implements AdminAuthService {
        // ...
    }
  • 解析

    • @Valid:触发对 AuthLoginReqVO 对象的属性校验。
    • @NotEmpty:确保字段非 null 且非空字符串。
    • @Length:限制字符串长度在 4-16 位。
    • @Pattern:确保用户名只包含字母和数字。
    • 如果校验失败,抛出 MethodArgumentNotValidException,由 GlobalExceptionHandler 处理。

情况二:普通类型参数

  • 直接在方法参数上添加校验注解:

    java 复制代码
    // 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 捕获:

      java 复制代码
      @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 注解
java 复制代码
@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
java 复制代码
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
java 复制代码
@Data
public class AppAuthLoginReqVO {

    @NotEmpty(message = "手机号不能为空")
    @Mobile // 应用自定义注解
    private String mobile;
}
  • 解析
    • @Mobile 校验 mobile 是否符合手机号格式。
    • 校验失败抛出 ConstraintViolationException,由 GlobalExceptionHandler 处理。

1.5 校验异常处理

  • 异常类型
    • Bean 参数校验失败:抛出 MethodArgumentNotValidException 或 BindException。
    • 普通参数校验失败:抛出 ConstraintViolationException。
  • 处理流程
    • GlobalExceptionHandler 捕获这些异常,转换为 CommonResult:

      java 复制代码
      @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 状态码和用户友好的错误消息。

1.6 WebSocket 场景

  • 校验方式:WebSocket 握手请求(HTTP GET)通过查询参数传递数据,可使用普通参数校验:
java 复制代码
@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 指定时间格式:
java 复制代码
@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:

    java 复制代码
    @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
  • 解析:前端将时间转换为毫秒时间戳,符合后端预期。

2.4 Response Body 时间响应

  • LocalDateTime 字段序列化为 Long 时间戳:

    java 复制代码
    @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 方法灵活展示任意格式,规范性强。
  • 实现
    • 使用自定义序列化器和反序列化器:

      java 复制代码
      /**
       * 基于时间戳的 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 中:

      java 复制代码
      @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:

    java 复制代码
    @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:

    java 复制代码
    @Data
    public class UserRespVO {
        @JsonSerialize(using = LocalDateTimeSerializer.class)
        @JsonDeserialize(using = LocalDateTimeDeserializer.class)
        @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
        private LocalDateTime createTime;
    }
  • 效果:仅 createTime 字段使用指定格式。

相关推荐
酷炫码神2 分钟前
C#运算符
开发语言·c#
小秋学嵌入式-不读研版7 分钟前
C42-作业练习
c语言·开发语言·笔记
休息一下接着来13 分钟前
C++ 条件变量与线程通知机制:std::condition_variable
开发语言·c++·算法
爱尚你199323 分钟前
Java 泛型与类型擦除:为什么解析对象时能保留泛型信息?
java
小哈里33 分钟前
【pypi镜像源】使用devpi实现python镜像源代理(缓存加速,私有仓库,版本控制)
开发语言·python·缓存·镜像源·pypi
努力学习的小廉37 分钟前
【C++】 —— 笔试刷题day_29
开发语言·c++·算法
电商数据girl1 小时前
酒店旅游类数据采集API接口之携程数据获取地方美食品列表 获取地方美餐馆列表 景点评论
java·大数据·开发语言·python·json·旅游
天天打码1 小时前
python版本管理工具-pyenv轻松切换多个Python版本
开发语言·python
CircleMouse1 小时前
基于 RedisTemplate 的分页缓存设计
java·开发语言·后端·spring·缓存
ktkiko111 小时前
顶层架构 - 消息集群推送方案
java·开发语言·架构