Lee's BlogLee's Blog
Article
Project
Category
Tag
Timeline
Article
Project
Category
Tag
Timeline
  • ISO 8601 日期时间格式:Java、JavaScript 解析与最佳实践

ISO 8601 日期时间格式:Java、JavaScript 解析与最佳实践

2026-10-07DevelopmentISO 8601JavaJavaScript日期时间

数据库 / Java:Instant → UTC 时间点

JSON:ISO 8601

前端:负责 yyyy-MM-dd HH:mm:ss + 用户时区展示

1. ISO 8601 是什么

ISO 8601 是日期和时间的信息交换标准。它按年、月、日等从大到小的顺序表达时间,减少 10/07/2026 究竟是 10 月 7 日还是 7 月 10 日这样的歧义。标准还覆盖周日期、年内日序、持续时间和时间区间,不能把它理解成单一字符串模板。参见 ISO 官方介绍。

日常接口最常见的是这种带 UTC 偏移量的扩展日期时间格式:

2026-10-07T16:30:45.123+08:00
│          │        │   └─ UTC 偏移量:比 UTC 快 8 小时
│          │        └──── 秒的小数部分,这里精确到毫秒
│          └───────────── 时间:16 时 30 分 45 秒
└──────────────────────── 日期:2026 年 10 月 7 日

T 分隔日期和时间;Z 表示 UTC,即零偏移量。它们是格式中的字面字符,不是变量。

2026-10-07T16:30:45.123+08:00
2026-10-07T08:30:45.123Z

这两行表示同一个时间点。转成 UTC 会改变钟表读数,但不会改变实际发生的时刻。

2. 常见表示法

类型示例含义
日历日期,扩展格式2026-10-07年、月、日
日历日期,基本格式20261007省略分隔符
时间16:30:45只有钟表时间
无偏移量的日期时间2026-10-07T16:30:45尚未指定时区的当地日期时间
UTC 日期时间2026-10-07T08:30:45Z明确的时间点
带偏移量的日期时间2026-10-07T16:30:45+08:00明确的时间点,保留偏移量
带小数秒的日期时间2026-10-07T08:30:45.123Z这里使用毫秒精度
周日期2026-W41-3ISO 周年 2026 的第 41 周、星期三
年内日序日期2026-2802026 年第 280 天
日历期间P1Y2M3D1 年、2 个月、3 天
时间长度PT2H30M2 小时、30 分钟
时间区间2026-10-07T08:00:00Z/2026-10-07T09:00:00Z起点与终点

持续时间中的 P 是期间标记,T 分隔日期部分与时间部分;P1M 是一个月,PT1M 是一分钟。一个月不能直接换算成固定秒数。接口还应单独约定区间边界是否包含终点,字符串本身不能代替业务约定。

这些表示法不意味着任意日期解析器都支持它们。尤其不能把周日期、持续时间直接传给 JS Date,期待它自动理解。

ISO 8601 与 RFC 3339

RFC 3339 为互联网时间戳选择了 ISO 8601 的一套受限规则。API 文档写“支持 ISO 8601”仍然不够具体:必须说明是否要求偏移量、秒、小数位以及可接受的年份范围。

本文建议的 Java/JS 互操作约定是:四位年份、完整日期和时间、大写 T、三位小数秒,输出统一为 UTC Z;如需接受偏移量输入,则采用 ±HH:mm。这是一套项目约定,不是 ISO 8601 的全部语法。闰秒和 24:00:00 等边界表示不纳入这套约定。

3. 先分清业务语义

业务数据应表达什么Java 类型JS 处理方式
订单创建时间、日志时间时间线上的瞬间InstantDate,输出 toISOString()
带原始偏移量的时间当地时间与偏移量OffsetDateTimeDate 加单独保存的原始字段
生日、账单日期单纯日期LocalDate保留 YYYY-MM-DD 字符串
每天营业的开门时间单纯钟表时间LocalTime时间字符串或专用类型
尚未确定时区的预约时间当地日期时间LocalDateTime当地日期时间字段
某地区每周举行的会议当地日期时间和地区时区规则LocalDateTime + ZoneId保存当地字段和 IANA 时区标识

