新手也能学得会的之 Java 时间类 使用与设计
一,计算机里面时间记录方式
在计算机系统中,时间通常以一个数值的形式进行内部表示,最广为人知的标准是 Unix 时间戳(Unix timestamp)——它定义为自 1970 年 1 月 1 日 00:00:00 UTC(协调世界时) 起所经过的秒数(或毫秒数)。这个时刻被称为 “Unix 纪元”(Unix Epoch),是现代操作系统和编程语言处理时间的共同起点。
值得注意的是,虽然 Unix 时间戳以 1970 年为“零点”,但它并非只能表示此之后的时间:1970 年之前的日期可以通过负数时间戳来表示(例如,1969 年 12 月 31 日对应时间戳 -86400)。不过,时间的表示与处理远不止一个整数那么简单——时区、夏令时、闰秒、日历系统(如公历、农历)等因素,使得“时间”成为软件开发中一个出人意料的复杂领域。
Java 作为一门广泛应用于企业级开发的语言,其时间处理 API 也经历了从早期简陋设计(如
Date和Calendar)到现代强大模型(java.time包)的演进。本文将带你深入理解 Java 中的时间表示、常用操作以及最佳实践。
1.1 Unix Epoch 时间轴示意图(文字版描述)
<---|----------------|----------------|----------------|----------------|--->
1900 1940 1970 2000 2038
↑
Unix Epoch
(1970-01-01 00:00:00 UTC)
时间戳 = 0
左侧(1970 年前):时间戳为负数
例如:1960-01-01 → 时间戳 ≈ -315,619,200 秒
右侧(1970 年后):时间戳为正数
例如:2025-10-30 → 时间戳 ≈ 1,761,840,000 秒
注意最开始的时候并不是“所有计算机都从 1970 开始”——Windows 用的是 1601 年,但 Java 在跨平台时统一抽象为 Unix Epoch
Java 的 System.currentTimeMillis() 返回的是 毫秒级 Unix 时间戳,这是 Java 与底层系统时间交互的基础。
二,Java 的时间类 java.util.Date
java.util.Date 是 Java 早期(JDK 1.0 起)的时间类,但它对 时区、夏令时、闰秒、日历系统 的处理方式存在严重的设计缺陷和误解。理解它的行为,有助于明白为什么 Java 8 引入了全新的 java.time(JSR-310)API。
2.1 基本时间的获取
java.util.Date 的本质:只是一个时间戳
📌 核心事实:java.util.Date并不包含时区信息,它内部只存储一个 long 类型的毫秒数,表示 自 1970-01-01T00:00:00 UTC 起经过的毫秒数(即 Unix 时间戳 × 1000)。
package com.toast.others.utils;
import java.util.Date;
/**
* @author toast
* @time 2025/9/30
* @remark
*/
public class DateExample {
public static void main(String[] args) {
System.out.println("当前时间:" + new Date()); // 实际 = new Date(System.currentTimeMillis());
System.out.println("1970年:" + new Date(0));
System.out.println("1970年:" + new Date(-86400 * 1000)); //往前推一天(86400 秒),Date()传入的是毫秒
}
}
输出结果:
当前时间:Fri Oct 31 09:36:08 CST 2025
1970年:Thu Jan 01 08:00:00 CST 1970
1969年:Wed Dec 31 08:00:00 CST 1969
这意味着:
它表示的是 UTC 时间线上一个绝对瞬间(instant)
它没有时区、没有日历、没有“本地时间”概念。
2.2 各要素的具体处理情况
✅ 闰秒(Leap Seconds)
❓什么是闰秒
地球自转速度并不均匀(受潮汐、地震等影响),导致基于地球自转的 世界时(UT1) 与基于原子钟的 国际原子时(TAI) 逐渐偏差。
为保持民用时间(UTC)与太阳日基本同步,国际地球自转服务(IERS) 会在必要时在 UTC 中插入 闰秒:
- 通常在 6 月 30 日或 12 月 31 日的 23:59:60 增加一秒
- 例如:23:59:58 → 23:59:59 → 23:59:60 → 00:00:00(次日)
现状:
- 自 1972 年以来已添加 27 次正闰秒(截至 2025 年)
- 2035 年起,国际组织计划取消闰秒,改用新机制
对计算机的影响:
- 大多数操作系统和编程语言(包括 Java)不处理闰秒
- Unix 时间戳假设每分钟都是 60 秒,跳过或平滑处理闰秒
- 因此,
System.currentTimeMillis()在闰秒发生时可能重复或跳变
💡 关键点:闰秒是天文时间与原子时间的协调机制,但主流软件系统通常忽略它,以简化设计。
- 完全忽略。
- Java(包括
Date和整个 JVM)不支持闰秒。 - Unix 时间戳本身也不包含闰秒(POSIX 时间假设每分钟都是 60 秒)。
- 所以
Date与闰秒无关,也不受影响。
📝 结论:不处理,也不需要处理(符合主流系统实践)。
⚠️ 时区(Time Zone)
❓什么是时区
攻读过《初中地理》的同学都知道
地球被划分为 24 个主要时区(每 15° 经度约 1 小时),每个地区使用统一的本地时间,以便与太阳日同步。例如:
- 北京使用 东八区(UTC+8)
- 伦敦使用 格林尼治标准时间(UTC+0)
- 纽约使用 东部时间(UTC-5 或 UTC-4,取决于夏令时)
技术意义:
同一个绝对时刻(如 2025-10-30T12:00:00 UTC),在不同地区显示为不同的“本地时间”:
- 北京:20:00
- 伦敦:12:00
- 纽约:08:00(假设非夏令时)
💡 关键点:时区是“绝对时间” ↔ “人类可读本地时间”之间的转换规则。
Date本身不存储时区。- 但它的
toString()方法、getHours()、getYear()等已废弃方法,会隐式使用 JVM 默认时区来格式化或解析时间!
Date date = new Date(0); // 1970-01-01 00:00:00 UTC
System.out.println(date.toString());
// 输出示例(如果你在东八区):
// Thu Jan 01 08:00:00 CST 1970
👉 问题:同一个 Date 对象,在不同机器上 toString() 结果不同!这造成大量混乱。
📝 结论:看似有时区,实则没有;格式化时偷偷用了默认时区,极易误导开发者。
⚠️ 夏令时(Daylight Saving Time, DST)
❓什么是夏令时
某些国家/地区在夏季将本地时间人为拨快 1 小时,以更充分利用日光、节约能源。通常:
- 春季某日凌晨 2 点 → 跳到 3 点(“少睡 1 小时”)
- 秋季某日凌晨 2 点 → 回拨到 1 点(“多睡 1 小时”)
例子:
- 美国纽约:
- 标准时间:UTC-5(EST)
- 夏令时:UTC-4(EDT),3 月第二个周日开始,11 月第一个周日结束
对程序的影响:
- 同一个本地时间可能对应两个 UTC 时刻(如 2023-11-05 01:30 AM 出现两次)
- 某些本地时间根本不存在(如 2023-03-12 02:30 AM 直接跳到 03:30)
💡 关键点:夏令时让“本地时间”与“绝对时间”的映射变得非一一对应,极易引发时间解析错误。
Date本身不受 DST 影响(因为它只是 UTC 时间戳)。- 但当你用
SimpleDateFormat或旧Calendar将Date转为“本地时间”时,会应用当前时区的 DST 规则。
// 假设时区为 America/New_York(有 DST)
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
sdf.setTimeZone(TimeZone.getTimeZone("America/New_York"));
Date date = new Date(1678608000000L); // 2023-03-12 07:00:00 UTC
System.out.println(sdf.format(date)); // 可能输出 2023-03-12 03:00:00(DST 切换日!)
📝 结论:Date 本身不处理 DST,但与之配合的格式化/解析类会处理,行为依赖时区规则。
⚠️ 日历系统(Calendar System)
Date只支持公历(Gregorian calendar),且无法切换。更糟的是,它的
getYear()、getMonth()等方法:
getYear()返回的是 年份 - 1900(如 2025 年返回 125)
getMonth()返回 0–11(0 表示 January)这些方法早已被标记为
@Deprecated
Date date = new Date();
int year = date.getYear(); // 错误!已废弃,且结果 = 2025 - 1900 = 125
int month = date.getMonth(); // 错误!0 表示一月
实际的日历计算(如月份天数、星期几)是由 java.util.Calendar 完成的,而 Calendar 虽然支持多种日历(如 GregorianCalendar、JapaneseImperialCalendar),但与 Date 耦合混乱
📝 结论:Date 本身无日历概念;依赖 Calendar 实现,但设计割裂、易错。Date 是一个时间戳,但是 "Date" 这个名词让我们联想到日期,容易理解混淆。
🔚 总结:java.util.Date 对复杂时间要素的处理
|
闰秒 |
完全忽略(符合 POSIX 标准) |
✅ 无问题 |
|
时区 |
不存储,但 |
❌ 极易误导 |
|
夏令时 |
本身不处理,但格式化时依赖 |
⚠️ 行为不透明 |
|
日历系统 |
仅支持公历,且通过废弃方法暴露,需依赖 |
❌ 设计混乱 |
三,java.util.Date 的时间格式化
3.1 Date 的默认格式化
前面讲解了,当直接输出 Date 时间类的时候,Date.toString() 隐式使用默认时区。因为JVM会默认根据当前的时区处理时间的格式化
// 实际 = new Date(System.currentTimeMillis());
System.out.println("当前时间:" + new Date());
// 输出 当前时间:Thu Oct 30 17:43:18 CST 2025 JVM 默认的格式化
3.2 SimpleDateFormat 格式化
package com.toast.others.utils;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* @author toast
* @time 2025/10/30
* @remark
*/
public class DateFormatExample {
public static void main(String[] args) {
Date now = new Date(); // 当前时间
// 自定义格式:年-月-日 时:分:秒
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String formatted = sdf.format(now);
System.out.println(formatted);
// 其他常见格式
System.out.println(new SimpleDateFormat("yyyy/MM/dd").format(now));
System.out.println(new SimpleDateFormat("dd-MMM-yyyy").format(now));
System.out.println(new SimpleDateFormat("EEEE, MMMM dd, yyyy").format(now));
System.out.println(new SimpleDateFormat("HH:mm").format(now));
}
}
输出结果
2025-10-30 17:59:29
2025/10/30
30-10�?-2025
星期�?, 十月 30, 2025
17:59
如果有乱码的话,可能是时区不一样,一般是不会乱码的。毕竟默认就是获取的地区就是中文
中文 Windows 系统下,SimpleDateFormat 输出的月份(MMM)和星期(EEEE)显示为乱码(如 10?、星期?)。
这是因为 SimpleDateFormat 默认使用 JVM 的默认 Locale(区域设置),而你的系统 Locale 是中文(zh_CN),但格式模式中使用了 英文缩写(如 MMM 期望输出 "Oct",EEEE 期望输出 "Thursday"),而 JVM 在中文环境下尝试用中文资源去匹配英文格式,导致编码或资源查找异常,最终输出乱码。
解决方案:显式指定 Locale - 英文
package com.toast.others.utils;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
/**
* @author toast
* @time 2025/10/30
* @remark
*/
public class DateFormatExample {
public static void main(String[] args) {
Date now = new Date();
// 显式指定使用英文 Locale
SimpleDateFormat sdf1 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH);
System.out.println(sdf1.format(now));
System.out.println(new SimpleDateFormat("yyyy/MM/dd", Locale.ENGLISH).format(now));
System.out.println(new SimpleDateFormat("dd-MMM-yyyy", Locale.ENGLISH).format(now)); // 30-Oct-2025
System.out.println(new SimpleDateFormat("EEEE, MMMM dd, yyyy", Locale.ENGLISH).format(now)); // Thursday, October 30, 2025
System.out.println(new SimpleDateFormat("HH:mm", Locale.ENGLISH).format(now));
}
}
2025-10-30 18:04:30
2025/10/30
30-Oct-2025
Thursday, October 30, 2025
18:04
解决方案:显式指定 Locale - 中文
package com.toast.others.utils;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
/**
* @author toast
* @time 2025/10/30
* @remark
*/
public class DateFormatExample {
public static void main(String[] args) {
Date now = new Date();
// 显式指定使用中文 Locale
SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 EEEE HH:mm", Locale.CHINESE);
System.out.println(sdf.format(now));
System.out.println(new SimpleDateFormat("yyyy/MM/dd", Locale.ENGLISH).format(now));
System.out.println(new SimpleDateFormat("dd-MMM-yyyy", Locale.ENGLISH).format(now)); // 30-Oct-2025
System.out.println(new SimpleDateFormat("EEEE, MMMM dd, yyyy", Locale.ENGLISH).format(now)); // Thursday, October 30, 2025
System.out.println(new SimpleDateFormat("HH:mm", Locale.ENGLISH).format(now));
}
}
输出结果:
2025年10月30日 星期四 18:19
2025/10/30
30-Oct-2025
Thursday, October 30, 2025
18:19
3.3 指定时区格式化
package com.toast.others.utils;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;
/**
* @author toast
* @time 2025/10/30
* @remark
*/
public class DateFormatWithTimeZone {
public static void main(String[] args) {
Date now = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// 设置为 UTC
sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
System.out.println("UTC: " + sdf.format(now));
// 设置为上海(东八区)
sdf.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai"));
System.out.println("Shanghai: " + sdf.format(now));
// 设置为纽约(自动处理夏令时)
sdf.setTimeZone(TimeZone.getTimeZone("America/New_York"));
System.out.println("New York: " + sdf.format(now));
}
}
输出结果
UTC: 2025-10-30 10:24:27
Shanghai: 2025-10-30 18:24:27
New York: 2025-10-30 06:24:27
四,常见使用场景
在SpringBoot Web项目场景当中,时间数据请求的格式化处理
4.1 场景一:SpringBoot 2.6 版本前原生处理(基于 Jackson)
public class Event {
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
private LocalDateTime eventTime;
}
@JsonFormat
- 主要控制 输出(序列化)查询的时候,时间格式化由它生效
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
- 主要控制 输入 (序列化)新增,修改的时候,时间格式化有它生效
也就是说 springBoot 2.6 之前的原生版本时间格式化处理需要从输出和输入两个方向分别做处理
所以正在使用SpringBoot 2.6 之前版本同学的发现时间格式化查询生效,写操作不生效或者查询不生效,写操作等单方面生效。可以去检查检查
在 SpringBoot 2.6 + 版本(基于Jackson)之后的处理输入和输出都由一个注解完成
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime eventTime;
Jackson 会自动用该 pattern 同时处理输入和输出(前提是请求的时间数据格式匹配)。
4.2 场景二:SpringBoot 采用 使用 Fastjson 注解
使用 Fastjson 注解(@JSONField)的准备工作
public class Event { // 假设有一个实体类
private LocalDateTime eventTime;
// getter/setter
}
// 前端希望:
// 输入:{"eventTime": "2025-10-30 18:30:00"}
// 输出:{"eventTime": "2025-10-30 18:30:00"}
导入 fastjson2依赖
<!-- https://mvnrepository.com/artifact/com.alibaba.fastjson2/fastjson2 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.60</version>
</dependency>
禁用 Jackson,启用 Fastjson(配置类)
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter();
FastJsonConfig config = new FastJsonConfig();
config.setWriterFeatures(JSONWriter.Feature.WriteMapNullValue);
config.setReaderFeatures(JSONReader.Feature.SupportSmartMatch);
converter.setFastJsonConfig(config);
converters.clear();
converters.add(converter);
}
}
⚠️ 注意:Spring Boot 3+ 对 Fastjson2 支持更好,建议用 fastjson2。
import com.alibaba.fastjson2.annotation.JSONField;
import java.time.LocalDateTime;
public class Event {
@JSONField(format = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime eventTime;
// getter/setter
}
行为说明
@JSONField(format = "yyyy-MM-dd HH:mm:ss")
输出(序列化) --> LocalDateTime → "2025-10-30 18:30:00" ✅
输入(反序列化)--> "2025-10-30 18:30:00" → LocalDateTime ✅
如果你发现查询的时候,时间格式化生效,写操作的时候时间格式化却失败,报错显示时间数据为[null]。但是前端也确实将时间内容传入过来了。这个时候问题并不出现时间格式化。而是协议传输的方式不一致。
五,协议传输问题排查
5.1 先排查请求方式是否统一:是否前端统一的请求方式 GET, POST 等。
5.2 第二需要排查 Media types (MIME types) 客户端和服务器是否一致,在HTTP 协议上的体现就是 Content-type ;
如果你的请求方式是 content-type: application/json , 那么后端的接收方式需要使用@RequestBody
如果你的是请求方式是 content-type: application/x-www-form-urlencoded 那么后端的接收方式需要使用@ModelAttribute或者@RequestParam
【详情如下】
当前端通过 form-data 或 x-www-form-urlencoded 方式提交数据时,HTTP 请求头中的 Content-Type 会分别显示如下:
✅ 1. application/json
- 触发场景:前端提交方式是 application/json
- 请求头:
Content-Type: application/json
- 请求体示例:
{"eventTime": "2025-10-30 18:30:00"}
🔧 Spring Boot 后端接收方式:
必须使用 @RequestBody
@PostMapping("/events")
public ResponseEntity<?> createEvent(@RequestBody EventDTO event) {
// event 对象由 Jackson/Fastjson 自动反序列化
return ResponseEntity.ok().build();
}
✅ 2. application/x-www-form-urlencoded
- 触发场景:HTML 表单默认提交方式(
<form method="post">无enctype或显式指定enctype="application/x-www-form-urlencoded") - 请求头:
Content-Type: application/x-www-form-urlencoded
- 请求体示例:
username=admin&eventTime=2025-10-30+18%3A30%3A00
🔧 Spring Boot 后端接收方式:
直接使用 @RequestParam 或 实体类 + @ModelAttribute(不能用 @RequestBody):
@PostMapping("/submit")
public String handleForm(
@RequestParam String username,
@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime eventTime) {
// ...
}
或使用实体类:
public class EventForm {
private String username;
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime eventTime;
// getter/setter
}
@PostMapping("/submit")
public String handleForm(@ModelAttribute EventForm form) {
// Spring 会自动将 form-data 绑定到 form 对象
}
✅ 关键点:
- 使用
@DateTimeFormat(来自org.springframework.format.annotation)处理时间字符串 - 不要用
@RequestBody,因为x-www-form-urlencoded不是 JSON
✅ 3. multipart/form-data
- 触发场景:HTML 表单包含文件上传(
<form enctype="multipart/form-data">) - 请求头:
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
- 请求体示例(片段):
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="username"
admin
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="eventTime"
2025-10-30 18:30:00
------WebKitFormBoundary7MA4YWxkTrZu0gW
🔧 Spring Boot 后端接收方式:
同样使用 @RequestParam 或 @ModelAttribute,不能用 @RequestBody:
@PostMapping("/upload")
public String handleUpload(
@RequestParam String username,
@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime eventTime,
@RequestParam MultipartFile file) {
// ...
}
或用实体类(推荐):
public class UploadForm {
private String username;
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime eventTime;
private MultipartFile file;
// getter/setter
}
@PostMapping("/upload")
public String handleUpload(@ModelAttribute UploadForm form) {
// 自动绑定普通字段 + 文件
}
✅ 关键点:
- 文件字段用
MultipartFile - 普通字段仍用
@DateTimeFormat处理时间 - Spring Boot 默认支持
multipart,无需额外配置(除非大文件需调参数)
❌ 常见错误
// 错误!@RequestBody 用于 JSON(application/json)
@PostMapping("/submit")
public void bad(@RequestBody EventForm form) { ... }
@RequestBody依赖HttpMessageConverter(如 Jackson),只处理 JSON/XML 等格式x-www-form-urlencoded和multipart/form-data是 表单数据,由 Spring 的WebDataBinder处理,走@ModelAttribute/@RequestParam路径
SpringMVC 默认的请求方式
当你的请求接口编写的时候,并没有写任何的Annotation注解。
@RequestBody, @RequestParam, @ModelAttribute
public class UploadForm {
private String username;
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime eventTime;
private MultipartFile file;
// getter/setter
}
@PostMapping("/upload")
public String handleUpload(UploadForm form) { // 默认采用@ModelAttribute
// 自动绑定普通字段 + 文件
}
@GetMapping("/get-file")
public Map<String, Object> getFile(String filename) { // 默认采用@RequestParam
}
默认采用的是@RequestParam或者@ModelAttribute
📌 总结对比表
|
提交方式 |
|
后端注解 |
时间格式化 |
|
|
|
|
|
|
|
|
|
|
|
JSON(对比) |
|
|
|
💡 补充建议
- 如果你控制前端,优先使用 JSON(
application/json),配合@RequestBody+@JsonFormat,语义更清晰。 - 表单提交(尤其是带文件)才用
form-data或x-www-form-urlencoded。 - 时间字段务必用
@DateTimeFormat显式指定格式,避免依赖全局配置导致不一致。
5.3 多学一点系列:MIME type
MIME(Multipurpose Internet Mail Extensions,多用途互联网邮件扩展) 虽然最初是为电子邮件设计的,但在 Web(HTTP) 中扮演着至关重要的角色。它本质上是一种标识数据类型的标准机制,让客户端(如浏览器)和服务器能正确理解传输内容的格式。
🌐 MIME 在 Web 中的核心作用
在 HTTP 协议中,MIME 类型通过 Content-Type 响应头(或请求头)来声明资源的数据格式,从而指导浏览器如何处理接收到的内容。
示例:
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
→ 浏览器知道这是 HTML 文档,应渲染为网页。
Content-Type: application/json
→ 浏览器或 JavaScript 知道这是 JSON 数据,可用于 API 调用。
Content-Type: image/png
→ 浏览器知道这是 PNG 图片,应显示为图像。
📦 常见 Web MIME 类型示例
|
|
HTML 文档 |
|
|
CSS 样式表 |
|
|
JavaScript 脚本 |
|
|
JSON 数据(REST API 常用) |
|
|
XML 数据 |
|
|
各类图片 |
|
|
PDF 文件 |
|
|
未知二进制流(通常触发“下载”) |
🔧 MIME 如何影响 Web 行为?
- 浏览器渲染决策
若服务器返回
Content-Type: text/plain,即使内容是<h1>Hello</h1>,浏览器也会原样显示文本,而非渲染为标题。若返回
Content-Type: text/html,则会解析并渲染 HTML。
- AJAX / Fetch API 行为
fetch().json()要求响应头为application/json,否则可能抛出错误(取决于浏览器策略)。
- 文件下载 vs 预览
Content-Type: application/pdf→ 浏览器可能内嵌预览
Content-Type: application/octet-stream+Content-Disposition: attachment→ 强制下载
- 安全限制(MIME Sniffing)
为防止 XSS,现代浏览器会严格校验 MIME 类型
例如:若脚本请求返回
text/plain,即使内容是 JS 代码,浏览器也不会执行(除非明确声明Content-Type: application/javascript)
⚙️ 在 Spring Boot 中如何设置 MIME?
Spring Boot 会根据返回类型自动推断Content-Type:
- 返回
String→text/plain - 返回
@ResponseBody对象 →application/json - 返回
ResponseEntity<Resource>(如文件)→ 根据文件扩展名自动设置(如.png→image/png)
你也可以显式指定:
@GetMapping(value = "/data", produces = "application/json;charset=UTF-8")
public MyData getData() {
return new MyData();
}
@GetMapping("/download")
public ResponseEntity<Resource> download() {
Resource file = ...;
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=file.pdf")
.body(file);
}
📌 总结:MIME 在 Web 中的关键意义
MIME 类型是 HTTP 通信中“数据语义”的桥梁。
它确保:
- 服务器能准确描述发送的内容是什么;
- 客户端能正确解析、渲染或处理这些内容;
- 整个 Web 生态(HTML、CSS、JS、API、文件传输)能协同工作。
没有 MIME,浏览器将无法区分一段文本是“代码”、“文章”,还是“配置文件”——Web 将陷入混乱。
六,java.util.Date 和 java.sql.Date 区别
java.util.Date 和 java.sql.Date 虽然名字相似,但设计目的、使用场景和行为有本质区别。理解它们的差异对 Java Web 开发(尤其是数据库交互)非常重要。
📌 一、核心区别概览
|
特性 |
|
|
|
所属包 |
|
|
|
设计目的 |
表示一个时间点(毫秒时间戳),包含日期 + 时间 |
专为 SQL DATE 类型 设计,仅表示日期(年月日) |
|
是否包含时间部分 |
✅ 包含(时分秒毫秒) |
❌ 理论上不包含(但底层仍继承自 |
|
是否可直接用于 JDBC |
⚠️ 可用但不推荐(语义不清) |
✅ 推荐用于 |
|
是否推荐在新项目中使用 |
❌(已被 |
❌(JDBC 4.2+ 推荐直接用 |
重点:不管是Date 还 Java.sql.Date 都被Java8 新日期给替代了。所以仅作了解
🧱 二、详细说明
1. java.util.Date
- 本质:一个包装了 long 毫秒值 的类,表示自 1970-01-01T00:00:00 UTC 起的绝对时间点。
- 包含完整时间信息:
Date now = new Date(); // 例如:Thu Oct 30 19:30:45 CST 2025
- 问题:
-
- 名字叫 “Date”,却包含时间,语义误导。
- 线程不安全(
SimpleDateFormat相关)。 - 方法如
getYear()返回year - 1900,设计反人类。
- 用途:早期 Java 时间处理的通用类(现已过时)。
2. java.sql.Date
- 继承关系:
public class Date extends java.util.Date
⚠️ 虽然继承自 java.util.Date,但语义上只应表示“日期”(年月日)。
- 设计初衷:与 SQL 的
DATE类型 对应(SQLDATE通常只存年月日,如2025-10-30)。 - 关键行为:
-
- 构造时会“清零”时间部分(时分秒毫秒设为 0):
long now = System.currentTimeMillis();
java.sql.Date sqlDate = new java.sql.Date(now);
// sqlDate 内部毫秒值被调整为当天 00:00:00.000 UTC
-
- 但 toString() 仍可能显示时间(因继承自
java.util.Date,受默认时区影响):
- 但 toString() 仍可能显示时间(因继承自
System.out.println(sqlDate); // 可能输出:2025-10-30(理想)或 2025-10-29(时区问题!)
- JDBC 使用示例:
PreparedStatement ps = conn.prepareStatement("INSERT INTO events(event_date) VALUES (?)");
ps.setDate(1, new java.sql.Date(System.currentTimeMillis())); // ✅ 正确
⚠️ 三、常见陷阱
陷阱 1:java.sql.Date 仍有时区问题
// 假设系统时区是 Asia/Shanghai (UTC+8)
long utcMidnight = ...; // 2025-10-30 00:00:00 UTC
java.sql.Date d = new java.sql.Date(utcMidnight);
System.out.println(d); // 可能显示 2025-10-30 或 2025-10-29!
因为 toString() 使用本地时区解析毫秒值,而 SQL 存储的是无时区日期,容易造成显示不一致。
陷阱 2:误将 java.util.Date 当“纯日期”用
// 错误:用 util.Date 表示“生日”
Date birthday = new Date(); // 包含当前时分秒!
应使用 java.time.LocalDate。
✅ 四、现代 Java(Java 8+)的正确做法
从 JDBC 4.2(Java 8+)开始,直接支持 java.time 类型,无需再用 java.sql.Date:
|
SQL 类型 |
推荐 Java 类型 |
JDBC 方法 |
|
|
|
|
|
|
|
同上 |
|
|
|
同上 |
|
|
|
同上 |
示例(推荐):
// 插入
LocalDate eventDate = LocalDate.of(2025, 10, 30);
ps.setObject(1, eventDate);
// 查询
LocalDate date = rs.getObject("event_date", LocalDate.class);
✅ 优势:
- 语义清晰(
LocalDate就是纯日期) - 无时区干扰
- 不可变、线程安全
- 无需在
util.Date和sql.Date之间转换
📝 总结
|
场景 |
推荐方案 |
|
新项目(Java 8+) |
✅ 直接用 |
|
维护旧代码(JDBC < 4.2) |
⚠️ 用 |
|
绝对不要 |
❌ 混淆两者,或用 |
💡 记住:
java.util.Date= 时间戳(带时间)java.sql.Date= SQL 日期(理论上无时间,但有历史包袱)- 现代开发:忘掉它们,拥抱
java.time
更多推荐


所有评论(0)