在实际的Java Web项目里,JSP页面上的日期处理是我见过翻车最多的基础功能之一。明明只是一个日期格式化,愣是能搞出时区错乱、年份少一年、英文月份、性能慢到页面崩溃等各种事故。这个标题看起来简单,背后牵扯的东西却不少:后端Java代码里怎么取时间、传参格式怎么统一、JSP页面上怎么显示、遇到null怎么处理、日志里怎么排查。这篇文章我先把JSP日期处理的完整技术链路拆开,再给出一套能直接抄作业的方案,适合刚入门的Java Web开发者和正在做传统JSP项目毕设的同学参考。
1. JSP日期处理的整体思路拆解:先搞清楚日期在哪里被处理
1.1 一个日期在JSP项目中要经过的三层处理
很多新手拿到“JSP日期处理”这个需求,第一反应就是在JSP页面里写死一个日期格式,或者翻出SimpleDateFormat直接在页面里new一个对象。这种做法能跑,但通常撑不过第二个需求。
一个日期从数据库到用户浏览器,至少要经过三个位置:后端Java代码里取数、赋值到实体/封装对象、JSP页面上渲染输出。严格来说还可能经过前端JavaScript二次格式化,以及浏览器最终显示这一步。任何一个环节的格式约定不一致,页面上的日期就会“看起来不对劲”。
我的习惯是这样的:后端负责标准化(统一存时间戳或标准格式字符串),JSP页面负责展示格式化(用JSTL标签或EL表达式来做),前端交互(比如日期选择器、倒计时)用JavaScript单独处理。这样一层归一层,排查问题的时候只需要按链路定位。
比如说用户注册时间,数据库里存的是DATETIME类型,MyBatis查出来映射成java.util.Date,传给JSP。如果JSP页面直接用${user.regTime}输出,出来的是一串“Sat Jun 10 10:00:00 CST 2023”这种英文+星期的格式,丑到没法看。这时候就需要在页面层做格式化,或者在实体类里提前格式化好。
1.2 为什么不用页面内嵌Java代码来处理日期
JSP页面内嵌<% %>Java代码是很多老教程的写法,比如在页面里写一个<% SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); %>这样的片段。我不推荐这么干,原因有三个。
第一,JSP本质上是视图层,职责是展示,塞入大量Java代码会让页面变得极其难以维护。第二,SimpleDateFormat本身有线程安全问题,在多线程访问同一实例时会出数据错乱,而JSP页面普遍被多个请求共享,很容易踩中这个雷。第三,页面里写Java代码无法利用JSTL标签和EL表达式的便捷性,代码复用率低。
正确做法是:JavaBean在后台准备数据,JSP页面用EL表达式${}取值,用JSTL的fmt:formatDate标签做格式化。EL表达式处理null值比较友好,空值输出是一个空串而不是“null”字符串,这一点对用户体验影响很大。
1.3 JSP项目里日期处理的五种常见场景
结合我在实际项目中遇到的情况,JSP日期处理大致分成以下五类场景,每类的处理方案略有不同:
| 场景类型 | 典型需求 | 推荐处理位置 |
|---|---|---|
| 列表页展示 | 订单时间、文章发布时间,精度到分钟或日期即可 | JSP页面用fmt:formatDate格式化为“yyyy-MM-dd HH:mm” |
| 详情页展示 | 用户详情、商品详情,可能要“yyyy年MM月dd日” | 后端在实体类提供格式化的getter方法,或者JSP格式化 |
| 表单提交 | 用户填的生日、预约日期,需要把字符串解析成日期入库 | 后端接收String,用DateTimeFormatter或SimpleDateFormat解析 |
| 日期范围查询 | 按开始日期和结束日期过滤列表 | 后端接收字符串后转成时间范围,注意“结束日期要加一天”的坑 |
| 动态显示 | 倒计时、实时时间 | 前端JavaScript处理,JSP只负责输出初始值 |
我在做“JSP个人信息展示页面”这个热门需求时,就是按照这五类场景逐一处理的。个人信息页往往同时涉及生日显示、注册时间显示、最近登录时间显示等,正好涵盖了列表页展示和详情页展示两类,格式化要求还不一样,是练习日期处理的好素材。
2. 后端Java日期处理的核心代码与原理
2.1 实体类日期字段的最佳类型选择与理由
很多JSP项目的实体类里日期字段还在用java.util.Date,这倒不是说不能用,但在新写的代码里我更推荐用java.time.LocalDate、LocalDateTime或者Instant,这是JDK 8及以后版本提供的现代时间API。
用传统Date类型的问题在于:它不代表一个具体的日期,而是一个时间戳的毫秒数,打印出来是“Sat Jun 10 10:00:00 CST 2023”这种格式,不具备任何自解释性。而且Date的很多方法(比如getDay()、getYear())已经废弃,命名混乱,内部还包含时区信息,容易在序列化和国际化时搞出幺蛾子。
LocalDate只代表年月日,适用于生日、节假日这类不需要时分秒的数据;LocalDateTime代表年月日时分秒,适用于订单时间、操作日志等;Instant是时间线上的一个点,适用于需要跨时区比较的时间。用这个组合,类型本身就说明了语义,代码的可读性大幅提升。
比如用户实体类里: public class User { private LocalDate birthday; // 生日,只需要年月日 private LocalDateTime regTime; // 注册时间,需要精确到秒 private LocalDateTime lastLoginTime; // 最近登录时间 }
2.2 SimpleDateFormat的线程安全问题详解
如果你还在用SimpleDateFormat,一定要知道它有一个经典问题:线程不安全。SimpleDateFormat内部维护了一个Calendar对象,format和parse操作都会修改这个Calendar的状态。当多个线程共享同一个SimpleDateFormat实例时,一个线程正在解析日期,另一个线程把Calendar改成别的值了,前面那个线程就会拿到错误的结果。
我见过一个生产事故:一个JSP列表页每5秒自动刷新一次,部署在多线程容器里,页面上的时间偶尔会变成“2023年6月30日星期六”或者干脆解析异常抛到页面上。排查了一上午,最后定位到是两个请求共用了Servlet中静态的SimpleDateFormat实例。
解决方案有两个方向。一是每次用时new一个实例,这种最简单,适合并发量不高的场景;二是用ThreadLocal包装一下,每个线程持有自己的一份实例,适合高并发的场景。在JSP项目里我通常建议直接换成DateTimeFormatter,它本身就是线程安全的,不需要额外的包装。
2.3 基于JDK 8+的日期格式化与解析实操
JDK 8之后推荐用DateTimeFormatter来处理日期格式化和解析。它线程安全,API语义清晰,和LocalDateTime配合非常顺手。
格式化输出当前时间: LocalDateTime now = LocalDateTime.now(); DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); String text = now.format(formatter); // 输出:2025-01-15 14:30:22
解析字符串为日期对象: String birthdayStr = "1998-08-20"; DateTimeFormatter pattern = DateTimeFormatter.ofPattern("yyyy-MM-dd"); LocalDate birthday = LocalDate.parse(birthdayStr, pattern); // 得到 LocalDate 对象,可以直接存库
解析时有一个容易忽略的点:如果字符串自带时分秒但你的格式串里没写,会出现DateTimeParseException。反过来也一样。所以格式串和实际字符串必须严格对应。处理用户表单输入时,这种问题尤其常见,因为用户可能输入“2023/06/10”、“2023年6月10日”、“2023-6-10”等各种奇奇怪怪的格式。
我一般会做一个统一的日期解析工具类,内部支持多种格式匹配: public static LocalDate parseFlexibleDate(String dateStr) { List patterns = Arrays.asList("yyyy-MM-dd", "yyyy/MM/dd", "yyyy年MM月dd日", "yyyy-M-d"); for (String p : patterns) { try { DateTimeFormatter f = DateTimeFormatter.ofPattern(p); return LocalDate.parse(dateStr, f); } catch (DateTimeParseException ignored) { } } throw new IllegalArgumentException("无法解析的日期格式: " + dateStr); }
这样遇到用户输入不规范的情况,不至于直接400报错给用户,而是能兜住大多数常见写法。
2.4 常用日期操作:比较、计算与前后端传输
日期处理不只是格式化,还包括比较和计算。比如判断用户注册满一年了吗,计算合同到期还有多少天,这些场景在JSP后台逻辑中也很常见。
LocalDate和LocalDateTime都实现了Comparable接口,直接用isBefore、isAfter、isEqual比较即可: LocalDate deadline = LocalDate.of(2025, 6, 1); if (LocalDate.now().isAfter(deadline)) { // 已过期 }
日期计算用plusDays、plusMonths、minusDays等方法,返回新对象,原对象不变: LocalDate today = LocalDate.now(); LocalDate oneMonthLater = today.plusMonths(1); long daysBetween = ChronoUnit.DAYS.between(today, oneMonthLater);
这个方法很实用:比如做会员到期提醒时,判断“距离到期还有几天”,直接拿到期日和今天做差值,ChronoUnit.DAYS.between会返回一个long类型的相差天数。
前后端传输日期时,我建议后端统一输出两种格式:一种是人看的(YYYY-MM-DD HH:mm:ss),用于页面展示;一种是时间戳数字(epoch millis),用于JavaScript做日期计算。这样前端拿到时间戳,想怎么格式化都行,不受时区干扰。
3. JSP页面上的日期显示与格式化技巧
3.1 JSTL fmt:formatDate标签的使用与属性详解
如果项目还在用JSP且引入了JSTL,页面上的日期格式化推荐直接用fmt:formatDate标签,这是最标准、最省事的方案。
首先要在页面顶部引入JSTL核心库和格式化库: <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
然后在需要显示日期的地方: <fmt:formatDate value="${user.regTime}" pattern="yyyy-MM-dd HH:mm:ss" />
fmt:formatDate的属性主要有这么几个:value是必填的,类型是java.util.Date;pattern用来指定输出格式,比如“yyyy-MM-dd”或“yyyy年MM月dd日”;dateStyle和timeStyle可以指定预定义样式,比如short、medium、long,但这里有个坑,如果dateStyle和timeStyle同时指定或者一个指定一个不指定,组合逻辑很容易混乱,我建议直接指定pattern,虽然代码多几个字符,但输出结果完全可控。
有同学会问一个问题:后端如果用的是LocalDateTime,fmt:formatDate的value属性认不认?答案是:不认,严格来说Tomcat 9以上的EL实现支持LocalDateTime,但fmt:formatDate的value被限制为java.util.Date类型,传入LocalDateTime会报错或者格式化失败。如果后端用了LocalDateTime,通常有以下三个处理方案。
第一个方案是在实体类里多提供一个返回Date类型的方法: public Date getRegTimeAsDate() { return Date.from(regTime.atZone(ZoneId.systemDefault()).toInstant()); }
第二个方案是后端在进入页面之前手动转为Date,或者在Controller/model里把LocalDateTime转成对应格式的字符串。JSP里的JavaBean和Servlet/Spring MVC的Model完全可以容纳字符串字段。
第三个方案是不用fmt:formatDate标签,直接用EL表达式配合自定义函数。EL表达式虽然不能直接格式化日期,但可以通过自定义EL函数来调用静态方法: ${my:formatDateTime(user.regTime)}
这个方法维护稍微麻烦一点,需要写一个Tag EL函数类,但灵活性最高。
我个人最推荐的是方案一,在实体类里增加一个额外的getter方法,不改动原有结构,对JSP页面的侵入最小。这种“实体类携带展示格式”的做法在传统JSP项目里很常见,不算优雅,但足够务实。
3.2 EL表达式直接输出日期的结果异常原因
新手常犯一个错误:直接用${user.regTime}在页面上输出日期,然后惊讶地发现显示出来的是“Wed Apr 10 10:30:00 CST 2024”这种格式。
这是因为Object.toString()方法输出的结果就是这样。java.util.Date重写了toString(),默认用英文+星期的格式来表达时间。EL表达式不会自动帮你做格式化,它只会调用对象的toString()方法。
解决方案:一种是改用fmt:formatDate,一种是提前在实体类或工具类中把日期转成字符串。
我在做实名认证展示页时,用户身份证有效期是“2030.12.31”这种格式,页面直接${user.certExpireDate}输出,结果变成一大串英文和数字。后来在实体类里加了一个getCertExpireStr()方法,返回“2030年12月31日”,页面一律显示这个字段,整个世界清净了。
3.3 空值日期在JSP页面显示的三种处理方式
日期为空是JSP页面上经常遇到的情况。用户没有填写生日、商品没有下架时间、管理员没有设置最后登录时间,这些字段可能是null。如果直接使用fmt:formatDate,它对于null的value会输出空串,不会报错,这是比较友好的一点。但如果你用自定义EL函数,或者自己在页面里拼接字符串,就要格外小心null导致NPE(NullPointerException)。
我常用的三种方式如下。
第一种,页面级兜底: <fmt:formatDate value="${user.birthday}" pattern="yyyy-MM-dd" />
第二种,默认值替代: <c:choose> <c:when test="${not empty user.birthday}"> <fmt:formatDate value="${user.birthday}" pattern="yyyy-MM-dd" /> </c:when> <c:otherwise>未设置</c:otherwise> </c:choose>
第三种,后端封装默认值。在实体类或VO类的getter方法里判空返回默认值: public String getBirthdayDisplay() { return birthday == null ? "未设置" : birthday.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")); }
这三种方式各有适用场景,页面级兜底适合所有时间字段都无所谓是否为空的情况;默认值替代适合列表页,让用户知道这一项没有填;后端封装默认值适合需要在多个页面上复用的展示逻辑。
3.4 基于标签库的JSP页面日期格式化完整代码示例
这一节给出一个可直接参考的列表页日期展示片段。场景是展示用户列表,需要显示注册时间和最后登录时间。
假设后端在Model/request中放了一个List ,User实体类包含LocalDateTime regTime和lastLoginTime字段及对应的getter。
JSP页面关键代码:
| 用户名 | 注册时间 | 最近登录 |
|---|---|---|
| ${user.username} | 未知 | 从未登录 |
注意这里的value用的是${user.regTimeAsDate},对应实体类中的getRegTimeAsDate()方法。如果实体类里没有这个方法,你可以在Servlet或Controller中遍历列表,把时间对象转换为Date对象传给页面,或者干脆在实体类里再加一个格式化后的字符串字段。
这种组合方式同时兼顾了三个需求:空值处理、格式定制、列表循环。fmt:formatDate配合c:forEach是JSP页面处理日期列表最常见的一套组合拳。
4. 实战案例:JSP个人信息展示页面中的日期字段全流程处理
4.1 功能拆解与页面设计
以热搜词“jsp个人信息展示页面”为例,做一个完整的个人中心页面。这个页面通常包含以下和日期相关的字段:
- 生日:用户自己填写,格式为“1995年8月20日”
- 注册时间:系统自动记录,格式为“2023年6月10日 14:23:05”
- 最近登录时间:系统自动更新,格式为“半小时前”或具体时间
- 会员到期时间(如果有会员体系):需要显示“2026年12月31日”
- 实名认证有效期:可能显示“2030.05.20”这种格式
这里面就涉及了“详情页展示”和“动态时间显示”两类场景,正好能覆盖绝大多数日期处理的细节。我需要先梳理一下数据库字段类型,再设计实体类,然后是Controller/Servlet层的处理逻辑,最后是JSP页面的展示。
对于这个页面,我的实现思路是把“存储格式”和“展示格式”分离。数据库统一存标准时间,实体类里提供多个面向不同展示场景的getter方法,页面按需取用。
4.2 数据库表结构设计与实体类POJO实现
数据库表这里以MySQL为例,日期字段统一使用DATETIME类型,生日用DATE类型。DATETIME比TIMESTAMP的好处是范围更大,而且不受2038年问题限制,适合长期存储。
CREATE TABLEuser(idint(11) NOT NULL AUTO_INCREMENT,usernamevarchar(50) NOT NULL,birthdaydate DEFAULT NULL,reg_timedatetime DEFAULT NULL,last_login_timedatetime DEFAULT NULL,vip_expire_timedatetime DEFAULT NULL,cert_expire_datedate DEFAULT NULL, PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
实体类里,我推荐用LocalDate对应DATE,LocalDateTime对应DATETIME。配合MyBatis 3.4.5以上版本或者较新的JDBC驱动,可以自动完成类型映射,不需要额外写TypeHandler。
public class User { private Integer id; private String username; private LocalDate birthday; private LocalDateTime regTime; private LocalDateTime lastLoginTime; private LocalDateTime vipExpireTime; private LocalDate certExpireDate; // 省略 getter/setter }
这里有个细节:如果项目使用的是老版本MyBatis plus或Hibernate,日期类型映射可能不支持LocalDate/LocalDateTime,会报“无法识别Java 8日期类型”之类的错。这种情况下要么升级依赖版本,要么在配置里注册JSR310支持,要么退回到java.util.Date字段。我遇到过项目用了MyBatis 3.4.2,升级到3.4.6才解决。
4.3 Servlet/Controller层的日期数据准备与转换逻辑
在Servlet或Spring MVC的Controller里,要做三件事:查询用户基本信息,把注册时间和最近登录时间装入User对象;计算最近登录时间的相对显示(比如“刚刚”“5分钟前”“昨天”);给JSP页面返回数据。
注册时间这里我做一个简单的抽闲计算逻辑,写一个工具方法:
public static String getRelativeTime(LocalDateTime targetTime) { LocalDateTime now = LocalDateTime.now(); Duration duration = Duration.between(targetTime, now); long minutes = duration.toMinutes(); if (minutes < 1) { return "刚刚"; } else if (minutes < 60) { return minutes + "分钟前"; } else if (minutes < 24 * 60) { return (minutes / 60) + "小时前"; } else { return targetTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")); } }
然后在获取用户信息的接口里:
public void getUserInfo(HttpServletRequest req, HttpServletResponse resp) { User user = userService.getById(1); // 页面中显示的额外字段 user.setLastLoginDisplay(getRelativeTime(user.getLastLoginTime())); req.setAttribute("user", user); req.getRequestDispatcher("/userInfo.jsp").forward(req, resp); }
这里我把“相对时间”字符串算好后直接放到实体类的冗余字段里,页面拿着这个字段展示即可。实体类增加一个lastLoginDisplay字符串字段,这就是传统JSP项目的典型做法。
另外,如果页面需要展示“会员是否已过期”,也建议在后端提前算好。避免在JSP里写复杂的比较逻辑。
4.4 个人信息JSP页面的完整代码与效果预期
个人信息页面userInfo.jsp的关键代码:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
个人信息
- ${user.username}
- 未填写
- ${user.lastLoginDisplay}
- 无会员
这里用到了user.birthdayAsDate、user.regTimeAsDate、user.vipExpireTimeAsDate这些额外的getter方法,它们负责把LocalDate/LocalDateTime转换为java.util.Date,以便fmt:formatDate渲染。
实体类里的转换方法写法如下:
public Date getBirthdayAsDate() { return birthday != null ? Date.from(birthday.atStartOfDay(ZoneId.systemDefault()).toInstant()) : null; }
public Date getRegTimeAsDate() { return regTime != null ? Date.from(regTime.atZone(ZoneId.systemDefault()).toInstant()) : null; }
public Date getVipExpireTimeAsDate() { return vipExpireTime != null ? Date.from(vipExpireTime.atZone(ZoneId.systemDefault()).toInstant()) : null; }
页面上最终的渲染效果是:
- 生日显示为“1995年08月20日”
- 注册时间显示为“2023-06-10 14:23:05”
- 最近登录显示为“5分钟前”
- 会员到期显示为“2026年12月31日”
视觉效果干净且统一,没有英文星期,没有默认的CST标识,空值也不会刺眼。
4.5 表单提交场景中的日期解析与格式规整
个人信息页面除了展示还有编辑功能,用户提交生日时通常是一个“1995-08-20”的字符串。在Servlet/Controller里解析:
String birthdayParam = req.getParameter("birthday"); if (birthdayParam != null && !birthdayParam.trim().isEmpty()) { DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); LocalDate birthday = LocalDate.parse(birthdayParam.trim(), formatter); user.setBirthday(birthday); }
前端表单里我一般用type="date"的input,这样浏览器会强制按“YYYY-MM-DD”的格式提交,后端解析就省心多了。不过这有一个兼容性问题:某些旧版浏览器把type="date"当成type="text"来渲染,用户输入“2023年6月10日”这种格式,后端就会被解析报错。我的兜底方案是在后端写一个宽松解析方法,格式匹配不上多个候选格式时抛出友好的业务异常,返回给用户“日期格式不正确,请使用YYYY-MM-DD格式”的提示,而不是500页面。
表单里如果用日期选择器(比如laydate、flatpickr),注意和JSP的结合方式:JSP渲染初始值,JavaScript日期选择器负责交互,提交的格式由JS统一成“YYYY-MM-DD”。这个分工对用户体验和开发体验都比较友好。
5. 常见日期处理问题与排查技巧实录
5.1 日期显示少了几个月或一天的原因与解法
有同学遇到过这样的问题:数据库里存的是“2023-06-10”,页面上显示成“2023-05-10”或者“2023-06-09”。这种问题十有八九是时区问题。
MySQL连接串里如果没有指定serverTimezone,默认可能按服务器时区或UTC来处理。比如你的MySQL服务器在时区Asia/Shanghai,但JDBC连接串用了UTC,那从库里查出的DATETIME会被JDBC自动转换一次,转换后显示出来的时间就少了8小时。
排查步骤是:先检查MySQL连接串有没有加serverTimezone=Asia%2FShanghai(UTC+8对应Asia/Shanghai)。再检查JVM默认时区,在代码里打印一下TimeZone.getDefault().getID()看看是不是Asia/Shanghai。最后检查数据库服务器的time_zone变量,执行SELECT @@global.time_zone, @@session.time_zone;查看。
推荐统一的做法是MySQL连接串显式指定: jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
这样JDBC、JVM、数据库三者的时区就能一致对齐。
5.2 fmt:formatDate格式化出错时怎么快速定位
fmt:formatDate报错通常集中在两个点。
第一是类型不支持。刚才提到过,如果value传入的是LocalDateTime或LocalDate,格式化会失败或者输出不对。快速判断方法:看页面上是否有“Cannot convert”或“No value”相关的错误堆栈;或者在后端临时打印一下传到页面的数据的class类型。
第二是pattern写法错误。pattern里如果写了“YYYY”(大写),它会表示“Week Year”而不是“Year”(小写yyyy才是年份)。Week Year在跨年周的时候会和真实年份差一年。比如2024年1月1日如果是上一年的最后一周,按YYYY格式化会输出2023。这绝对是经典坑,我的建议是年份永远使用小写yyyy。
顺带提醒一下月份符号:大写的MM是月份,小写的mm是分钟。如果你写了“yyyy-mm-dd”,分号前面的mm会被当成分钟,解析时会直接报错或者显示异常数据。
5.3 页面显示英文/乱码日期时的整链路检查顺序
如果页面上日期变成“Wed Jun 10 14:23:05 CST 2023”或者“Jun 10, 2023”这种英文格式,说明页面显示的实际上是Date对象的默认toString输出。检查链路按照这个顺序来:
- 第一步,看JSP代码用的是不是${user.regTime}原样输出。如果用了fmt:formatDate标签还出现英文,就检查标签库的URI引用是否写对,JSTL库是否成功引入。
- 第二步,看实体类里是不是同时存在getRegTime()和getRegTimeAsDate()方法,而JSP里无意中把RegTimeAsDate写成了RegTime。
- 第三步,看后端返回的字段是不是已经是字符串,并在Controller中做了格式化。如果后端的格式化用的还是默认Locale(英文),也会显示英文月份。可以用标签的locale属性指定中文环境。
实际上显示英文最多的情况就是压根没用格式化标签,直接原样输出了,这一点从代码上就能一眼看出。
5.4 日期范围内的查询边界问题:结束日期总是查不到当天
做日期范围查询的时候有个高频问题:用户选择“2025-01-01 到 2025-01-15”,结果1月15日的数据查不出来。原因是数据库里存的时间是“2025-01-15 14:23:05”这种完整datetime,而后端查询条件只用了“2025-01-15”。
正确做法是结束日期加一天,用开区间排除边界: WHERE create_time >= '2025-01-01 00:00:00' AND create_time < '2025-01-16 00:00:00'
用小于结束日期的下一天,而不是小于等于结束日期当天。这样避免了23:59:59.999的精度问题,这也是MySQL DATETIME(3)等毫秒精度字段经常会遇到的问题。
在Java端计算结束区间的下一天: LocalDate startDate = LocalDate.parse("2025-01-01"); LocalDate endDate = LocalDate.parse("2025-01-15"); LocalDateTime startTime = startDate.atStartOfDay(); LocalDateTime endTime = endDate.plusDays(1).atStartOfDay();
然后用startTime和endTime作为查询参数传入Mapper。
5.5 传统JSP项目的日期处理性能注意事项
最后聊一下性能。传统JSP项目通常是同步渲染的,每一个页面上的日期格式化都会消耗少量CPU。绝大多数情况下这不是瓶颈,但有几个细节值得注意。
第一,避免在JSP里反复new SimpleDateFormat。如果非要用SimpleDateFormat,尽量在工具类里用ThreadLocal包一个,但最推荐的还是DateTimeFormatter(线程安全)。这一点在并发高的JSP页面上尤其明显。
第二,不要把大列表里的日期字段全部放在实体类getter里做复杂计算。比如在每一行的getter里调用“相对时间”算法,处理逻辑不复杂还好,如果里面涉及数据库查询或者远程调用,那就把性能拖垮了。相对时间等计算应该在Service层提前算好,放在冗余字段里,JSP只负责取。
第三,JSP页面输出日期尽量使用fmt:formatDate标签,它经过JSTL实现优化,比在Scriptlet里写Java格式化逻辑更可靠。而且JSTL的格式化标签会缓存已解析的pattern,同一页面大量使用也不会频繁触发内部解析。
写在最后的几点实操心得
做了不少Java Web项目之后,我对JSP日期处理的最大体会是:它在技术上不复杂,但对代码规范的要求特别高。很多问题的根源都是混用类型、混用时区、混用Java代码和标签库职责。现在新项目大概率已经拥抱前后端分离了,但校园实训、老系统维护、传统JSP毕设依旧大量存在,这套方案依然实用。
如果你正在做“基于JSP的毕设选题”,我建议在处理日期时尽量统一做到下面几点:数据库字段用DATETIME/DATE表达语义,实体类用LocalDate/LocalDateTime对应,页面展示用JSTL的fmt:formatDate,表单解析用DateTimeFormatter,查询边界用前闭后开。这些习惯一旦养成,不管以后写JSP还是Spring Boot,都少踩一半日期相关的坑。
最后再分享一个小技巧:如果项目里大量页面都需要同样的日期格式,不要每个页面都写一遍pattern。把格式化串统一放在一个常量类里,比如DateFormatConst.FULL_TIME等,JSP里直接引用这些常量字符串。这样等业务方某一天忽然要求“时间显示改成24小时制带秒”,你只需要改一个地方,而不是全局搜索替换所有JSP页面。