+08:00 是偏移量;Asia/Shanghai 是地区时区标识。偏移量只说明某一时刻与 UTC 的差值,地区时区还涉及历史变化、夏令时规则。未来日程不能只保存一次计算出的偏移量。

2026-10-07T16:30:45 没有偏移量,无法单独确定时间点。Java 的 LocalDateTime 不会自动补上时区;JS Date 却会把这种日期时间解释为运行环境当地时间,因此跨语言传输尤其容易出错。

4. Java:优先使用 java.time

以下示例使用 Java 8 起提供的 java.time,可放入 main 方法运行。根据输入的业务语义选择类型,避免用一个 LocalDateTime 承载所有时间字段。

4.1 解析、转换与比较

import java.time.*;
import java.time.format.DateTimeFormatter;

public class Iso8601Example {
    public static void main(String[] args) {
        LocalDate date = LocalDate.parse("2026-10-07");
        LocalTime time = LocalTime.parse("16:30:45");
        LocalDateTime local = LocalDateTime.parse("2026-10-07T16:30:45");

        OffsetDateTime offset = OffsetDateTime.parse(
            "2026-10-07T16:30:45.123+08:00",
            DateTimeFormatter.ISO_OFFSET_DATE_TIME
        );
        Instant instant = offset.toInstant();
        Instant utc = Instant.parse("2026-10-07T08:30:45.123Z");

        System.out.println(date);                    // 2026-10-07
        System.out.println(time);                    // 16:30:45
        System.out.println(local);                   // 2026-10-07T16:30:45
        System.out.println(instant);                 // 2026-10-07T08:30:45.123Z
        System.out.println(instant.equals(utc));     // true

        ZonedDateTime shanghai = instant.atZone(ZoneId.of("Asia/Shanghai"));
        System.out.println(shanghai.toLocalDateTime()); // 2026-10-07T16:30:45.123
    }
}

解析失败会抛出 DateTimeParseException,接口边界应将它转换成明确的输入错误。比较真实时刻可先转成 Instant;OffsetDateTime.equals() 还会比较当地日期时间和偏移量,不能直接用它判断两个不同偏移表示是否为同一瞬间。

Java 官方 DateTimeFormatter 文档 提供了日期、时间、偏移日期时间等内置格式器。优先使用这些常量。

4.2 固定毫秒精度的接口输出

Instant.toString() 的小数位数可能变化。若接口要求固定三位,显式格式化并明确截断策略:

import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.temporal.ChronoUnit;

DateTimeFormatter apiFormat = new DateTimeFormatterBuilder()
    .appendInstant(3)
    .toFormatter();

Instant value = Instant.parse("2026-10-07T08:30:45.123456789Z");
String jsonValue = apiFormat.format(value.truncatedTo(ChronoUnit.MILLIS));
System.out.println(jsonValue); // 2026-10-07T08:30:45.123Z

截断会丢失微秒和纳秒。如果业务依赖高精度排序,应保留原始精度,传输完整字符串并使用支持该精度的类型,不要经过 JS Date。

4.3 自定义日期格式必须严格校验

需要支持紧凑日期时,显式指定格式,不依赖自动猜测:

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.ResolverStyle;

DateTimeFormatter compact = DateTimeFormatter.ofPattern("uuuuMMdd")
    .withResolverStyle(ResolverStyle.STRICT);

System.out.println(LocalDate.parse("20261007", compact)); // 2026-10-07
// LocalDate.parse("20260230", compact); // 抛出 DateTimeParseException

uuuu 表示年,适合严格的 ISO 日历解析;yyyy 表示纪元内的年;YYYY 表示周所属的年,年末年初可能与日历年不同。日期时间模板还要区分 MM 月与 mm 分钟、HH 24 小时制与 hh 12 小时制。

4.4 时区与夏令时:不要默默替用户选择

如果当地时间已经明确属于上海,可使用 local.atZone(ZoneId.of("Asia/Shanghai"))。但有夏令时的地区可能出现不存在的时间或出现两次的时间。严格的预约逻辑应先检查有效偏移量:

import java.time.*;
import java.util.List;

