Servlet 处理日期
java.time 的格式化、解析、时区,以及和旧 Date 的互转。
Key Points新代码一律用 java.time:LocalDate 表示日期,Instant 表示时刻,格式化用线程安全的 DateTimeFormatter。
三个核心类型
| Type | Holds | Use when |
|---|---|---|
| LocalDate | 年月日 | 生日、账单日 |
| LocalTime | 时分秒 | 营业时间 |
| LocalDateTime | 年月日时分秒,无时区 | 页面展示 |
| Instant | UTC 瞬间 | 存储、比较、日志 |
| ZonedDateTime | 瞬间 + 时区 | 跨时区换算 |
格式化与解析
Java
import java.time.*;
import java.time.format.DateTimeFormatter;
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime now = LocalDateTime.now();
String s = now.format(fmt); // 2026-11-03 14:25:09
LocalDateTime back = LocalDateTime.parse(s, fmt);
// 页面里常用的中文格式
DateTimeFormatter cn = DateTimeFormatter.ofPattern("yyyy年M月d日");
System.out.println(LocalDate.of(2026, 11, 3).format(cn));Output
2026-11-03 14:25:09
2026年11月3日DateTimeFormatter is immutable and thread-safe, so a static final constant is fine. The legacy SimpleDateFormat is not, and sharing one across threads corrupts parses — a very subtle bug in old code.
时区
Java
ZoneId shanghai = ZoneId.of("Asia/Shanghai");
ZoneId utc = ZoneId.of("UTC");
ZonedDateTime local = ZonedDateTime.now(shanghai);
ZonedDateTime sameMoment = local.withZoneSameInstant(utc);
System.out.println(local); // 2026-11-03T14:25:09+08:00[Asia/Shanghai]
System.out.println(sameMoment); // 2026-11-03T06:25:09Z[UTC]Store timestamps in UTC (an
Instant or a timezone-aware column) and convert only for display. Storing local time breaks DST and cross-timezone users, and cannot be repaired later.与旧 API 互转
Java
// java.util.Date <-> Instant
Date legacy = Date.from(Instant.now());
Instant instant = legacy.toInstant();
// java.sql.Date / Timestamp <-> java.time
java.sql.Date sqlDate = java.sql.Date.valueOf(LocalDate.now());
LocalDate d = sqlDate.toLocalDate();
Timestamp ts = Timestamp.valueOf(LocalDateTime.now());
LocalDateTime ldt = ts.toLocalDateTime();计算差值
Java
LocalDate a = LocalDate.of(2026, 1, 31);
LocalDate b = LocalDate.of(2026, 11, 3);
Period period = Period.between(a, b);
System.out.println(period.getMonths() + " 个月 " + period.getDays() + " 天");
long days = ChronoUnit.DAYS.between(a, b);
System.out.println("相差 " + days + " 天");Output
9 个月 3 天
相差 276 天Period is a calendar difference (years, months, days); ChronoUnit.DAYS.between gives total days. Use the former for ages, the latter for countdowns — mixing them yields plausible but wrong numbers.
三个核心类型
| 类型 | 含义 | 适用 |
|---|---|---|
| 本地日期 | 年月日 | 生日、账单日 |
| 本地时间 | 时分秒 | 营业时间 |
| 本地日期时间 | 年月日时分秒,无时区 | 页面展示 |
| 时间戳 | UTC 瞬间 | 存储、比较、日志 |
| 带时区 | 瞬间 + 时区 | 跨时区换算 |
格式化与解析
Java
import java.time.*;
import java.time.format.DateTimeFormatter;
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime now = LocalDateTime.now();
String s = now.format(fmt); // 2026-11-03 14:25:09
LocalDateTime back = LocalDateTime.parse(s, fmt);
// 页面里常用的中文格式
DateTimeFormatter cn = DateTimeFormatter.ofPattern("yyyy年M月d日");
System.out.println(LocalDate.of(2026, 11, 3).format(cn));Output
2026-11-03 14:25:09
2026年11月3日DateTimeFormatter 是不可变且线程安全的,可以定义成 static final 常量共用。旧的 SimpleDateFormat 不是线程安全的,多线程共用会解析出错——这是老代码里非常隐蔽的 bug。
时区
Java
ZoneId shanghai = ZoneId.of("Asia/Shanghai");
ZoneId utc = ZoneId.of("UTC");
ZonedDateTime local = ZonedDateTime.now(shanghai);
ZonedDateTime sameMoment = local.withZoneSameInstant(utc);
System.out.println(local); // 2026-11-03T14:25:09+08:00[Asia/Shanghai]
System.out.println(sameMoment); // 2026-11-03T06:25:09Z[UTC]服务端存时间统一用 UTC(
Instant 或带时区的列),只在展示时转成本地时区。存本地时间会让夏令时和跨时区用户全部错乱,且事后无法还原。与旧 API 互转
Java
// java.util.Date <-> Instant
Date legacy = Date.from(Instant.now());
Instant instant = legacy.toInstant();
// java.sql.Date / Timestamp <-> java.time
java.sql.Date sqlDate = java.sql.Date.valueOf(LocalDate.now());
LocalDate d = sqlDate.toLocalDate();
Timestamp ts = Timestamp.valueOf(LocalDateTime.now());
LocalDateTime ldt = ts.toLocalDateTime();计算差值
Java
LocalDate a = LocalDate.of(2026, 1, 31);
LocalDate b = LocalDate.of(2026, 11, 3);
Period period = Period.between(a, b);
System.out.println(period.getMonths() + " 个月 " + period.getDays() + " 天");
long days = ChronoUnit.DAYS.between(a, b);
System.out.println("相差 " + days + " 天");Output
9 个月 3 天
相差 276 天Period 是「日历差」(几年几月几天),ChronoUnit.DAYS.between 是「总天数」。算年龄用前者,算到期天数用后者——混用会得到看似正确其实错位的结果。