ValidX时间段验证详解:ISO 8601标准与简化格式
📋 目录
- 引言
- 一、时间段是什么:先分清"时间点"与"时间段"
- 二、两种格式总览与 API 入口
- 三、ISO 8601 标准格式详解
- 3.1 标准结构 P[nY][nM][nD][T[nH][nM][nS]]
- 3.2 验证器对 ISO 格式的约束
- 四、简化格式详解
- 4.1 数字+单位组合规则
- 4.2 mo 与 m 的区分:月还是分钟
- 五、format 参数:ISO / SIMPLE / ANY 三种模式
- 六、注解方式实战
- 七、链式方式实战
- 八、空值与判空语义
- 九、常见有效/无效对照表
- 十、典型业务场景
- 十一、容易忽略的实现细节与坑
- 总结
- 项目地址
引言
“视频时长不超过 15 分钟”“优惠券有效期为 3 天”“任务最多重试 2 小时 30 分钟”——这些业务里到处是时间段(Duration)校验。但相比日期/时间点,时间段校验常被新手用"手写正则 + 各自约定"糊弄:有人存 "2.5h",有人存 "150min",有人存 "PT2H30M",格式五花八门,最终线上解析全靠运气。
ValidX 从 v1.0.0 起就内置了专门的时间段验证器 @Duration(链式为 isDuration),统一支持两套主流写法:
- ISO 8601 标准:PT2H30M、P1DT12H、P1Y2M3D——跨系统交换的通用语言
- 简化格式:2h30m、1d12h、1y2mo3d——人类可读的日常写法
本文直接对照 DurationValidator 源码与测试,讲清两套格式的完整规则、ISO_8601 / SIMPLE / ANY 三种模式的区别,以及最容易踩的 mo/m 混淆等实现细节。
文中结论对照 ValidX v1.2.0 源码与单元测试核实。
一、时间段是什么:先分清"时间点"与"时间段"
ValidX 的时间验证按语义分两类,别混用:
| 回答的问题 | “现在是几点?” | “持续了多久?” |
| 例子 | 2024-01-15、14:30:00 | PT2H30M、2h30m |
| ValidX 注解 | @Date、@DateTime、@HourMinute… | @Duration |
| 语义 | 日历上的某个时刻 | 一段长度,不锚定到某个日期 |
关键区别:PT2H30M 不代表"2 点 30 分",而是"两个半小时"这个长度。一个时间段可以和任意起点相加,本身没有日历位置。
二、两种格式总览与 API 入口
2.1 总览表
| ISO 8601 | DurationFormat.ISO_8601 | PT2H30M、P1DT12H、P1Y2M3D | 标准、跨平台通用 |
| 简化格式 | DurationFormat.SIMPLE | 2h30m、1d12h、1y2mo3d | 人类可读、书写简洁 |
| 任意(默认) | DurationFormat.ANY | 两者之一 | 两种都接受 |
2.2 API 入口
ValidX 提供注解与链式两条等价入口:
// 注解方式
@Duration
private String taskDuration; // 两种格式均可
// 链式方式(ANY:默认两种都接受)
ValidX validator = ValidX.init();
validator.isDuration("PT2H30M"); // true
validator.isDuration("2h30m"); // true
// 指定只收 ISO 格式
validator.isDuration("PT2H30M", Duration.DurationFormat.ISO_8601);
注解属性只有一个 format(默认 ANY),对应上面枚举的三种模式。
三、ISO 8601 标准格式详解
3.1 标准结构 P[nY][nM][nD][T[nH][nM][nS]]
ISO 8601 时间段以字母 P 开头,核心结构为:
P [nY] [nM] [nD] [T [nH] [nM] [nS]]
各部分含义([] 表示可选):
| P | Period(必需前缀) | P… |
| nY | 年 | P1Y(1 年) |
| nM | 月(日期部分) | P2M(2 个月) |
| nD | 天 | P3D(3 天) |
| T | 时间分隔符:其后是时分秒 | PT… |
| nH | 小时(T 之后) | PT4H(4 小时) |
| nM | 分钟(T 之后) | PT5M(5 分钟) |
| nS | 秒,支持小数 | PT6S、PT0.5S |
因此:
- PT2H30M = 2 小时 30 分钟(纯时间部分,P 后直接跟 T)
- P1DT12H = 1 天 12 小时
- P1Y2M3D = 1 年 2 个月 3 天(纯日期部分,无 T)
- P1Y2M3DT4H5M6S = 1 年 2 月 3 天 4 小时 5 分 6 秒(完整形式)
大写 T 的作用是把"日期部分"和"时间部分"分开:T 之前的 M 是月,T 之后的 M 是分钟——同一个字母,位置决定含义。
3.2 验证器对 ISO 格式的约束
对照源码,isValidIso8601Duration 在正则匹配之外还做了三条硬性检查:
P 和 PT 单独出现不合法——正则能匹配上空壳,但验证器会显式拒绝:
if (duration.equalsIgnoreCase("P") || duration.equalsIgnoreCase("PT")) {
return false;
}
"P"、"PT" 都是 false。
不含 T 时,必须带有天(D):
if (!duration.toUpperCase().contains("T") && !duration.toUpperCase().matches("^P\\\\d+D$")) {
return false;
}
也就是说 P1Y、P2M(只有年月、没有天)会被拒绝;合法纯日期形式必须有 D,如 P1D、P1Y2M3D。
含 T 时,T 后必须有至少一个时间单位——PT 后为空则拒绝。
此外正则 CASE_INSENSITIVE,所以 ISO 格式大小写不敏感:pt2h30m 与小写 PT2H30M 等价(有测试覆盖)。
四、简化格式详解
4.1 数字+单位组合规则
简化格式是"数字+单位"顺序拼接,供日常快速书写:
[ny][nmo][nd][nh][nm][ns]
支持的 6 个单位(必须按 年→月→天→时→分→秒 顺序出现,不可乱序):
| y / Y | 年 | 1y |
| mo / MO | 月 | 6mo |
| d / D | 天 | 3d |
| h / H | 小时 | 2h |
| m / M | 分钟 | 30m |
| s / S | 秒 | 45s |
合法值举例(均有测试覆盖):
validator.isDuration("2h"); // true
validator.isDuration("2h30m"); // true
validator.isDuration("1h30m15s"); // true
validator.isDuration("1d12h30m"); // true
validator.isDuration("1y6mo"); // true
validator.isDuration("90s"); // true
与 ISO 一样大小写不敏感:"2H30M"、"2h30m" 等价。
校验强制"至少一个非零单位":纯数字("123")、无效字符("2h30x")都会失败。
4.2 mo 与 m 的区分:月还是分钟
简化格式最容易踩的坑是月与分钟的字母冲突:
- 月 → 必须写 mo(month)
- 分钟 → 只写 m(minute)
因为 m 已被分钟占用,所以月份必须用双字母 mo 明确区分:
validator.isDuration("6mo"); // 6 个月(月 = mo)
validator.isDuration("6m"); // 6 分钟(分钟 = m)→ 不同含义!
validator.isDuration("1y2mo3d"); // 1 年 2 个月 3 天
验证器在解析时会特意排除 mo 里的 m(源码用"检查 m 后一位是否紧跟 o"来区分),确保 1y2mo3d 不会被误当成"1 年 2 分钟…"。
建议:跨系统/跨语言传递时优先用 ISO 格式避免歧义;简化格式多用于配置项等人类书写的内部场景。
五、format 参数:ISO / SIMPLE / ANY 三种模式
@Duration 的 format 属性与链式 isDuration 的第二参数一致,控制"收哪种写法":
| ANY(默认) | ISO 与简化都接受 | 兼容历史数据 / 宽松校验 |
| ISO_8601 | 只收 ISO 格式 | 跨系统协议字段、API 对接 |
| SIMPLE | 只收简化格式 | 内部配置、人工录入场景 |
模式是互斥白名单,不是"优先尝试":
// ISO_8601 模式下,简化格式会被拒绝
validator.isDuration("PT2H30M", DurationFormat.ISO_8601); // true
validator.isDuration("2h30m", DurationFormat.ISO_8601); // false
// SIMPLE 模式下,ISO 格式会被拒绝
validator.isDuration("2h30m", DurationFormat.SIMPLE); // true
validator.isDuration("PT2H30M", DurationFormat.SIMPLE); // false
提示:注解里的枚举路径可写 Duration.DurationFormat.ISO_8601;若单独 import 了注解内部枚举,直接写 DurationFormat.ISO_8601 即可。
六、注解方式实战
6.1 基础用法:两种格式都收
public class TaskDTO {
// 默认 ANY:ISO 与简化都合法
@Duration(message = "任务时长格式不合法")
private String duration; // "PT2H30M" 或 "2h30m" 均可
}
6.2 只收 ISO 8601:对接第三方协议
public class VideoUploadDTO {
// 视频平台协议要求 ISO 8601 时长
@Duration(format = Duration.DurationFormat.ISO_8601,
message = "视频时长必须为ISO 8601格式,如 PT1H30M")
private String duration;
}
6.3 只收简化格式:人工配置
public class RetryConfigDTO {
// 运维人员手填配置,用人类可读的简化格式
@Duration(format = Duration.DurationFormat.SIMPLE,
message = "重试窗口格式:如 2h30m / 1d12h")
private String retryWindow;
}
6.4 必填 + 格式:别忘了判空注解
public class CouponDTO {
// 必填且格式正确:@NotBlank 管空值,@Duration 管格式
@NotBlank(message = "有效期不能为空")
@Duration(message = "有效期格式不合法,如 PT24H 或 24h")
private String validity;
}
与 ValidX 所有格式注解一致,@Duration 对 null/"" 返回通过(放行),是否必填由 @NotBlank 等决定——这是设计上的关注点分离,不是 bug。
七、链式方式实战
链式 isDuration 适合 Service 层动态校验,例如从外部接口/配置中心取回的字符串:
public void validateTaskTimeLimit(String timeLimit) {
ValidX validator = ValidX.init()
.config(ValidXConfig.GLOBAL_NOT_EMPTY) // 必填
.field("任务时限").isDuration(timeLimit); // 默认 ANY
if (!validator.passed()) {
throw new ValidationException(validator.getErrors());
}
}
指定格式并搭配字段标签与局部覆盖:
// 字段标签让报错更可读;allowNull 允许个别可选
ValidX validator = ValidX.init()
.config(ValidXConfig.GLOBAL_NOT_NULL)
.field("视频时长").isDuration(video.duration(), Duration.DurationFormat.ISO_8601)
.field("封面描述时长(可选)").allowNull().isDuration(optionalText);
if (!validator.passed()) {
System.out.println(validator.getErrors()); // ["视频时长: …", …]
}
多种值混合在同一链上验证(来自官方测试):
ValidX validator = ValidX.init()
.isDuration("PT2H30M")
.isDuration("2h30m")
.isDuration("P1DT12H");
validator.passed(); // true:三种合法写法都被 ANY 接受
八、空值与判空语义
DurationValidator.isValid 的判空逻辑与 ValidX 全线注解一致:
| null | 通过(true) | 由 @NotNull 等负责判空 |
| "" | 通过(true) | 由 @NotBlank/@NotEmpty 负责判空 |
| 非 String 类型 | 失败(false) | 只支持字符串 |
| 合法时间段字符串 | 通过 | 见上文规则 |
链式模式下受全局/局部配置影响(有专门测试覆盖):
// 默认:null 与空串都放行
ValidX v1 = ValidX.init().isDuration(null); // true
ValidX v2 = ValidX.init().isDuration(""); // true
// GLOBAL_NOT_NULL 下 null 失败;allowNull 可局部豁免
ValidX v3 = ValidX.init().config(ValidXConfig.GLOBAL_NOT_NULL);
v3.isDuration(null); // false
v3.allowNull().isDuration(null); // true
// GLOBAL_NOT_EMPTY 下空串失败;allowEmpty 可豁免
ValidX v4 = ValidX.init().config(ValidXConfig.GLOBAL_NOT_EMPTY);
v4.isDuration(""); // false
v4.allowEmpty().isDuration(""); // true
九、常见有效/无效对照表
| PT2H30M | ✅ | ❌ | ✅ |
| P1DT12H | ✅ | ❌ | ✅ |
| P1Y2M3D | ✅ | ❌ | ✅ |
| P1Y2M3DT4H5M6S | ✅ | ❌ | ✅ |
| pt2h30m(小写) | ✅ | ❌ | ✅ |
| P1D | ✅ | ❌ | ✅ |
| P1Y / P2M(无 D 无 T) | ❌ | ❌ | ❌ |
| P / PT(空壳) | ❌ | ❌ | ❌ |
| PT 后无单位 | ❌ | ❌ | ❌ |
| 2h30m | ❌ | ✅ | ✅ |
| 1d12h30m | ❌ | ✅ | ✅ |
| 1y2mo3d | ❌ | ✅ | ✅ |
| 6mo(6 个月) | ❌ | ✅ | ✅ |
| 1y6mo | ❌ | ✅ | ✅ |
| 90s | ❌ | ✅ | ✅ |
| 123(纯数字) | ❌ | ❌ | ❌ |
| 2h30x(无效字符) | ❌ | ❌ | ❌ |
| 2.5h(简化格式小数) | ❌ | ❌ | ❌ |
| null / "" | ✅ | ✅ | ✅ |
十、典型业务场景
| 视频/音频上传时长 | ISO 8601(协议对接) | @Duration(format = ISO_8601) private String videoDuration; |
| 任务/批处理超时窗口 | 简化格式(人工配置) | @Duration(format = SIMPLE) private String timeout = "2h30m"; |
| 优惠券/会员有效期 | ISO 8601(跨系统) | @Duration(format = ISO_8601) private String validity = "P30D"; |
| 缓存/Token TTL | 简化或 ISO 均可 | 链式 .field("TTL").isDuration(ttl, DurationFormat.ANY) |
| 活动倒计时/间隔 | 任选一种并全局统一 | 建 DTO 统一用 @Duration + 约定格式 |
工程建议:一个系统内只选一种主格式(推荐 ISO 8601),把另一种仅用于兼容存量数据;入库前用 @Duration 校验,别让格式问题拖到解析阶段才暴露。
十一、容易忽略的实现细节与坑
总结
| 定位 | 时间段验证 = “持续多长”,区别于时间点的"是哪一刻" |
| ISO 8601 | P[nY][nM][nD][T[nH][nM][nS]],T 前 M 是月、T 后 M 是分钟 |
| 简化格式 | [ny][nmo][nd][nh][nm][ns],单位按序;mo=月、m=分钟 |
| 三种模式 | ANY(默认都收)/ ISO_8601 / SIMPLE,互斥白名单 |
| API | 注解 @Duration(format=…) + 链式 isDuration(value[, format]) |
| 空值 | 放行 null/"",必填叠加 @NotBlank 或全局/局部非空配置 |
| 两个最易踩的坑 | ①P1Y 无 D/T 会被拒;②mo(月) 与 m(分钟) 混用 |
时间段校验的通用心法是:一个系统统一一种格式、入库前必校验、跨系统必用 ISO。ValidX 的 @Duration 把两种格式 + 模式切换做成了开箱即用的注解和链式方法,直接声明即可,不必再手写正则。
项目地址
- GitHub:https://github.com/vipxieliang/ValidX
- Gitee:https://gitee.com/vipxieliang/ValidX
- Maven Central:https://central.sonatype.com/artifact/io.github.vipxieliang/validx