LocalDateTime local = LocalDateTime.parse("2026-11-01T01:30:00");
ZoneId zone = ZoneId.of("America/New_York");
List<ZoneOffset> offsets = zone.getRules().getValidOffsets(local);

if (offsets.isEmpty()) {
    throw new IllegalArgumentException("该当地时间不存在,请重新选择");
}
if (offsets.size() > 1) {
    throw new IllegalArgumentException("该当地时间出现两次,请明确 UTC 偏移量");
}
ZonedDateTime appointment = ZonedDateTime.ofStrict(local, offsets.get(0), zone);

示例会进入“出现两次”的分支。普通 atZone() 对这些情况有默认调整和选择行为,业务需要明确接受还是拒绝它。

Java 的 ZonedDateTime.toString() 可能输出 ...-04:00[America/New_York]。不要把含方括号时区名的字符串当成 JS Date 必须支持的输入;跨端可把 timeZone 单独放在字段中。

4.5 日历期间与固定时间长度

import java.time.*;

Period month = Period.parse("P1M");
Duration elapsed = Duration.parse("PT2H30M");

System.out.println(LocalDate.parse("2026-01-31").plus(month)); // 2026-02-28
System.out.println(elapsed.toMinutes());                     // 150

Period 表达年、月、日的日历运算;Duration 表达秒、纳秒组成的时间长度。跨夏令时变化时,加一个日历日与加 24 小时可能得到不同的当地时间。Java 的 Duration 将一天按 24 小时处理。参见官方 Period 与 Duration 文档。

5. JavaScript:Date 能做什么

5.1 解析时间点并输出 UTC

const date = new Date('2026-10-07T16:30:45.123+08:00');

if (Number.isNaN(date.getTime())) {
  throw new TypeError('无效的日期时间');
}

console.log(date.toISOString()); // 2026-10-07T08:30:45.123Z
console.log(date.getTime());     // Unix 时间戳,单位为毫秒

const utc = new Date('2026-10-07T08:30:45.123Z');
console.log(date.getTime() === utc.getTime()); // true

Date 保存的是以毫秒表示的时间点,不保存原始 +08:00 或地区时区。输出 UTC 用 toISOString();展示用显式时区。无效 Date 调用 toISOString() 会抛出 RangeError。

ECMAScript 规定了一套简化的 ISO 日期时间字符串格式,原生解析并不覆盖全部 ISO 8601。超出这套格式的输入可能依赖实现,别用 2026/10/07 16:30:45 这样的字符串做跨环境交换。参见 ECMAScript 日期时间字符串规范。

5.2 两种无偏移量输入的行为不同

new Date('2026-10-07');          // 纯日期:按 UTC 的当天零点解释
new Date('2026-10-07T16:30:45'); // 日期时间:按运行环境当地时区解释

同一份代码在上海和纽约可能得到不同结果。生日等纯日期最好保持为 2026-10-07 字符串,不要先转成时间点再按当地时区展示,否则在 UTC 西侧可能显示成前一天。

也不要用 date.toISOString().slice(0, 10) 获取用户当地日期:它取的是 UTC 日期。

5.3 指定展示时区

const date = new Date('2026-10-07T08:30:45.123Z');
const formatter = new Intl.DateTimeFormat('zh-CN', {
  timeZone: 'Asia/Shanghai',
  year: 'numeric',
  month: '2-digit',
  day: '2-digit',
  hour: '2-digit',
  minute: '2-digit',
  second: '2-digit',
  hourCycle: 'h23',
});

console.log(formatter.format(date)); // 表示上海当地的 2026-10-07 16:30:45

具体分隔符由本地化实现决定,格式化结果用于展示,别再把它送回日期解析器。Intl.DateTimeFormat 能转换展示时区,但不负责把任意地区的当地时间字符串解析成时间点。

5.4 严格校验接口输入

仅检查 Date.parse() 是否为 NaN 不够:某些非法日历日期可能被归一化。对接口约定的固定 UTC 毫秒格式,可以先限制形状,再用往返转换检查日历值:

