做Java Web的老哥应该都有过这种经历页面明明只想显示一个上市日期、创建时间结果 JSP 里直接输出来的却是Thu Jun 06 16:32:08 CST 2024这种让人眼前一黑的东西。想格式化一下吧SimpleDateFormat的格式串写错一个字母出来的日期就完全不是那么回事想用 JSTL 吧标签库没引对页面直接报 500。其实 JSP 日期处理说白了就是一句话把服务端拿到的日期对象用你想要的格式安全地送到页面上。但这中间涉及的 API、标签、时区、类型互转每一个环节都能给你挖坑。这篇我把自己在 JSP 项目里玩日期的那套东西整理出来包括 Scriptlet 硬格式化的姿势、JSTL 模板标签的优雅写法、字符串和 Date 和时间戳的三方转换、日期加减和比较的实用套路还有我这些年踩过的各种奇葩坑。无论你是刚学 JSP 做课程设计还是在维护十年前的遗留项目都可以直接照着抄。1. 先搞清楚 JSP 里的日期从哪来服务端对象和页面输出的基本关系1.1 JSP 是服务端渲染你拿到的永远是服务器的时间很多新手容易忽略一件事JSP 页面是在服务器上执行完再输出 HTML 给浏览器的。所以你在 JSP 的 Java 代码块里new Date()拿到的是服务器当前时间跟用户浏览器的本地时间一毛钱关系都没有。这个点平时在本地开发看不出问题一旦项目部署到云服务器而服务器时区不是北京时间页面上显示的 “当前时间” 就会跟用户手机上的时间差好几个小时。这就是为什么我后面专门会讲时区配置这是 JSP 日期处理里最容易翻车、但查起来又最隐蔽的问题。1.2 日期在 JSP 页面里的三种存在形式我在实际项目里碰到的日期数据无非三种形态String 字符串从request.getParameter(birthday)拿到的表单值、从数据库varchar字段查出来的日期字符串统统是这种。java.util.Date / java.sql.Date / java.sql.Timestamp从数据库日期字段映射出来的、或者业务层new Date()出来的标准日期对象。long 时间戳System.currentTimeMillis()返回的毫秒值很多老系统喜欢用bigint存时间查出来就是一个数字。这三者之间的转换基本覆盖了 JSP 日期处理 90% 的场景。搞清楚当前数据是哪一种形态再决定用格式化、解析还是直接输出思路就清晰了。1.3 为什么不能直接out.println(date)给用户看原因很简单Date.toString()输出的格式不适合展示。它输出的是Tue Apr 11 10:23:45 CST 2025这种带英文缩写、带时区的形式中文用户看不懂也不符合大多数业务系统的展示要求比如2025-04-11 10:23:45。更麻烦的是Date.toString()的输出格式在不同 JDK 版本、不同 Locale 环境下不完全一致你要是写测试用例去断言它很容易被环境差异坑到。所以我的习惯是任何日期在到达页面边界之前统一转成字符串绝不让Date对象直接进 JSP 输出。2. Scriptlet 直接处理日期SimpleDateFormat 和 Calendar 的三种常用姿势虽然 JSP 规范早就不建议在页面里大量写 Java 代码但现实是很多老项目里全是% ... %小脚本。既然碰到了就得会改、会写。这里讲三种我自己用得最多的 Scriptlet 日期处理方式。2.1 格式化当前时间SimpleDateFormat 的基本操作最基础的场景页面上显示 “今天是哪年哪月哪日几点几分”。% page importjava.util.Date, java.text.SimpleDateFormat % % Date now new Date(); SimpleDateFormat sdf new SimpleDateFormat(yyyy-MM-dd HH:mm:ss); String nowStr sdf.format(now); % p系统当前时间% nowStr %/p输出结果就是标准的2025-06-06 14:30:25。这里有几个格式字符必须刻在脑子里字母含义易错点yyyy四位数年份别写成YYYY后面专门讲MM月份01~12大写跟分钟区别开dd日01~31小写HH24小时制小时大写是 0~23hh12小时制小时小写是 0~11下午会变成 1 点看起来像凌晨mm分钟小写跟月份区别ss秒我见过太多次把HH:mm写成hh:mm结果下午三点显示成03:00的 bug。格式串错了页面不会报错只会给你一个看起来很合理但实际错得离谱的结果这种 bug 最难查。2.2 动态获取年月日时分秒用 Calendar 而不是 Date 的老方法有时候不只要格式化整个时间而是想单独拿年份、月份、日期来拼页面结构。这个时候Calendar是更顺手的选择。% Calendar cal Calendar.getInstance(); int year cal.get(Calendar.YEAR); int month cal.get(Calendar.MONTH) 1; int day cal.get(Calendar.DAY_OF_MONTH); int hour cal.get(Calendar.HOUR_OF_DAY); int minute cal.get(Calendar.MINUTE); int second cal.get(Calendar.SECOND); int week cal.get(Calendar.DAY_OF_WEEK); // 注意1是周日2是周一 %这里有个天坑Calendar.MONTH是从 0 开始的。一月份返回的是 0十二月返回的是 11。你要是忘了加 1页面上永远显示 “现在是 0 月”而且这个错误不会报异常特别阴险。DAY_OF_WEEK也一样返回值里 1 代表星期日2 代表星期一跟中国的习惯不一样做 “周一” 到 “周日” 的显示映射时记得要处理。2.3 格式化数据库查询结果先拿 Date 再转 String从数据库查出日期字段比如订单创建时间在 JSP 里展示前先格式化成字符串。% Date createTime (Date) request.getAttribute(orderCreateTime); SimpleDateFormat sdf new SimpleDateFormat(yyyy-MM-dd); String createTimeStr (createTime null ? : sdf.format(createTime)); % p创建日期% createTimeStr %/p注意我做了空值判断。数据库里datetime允许为空查出来是null直接sdf.format(null)会抛NullPointerException页面直接 500。这种“查询正常但页面崩了”的情况十次里有八次是日期字段没判空。2.4 一个原则尽量在服务端转好让 JSP 只负责输出在实际操作中我总结出一个原则JSP 里的 Java 代码越少越好尤其是日期格式化这种逻辑最好在 Servlet 或 Controller 层提前转好字符串放进 request 作用域JSP 只负责展示。这样有几个好处一是页面代码清爽二是日期格式统一在 Java 层维护三是不容易在 JSP 里写出线程不安全或者异常处理不到位的代码。老项目里那种满屏%的页面我都是慢慢往这个方向重构的。3. JSTL fmt 标签页面模板里更优雅的日期格式化写法如果你能控制项目的依赖和页面写法那就别再 Scriptlet 硬干了。JSTL 的fmt标签库是 JSP 页面格式化日期的标准方案它能在不写一行 Java 代码的情况下完成格式化和解析。3.1 引入标签库的三个步骤要在 JSP 里用fmt标签首先要引入 JSTL 标签库。% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt %依赖配置项目中要有jstl.jar和standard.jar或者 Maven 项目引入dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency这里有个版本坑Java EE 8 及以前用javax.servlet下的 JSTL如果是 Jakarta EE 9比如 Tomcat 10 以上包名变成了jakarta.servlet.jsp.jstl对应的 JAR 也要换。你如果用的是老 JSTL 1.2 的 JAR 部署到 Tomcat 10页面运行时会报Unable to find taglib或者直接 500。网上很多教程没提这点我当年升级 Tomcat 时就被坑了一下午。3.2 fmt:formatDate格式化显示的主角fmt:formatDate value${order.createTime} patternyyyy-MM-dd HH:mm:ss /value可以传一个java.util.Date对象也可以传一个long类型的毫秒时间戳。如果要把格式化结果存起来复用用var和scopefmt:formatDate value${order.createTime} patternyyyy-MM-dd varcreateDateStr / p下单日期${createDateStr}/p如果传进来的值是字符串比如从 request 参数里拿的那fmt:formatDate有时候也能解析一部分但前提是字符串符合SimpleDateFormat能识别的格式。为了避免不确定性我建议先用fmt:parseDate转成 Date再走formatDate。3.3 fmt:parseDate字符串转 Date 的模板方案解析前端传来的日期字符串可以这样写fmt:parseDate value${param.startDate} patternyyyy-MM-dd varstartDate /经过这一行startDate就成了一个Date对象你可以再拿去做格式化、比较、或者传给后台。这里最容易踩的坑是pattern必须跟value字符串的实际格式完全匹配。你写patternyyyy-MM-dd传进来的是2025/06/01结果就是转换失败或者拿到一个解析不完全的 Date。parseDate转换失败时行为还比较奇怪有时候会抛异常有时候会把能解析的部分解析掉、剩下忽略掉所以你最好在传参前就校验好格式别指望解析阶段兜底。3.4 fmt:timeZone页面级别的时区修正这个是 fmt 标签库被很多人忽略但又特别实用的能力。fmt:timeZone valueGMT8 fmt:formatDate value${createTime} patternyyyy-MM-dd HH:mm:ss / /fmt:timeZone当你遇到服务器时区不对、又不能马上重启 JVM 的情况可以在 JSP 页面里用fmt:timeZone临时指定时间区让格式化输出在正确的时区下进行。虽然治标不治本但它是紧急好用的冰敷贴。4. 字符串、时间戳、Date 对象三方互转与表单日期传参实战这一节是 JSP 日期处理里操作频率最高的部分因为网页交互免不了表单提交、显示、入库这几个动作。4.1 表单提交的日期字符串如何转成 Date 入库用户在前端选了一个 “出生日期”input typedate提交上来的值在request.getParameter(birthday)里是2025-06-01这样的字符串。你不可能直接把字符串塞给数据库的datetime字段得先转成java.util.Date再转成java.sql.Date。String birthdayStr request.getParameter(birthday); if (birthdayStr ! null !birthdayStr.trim().isEmpty()) { SimpleDateFormat sdf new SimpleDateFormat(yyyy-MM-dd); java.util.Date utilDate sdf.parse(birthdayStr); java.sql.Date sqlDate new java.sql.Date(utilDate.getTime()); // 然后 setDate 到 PreparedStatement }java.sql.Date是java.util.Date的子类但它重写了toString()只保留日期部分。当你要写数据库时直接用new java.sql.Date(utilDate.getTime())去构造别在java.util.Date和java.sql.Date之间强制转换会丢失时间信息或者报ClassCastException。这里还必须处理ParseException。用户输入的格式跟pattern对不上sdf.parse()就会抛异常。在实际项目中我一般这样防前端用input typedate统一下发格式后端先用正则校验^\d{4}-\d{2}-\d{2}$解析抛ParseException时给用户回一个友好的错误提示而不是直接 500。4.2 时间戳和 Date 对象的互转时间戳和 Date 的互转是最简单也最容易被忽略的一种// Date - 时间戳 long ts new Date().getTime(); // 时间戳 - Date Date date new Date(ts);很多老系统数据库里用bigint存秒级时间戳10位而 Java 的getTime()返回的是毫秒级13位。换算关系是long seconds System.currentTimeMillis() / 1000; // 13位转10位 Date date new Date(seconds * 1000L); // 10位转Date注意乘以1000L那个1000L的 L 别漏掉漏了的话seconds * 1000会按 int 计算超出范围溢出你得到的日期会变成一个诡异的值。4.3 页面显示时间戳的两种方式从接口拿到的时间戳要展示在 JSP 上我一般两种做法在 Servlet/后端先new Date(timestamp)转成 Date再用fmt:formatDate格式化直接在 JSP 里用 EL 表达式配合fmt标签fmt:formatDate value${order.createTime} patternyyyy-MM-dd HH:mm:ss /fmt:formatDate对Long类型的时间戳是可以直接处理的这省掉了不少 Scriptlet 转换代码。4.4 日期字符串和 Date 的“不严格”转换要小心在 JSP 里用parseDate解析2025-6-1月份、日没有前导零这种字符串时SimpleDateFormat默认是宽松解析模式它可能接受2025-13-40这种无效日期并且自动进位变成 2026-02-09。这就是为什么我强烈建议在表单日期传参时要先校验再解析别完全依赖SimpleDateFormat和parseDate的容错能力。5. 日期加减与边界计算日常业务里真正的核心逻辑光会格式化不算会处理日期。业务系统里最常见的需求其实是算出这个日期的 7 天后是哪天、两个日期相差几天、某个月的一号和最后一天。这节讲我实际项目里用得最多的几个计算套路。5.1 用 Calendar 加天数、加月数给日期加 N 天首选Calendar.add它可以自动处理跨月、跨年。SimpleDateFormat sdf new SimpleDateFormat(yyyy-MM-dd); Date publishDate sdf.parse(2025-01-30); Calendar cal Calendar.getInstance(); cal.setTime(publishDate); cal.add(Calendar.DAY_OF_MONTH, 7); // 加7天 Date expireDate cal.getTime(); cal.add(Calendar.MONTH, 1); // 再往后推1个月注意这里用的是add而不是set。set(Calendar.DAY_OF_MONTH, 7)是无脑把 “日” 字段改成 7不改动月份和年份而add会带着进位逻辑去计算1月30日加7天会正确得到2月6日。5.2 两个 Date 比较早晚before、after 还是 compareTo比较两个日期谁先谁后三种方式boolean isBefore date1.before(date2); boolean isAfter date1.after(date2); int result date1.compareTo(date2); // 负数/0/正数compareTo返回负值、0、正值分别表示date1在date2之前、相等、之后。这里有个细节Date的比较精确到毫秒两个时间如果一个到秒、一个到纳秒即使 “同一天”compareTo也可能不等于 0。所以做 “是否同一天” 这种判断时先清零时分秒再比或者比较格式化后的yyyy-MM-dd字符串。5.3 计算两个日期相差的天数这是我最常写的工具方法之一比如算会员到期还剩几天long diff expireDate.getTime() - now.getTime(); long days diff / (24 * 60 * 60 * 1000);注意两点结果是整数截断不是四舍五入。如果你想要 “不满一天算一天”要改成(diff 24*60*60*1000 - 1) / (24*60*60*1000)即向上取整。毫秒差里有时区漂移和夏令时的问题。在中国没有夏令时一般没事但如果你面对的是全球化的业务最好用Calendar先把两个日期都归零到当天 0 点再取毫秒差。5.4 月初、月末和定点定时场景报表场景里经常要拿 “本月第一天”“本月最后一天” 去做查询范围Calendar cal Calendar.getInstance(); // 本月第一天 cal.set(Calendar.DAY_OF_MONTH, 1); Date firstDay cal.getTime(); // 本月最后一天 cal.set(Calendar.DAY_OF_MONTH, cal.getActualMaximum(Calendar.DAY_OF_MONTH)); Date lastDay cal.getTime();getActualMaximum(Calendar.DAY_OF_MONTH)会自动根据月份判断 28、29、30 还是 31比你自己写switch判断闰年靠谱多了。另外做定时任务或查询 “近24小时” 这种需求我习惯用cal.add(Calendar.HOUR_OF_DAY, -24)而不是cal.add(Calendar.DAY_OF_MONTH, -1)。这两个在跨夏令时地区不一样在中国绝大多数场景等价但我还是建议按业务单位精确到小时或天数去加避免理解偏差。6. 我踩过的那些日期坑时区、线程安全与格式串陷阱这一节我用亲身踩过的坑来写每个都是真实项目里出过问题的点。JSP 日期处理看似简单但坑基本都藏在细节里。6.1 SimpleDateFormat 是线程不安全的别定义成 Servlet 成员变量这是 Java 日期处理里最经典的一个坑在 Servlet 里尤其严重。以前我图省事在 Servlet 类里定义一个全局的SimpleDateFormatpublic class OrderServlet extends HttpServlet { private SimpleDateFormat sdf new SimpleDateFormat(yyyy-MM-dd HH:mm:ss); // ... }Servlet 是单实例多线程的sdf被所有请求线程共享。SimpleDateFormat内部用一个Calendar对象来做解析和格式化多线程并发调用时会出现短暂的数据错乱表现出来的症状很吓人同一个请求里格式化出来的年份是 2025分钟是 86甚至出现NullPointerException。正确做法有几种每次需要时new SimpleDateFormat(...)用完即弃。JSP/Java 后台这种低频场景这点性能损耗完全可忽略推荐。用ThreadLocalSimpleDateFormat给每个线程一份。用 Java 8 的DateTimeFormatter它是线程安全的但是老项目如果还在 JDK 8 以下就不太方便。6.2 大写 YYYY 还是小写 yyyy跨年那天突然出现的幽灵日期这个坑我记一辈子。有次客户反馈2024 年 12 月 30 日页面上显示的下单日期变成了 2025-12-30。查了半天发现是有人把格式串写成了YYYY-MM-dd。YYYY大写是week-based-year基于周的年份它表示的是一年中某周所属的年份而不是自然年份。2024 年 12 月 30 日那一周绝大部分天数已经落在 2025 的周历里所以用YYYY格式化就变成了 2025。结论很简单年份一律用yyyy小写只有你明确需要周历场景才用YYYY。顺手检查一下项目里所有SimpleDateFormat和fmt:formatDate的 pattern这种 bug 通常在每年 12 月底和 1 月初集中爆发。6.3 服务器时区不对页面时间差了 8 小时JSP 页面里new Date()的格式化结果取决于 JVM 的默认时区。很多云服务器默认时区是 UTCJVM 没设置user.timezone的话页面上显示的北京时间会慢 8 小时。排查方法分两层JVM 层启动参数加-Duser.timezoneAsia/Shanghai或者在代码的静态块里TimeZone.setDefault(TimeZone.getTimeZone(Asia/Shanghai))。数据库连接层如果是 MySQL 8JDBC URL 上建议加serverTimezoneAsia/Shanghai否则驱动拿到的时间可能跟你本地时间不一致。在 JSP 页面上用fmt:timeZone做临时修正也是一种应急手段但根子还是要保证 JVM 时区正确。6.4 Java 8 都出了 DateTimeFormatter但老项目还被 Date 绑着我知道现在 Java 8 的java.time包让日期处理舒服了很多但 JSP 老项目里到处都是java.util.Date数据库驱动方法参数也要java.sql.Timestamp所以这篇我大量写了基于SimpleDateFormat和Calendar的用法——不是我守旧是在老项目里java.time和旧 API 之间的转换反而更费劲。如果你的项目能升级我建议核心业务逻辑里用LocalDateTime只有到 JSP 页面边界时才转成java.util.Date或者字符串这样两头都舒服。6.5 JSP 给 JavaScript 传日期时的格式与兼容性还有一种情况JSP 页面里输出一个日期字符串给 JavaScript 的new Date()去解析。var date new Date(2025-06-06 10:30:00);这在 Chrome 里没问题但在 iOS 的 Safari 上带空格的日期字符串解析会返回Invalid Date。稳妥的写法是服务端输出 ISO 8601 格式用T分隔日期和时间或者干脆输出毫秒时间戳让 JS 自己new Date(timestamp)var date new Date(1749166200000);这是我在移动端页面上踩过的最莫名其妙的坑之一——同一份 JSP电脑上一切正常手机上全是NaN。6.6 再补一句日期格式化测试要写边界用例最后分享一个小习惯任何跟日期相关的 JSP 功能我提测前都会自己过一遍这些边界用例2024-12-30用YYYY格式化会不会变 20252025-02-28加一天是不是2025-03-01空日期null会不会让页面 500用户输入2025-13-01会不会报错或者自动进位服务器时区改成 UTC 后页面时间是否偏移hh:mm:ss和HH:mm:ss在下午 3 点的表现。这些用例看起来不起眼但 JSP 日期处理的线上事故我见过的基本都在这几个点上。把边界测一遍比写多少代码都管用。