// 本项目约定:四位年份、UTC、大写 T/Z、固定三位毫秒。
// 这是业务格式校验器,不是完整 ISO 8601 校验器。
function parseApiInstant(value) {
  if (typeof value !== 'string' ||
      !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value)) {
    throw new TypeError('时间必须采用 YYYY-MM-DDTHH:mm:ss.SSSZ 格式');
  }

  const date = new Date(value);
  if (Number.isNaN(date.getTime()) || date.toISOString() !== value) {
    throw new TypeError('日期时间值无效');
  }
  return date;
}

console.log(parseApiInstant('2026-10-07T08:30:45.123Z').toISOString());
// parseApiInstant('2026-02-30T08:30:45.123Z'); // 拒绝不存在的日期
// parseApiInstant('2026-10-07T08:30:45.123');  // 拒绝缺少 Z

如果接口允许 ±HH:mm、可变小数位或地区时区,应换用满足该契约的严格解析方案;不能直接用上面的 UTC 往返比较拒绝所有偏移量输入,因为同一时间点本来就有多种合法表示。

对纯日期,也可以保留字符串并单独校验:

function parseDateOnly(value) {
  if (typeof value !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(value)) {
    throw new TypeError('日期必须采用 YYYY-MM-DD 格式');
  }
  // Date 仅用于校验,返回值仍是纯日期字符串。
  const probe = new Date(`${value}T00:00:00.000Z`);
  if (Number.isNaN(probe.getTime()) ||
      probe.toISOString().slice(0, 10) !== value) {
    throw new TypeError('日期值无效');
  }
  return value;
}

console.log(parseDateOnly('2024-02-29')); // 2024-02-29
// parseDateOnly('2026-02-29'); // 拒绝非闰年的 2 月 29 日

6. Java 与 JS 互操作示例

后端输出明确的时间点,前端按业务时区展示:

{
  "createdAt": "2026-10-07T08:30:45.123Z",
  "billingDate": "2026-10-07",
  "timeZone": "Asia/Shanghai"
}
  • createdAt:Java 使用 Instant,按固定毫秒格式序列化;JS 使用 Date 解析。
  • billingDate:Java 使用 LocalDate;JS 保留日期字符串。
  • timeZone:用于业务展示或日程计算。它不会改变 createdAt 所表示的瞬间。

未来循环日程应另外保存当地时间、地区时区和重复规则。例如“纽约每周一 09:00”不能用“每隔 7 × 24 小时”长期代替。

7. 最佳实践

  1. 先确定语义,再选格式。 事件时间用带偏移量的时间戳,生日用纯日期,未来日程保留当地时间与地区时区。
  2. 接口契约写具体。 推荐时间点输出为 2026-10-07T08:30:45.123Z;明确是否接受偏移量输入、精度、年份范围与非法值策略。
  3. 时间点统一成 UTC,展示时再转换。 数据库按字段语义选类型,检查驱动与连接时区的行为;仅靠“存了 ISO 字符串”无法保证一致性。
  4. Java 用 java.time。 内置格式器优先,自定义格式显式严格解析;避免共享非线程安全的 SimpleDateFormat。
  5. JS 不依赖自动猜测。 时间点输入必须明确偏移量,纯日期单独处理;接口边界同时检查形状与日历有效性。
  6. 约定精度和单位。 Java 可表达纳秒,JS Date 精确到毫秒。时间戳字段明确秒还是毫秒;需要更高精度时避免有损转换。
  7. 按时间值排序。 混合偏移量或不同小数位的字符串不保证按实际时间排序。只有统一到相同 UTC 格式、字段宽度与精度后,才适合按字符串排序。
  8. 区分日历运算与经过时长。 “下个月同一天”“明天同一当地时刻”“24 小时以后”是不同需求。
  9. 处理时区边界。 检查夏令时跳变、重复时刻、跨日、跨年和闰年;不要依赖服务器默认时区。

实际使用时,先回答三个问题:这是日期还是时间点?如果是当地时间,属于哪个时区?跨端交换要保留多少精度? 这三件事明确后,格式与解析代码才能保持一致。

Last Updated: 10/12/26, 6:01 AM
Contributors: zlx01

CC BY-NC-SA 4.0 © 2021~present 粤ICP备2021090228号