ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

JSP日期处理实战:后端格式化到页面展示的完整指南

JSP日期处理实战:后端格式化到页面展示的完整指南 1. JSP日期处理到底难在哪先搞懂你的页面是怎么“拿到时间”的做JavaWeb项目尤其是还在用JSP的老项目或者学校里的毕设日期处理是绕不开的一道坎。无论是用户注册页面要显示“注册时间”、个人信息展示页要格式化生日还是后台管理要按日期筛选订单你都会碰到一个尴尬的现实JSP页面里写Java代码也行用EL表达式也行用JSTL标签也行甚至混着来的也比比皆是。JSP日期处理之所以让人头疼不是因为“获取当前时间”这件事有多难而是因为同一个项目里日期数据可能从后端Model里来、从Session里来、从请求参数里来而且页面还能用三种完全不同的语法去渲染它。很多刚接触JSP的人会先去网上搜“jsp入门头歌实训答案”“idea新建jsp项目”之类的教程跟着一步步把页面跑起来然后卡在第一个动态数据上——日期。因为日期不像字符串那样拿来就能输出你直接往页面上放一个java.util.Date对象看到的是一串类似Thu Nov 14 15:30:00 CST 2024的英文日期既不符合中文习惯也跟数据库里存的对不上。更麻烦的是如果你在一个循环里渲染几十条记录每条记录都要格式化一次日期稍不留神就会踩到性能和线程安全的坑。我自己维护过几个老项目也带过一些做毕设的学生大家遇到的问题其实高度一致不是不会写代码而是不知道在JSP这个“多种语法混用”的环境里日期处理到底该按哪一套规矩来。这篇文章我想把这些年在JSP项目里处理日期的经验整理出来从后端到页面、从格式化到计算、从常见坑到排查思路一步步讲清楚。不管你是刚开始学JSP的新手还是在给传统“基于jsp的毕设选题”挣扎结果中的人这篇都能给你一个能直接抄作业的参考方案。1.1 三种常见的日期来源场景JSP页面里的日期数据来源无非三种。第一种是后端通过request.setAttribute或者ModelAndView传过来的Date对象这种最正规也是Spring MVC和传统Servlet项目最常见的做法。第二种是页面自己用new java.util.Date()现场生成多见于一些演示代码和临时页面这种做法不推荐因为页面里堆太多Java代码会让JSP失去“视图”的意义。第三种是从请求参数或者数据库中取出来的字符串比如2024-11-14 18:30:00这样的格式需要先解析成Date对象才能参与计算。这三种来源对应的处理手段完全不一样。第一种要解决的是“怎么在页面上优雅地格式化”第二种要考虑的是“怎么尽量避免在JSP脚本里写逻辑”第三种要解决的是“字符串和Date之间怎么安全地转换”。很多人栽跟头就是没搞清楚当前这份日期数据属于哪种来源上来就套用某一个办法结果经常是格式化格式不对、解析异常或者页面上直接白屏。1.2 为什么网上教程老是“教一半”网上关于JSP日期处理的教程其实不少但你搜“jsp 日期处理”出来的结果往往很零散有的只讲SimpleDateFormat怎么用有的直接甩一段JSTL标签代码还有的干脆说“建议改用前后端分离”。这也不能怪写教程的人因为JSP本身就是一个已经“过气”但还没“死透”的技术真正还在用它的人基本上都是三类学校里的学生、接传统企业外包的团队、以及维护老系统的开发。这三类人的需求差异很大教程却往往只覆盖其中一类。更麻烦的是JSP页面里可以混用脚本片段% %、EL表达式${}和JSTL标签这是它灵活的地方也是它混乱的根源。同一个日期你至少能在页面上找到五种以上不同的写法而且它们都能跑。等你接手别人的项目时看到的往往是最难维护的那种——脚本片段里直接用SimpleDateFormat格式化逻辑散落在每一行HTML之间。所以这篇文章我给自己定了个规矩凡是能在后端处理的就尽量在后端处理凡是能在JSTL标签里解决的就尽量不用脚本来写凡是需要脚本的一定要把逻辑封装成简洁的代码。因为JSP的职责是渲染视图日期处理这种“逻辑”的活能少放就少放。2. 后端日期处理三板斧获取、格式化、转换处理JSP项目的日期第一步永远不在页面而在后端。只要后端把日期处理得规规矩矩页面上的工作就简单多了。这一节我按使用频率从高到低把后端日期处理的三个基础动作讲透获取当前时间、格式化Date对象、把字符串解析成Date。2.1 获取当前时间别只会new Date()获取当前时间大多数人第一反应是new Date()。没错这能拿到当前时间但有个问题如果你需要同时拿到“当前日期”和“当前时间”或者需要做一些加减运算直接用Date对象并不方便。更好的选择是java.time包下的LocalDate和LocalDateTime但注意很多旧项目还在用Java 7甚至Java 6java.time在Java 8才引入。如果你做的传统JSP项目用的是JDK 1.7那就还是得靠java.util.Calendar。我用一个实际场景说明。假设你想在个人信息展示页面显示“今天是xxxx年xx月xx日”用Date写是这样Date now new Date(); SimpleDateFormat sdf new SimpleDateFormat(yyyy年MM月dd日); String today sdf.format(now);用Calendar写是这样Calendar cal Calendar.getInstance(); int year cal.get(Calendar.YEAR); int month cal.get(Calendar.MONTH) 1; // 注意月份从0开始 int day cal.get(Calendar.DAY_OF_MONTH); String today year 年 month 月 day 日;如果你用的是Java 8那LocalDate.now()一行就够了配合DateTimeFormatter也很优雅String today LocalDate.now().format(DateTimeFormatter.ofPattern(yyyy年MM月dd日));我的建议是新项目优先用java.time老项目老老实实用Calendar尽量别直接用new Date()去拼字符串。原因很简单Date的toString()输出格式不固定不能直接做展示和存储必须经过格式化才能用。而且Calendar能让你很方便地做“上个月”“下周三”这类计算Date本身做不到。2.2 格式化这件事记住“锁”比记住“格式”更重要格式化是日期处理里最常见的动作但也是坑最密集的地方。很多人学会了SimpleDateFormat的yyyy-MM-dd HH:mm:ss格式就以为万事大吉了。实际上SimpleDateFormat有个非常隐蔽的坑——它并非线程安全。怎么理解线程安全这个概念打个比方SimpleDateFormat就像一个没有锁的公共记账本多个柜台线程同时往里记账账就乱了。在JSP项目里如果你把SimpleDateFormat定义成一个静态成员变量然后在多线程环境下比如并发请求调用它的format方法就会时不时出现日期输出错乱的情况比如明明是2024年11月却输出了2023年1月。我在实际项目中踩过一次这个坑。当时有个统计报表页面高并发访问时偶尔会出现个别用户看到的时间是“1970年”排查了半天才发现是SimpleDateFormat被定义成了全局静态变量多个线程共享导致内部状态错乱。从那时候起我就给自己定了一条规矩SimpleDateFormat只能用两种方式使用——要么在方法局部创建每次new一个要么用ThreadLocal包装让每个线程持有自己的实例。下面的代码是安全的局部用法public String formatDate(Date date) { SimpleDateFormat sdf new SimpleDateFormat(yyyy-MM-dd HH:mm:ss); return sdf.format(date); }如果这段代码会被大量调用new对象带来的性能开销也不大因为SimpleDateFormat本身不是重资源。你可以放心地在方法内部创建。这也是我推荐优先方案。只有当你确实需要频繁创建并且能忍受轻微的性能损耗时才需要去考虑ThreadLocal的优化方案。2.3 字符串转日期用户输入是最不靠谱的源头字符串转日期是另一个高频需求。用户在页面上填一个出生日期提交到后端后后端把字符串1998-08-08解析成Date对象再存到数据库。这个流程看起来简单实际上有三个容易出错的地方。第一用户输入格式不可控。有人输1998-08-08有人输1998/08/08还有人会输1998年8月8日。如果后端只按一种格式解析必然抛ParseException。稳妥的做法是先用正则表达式或者String.matches校验格式再决定用什么SimpleDateFormat解析。第二解析时的parse方法有一个很多人不知道的“宽松模式”问题。SimpleDateFormat默认是宽松解析也就是说2024-13-45这样的非法日期它不会报错而是会帮你换算成2025-02-14。如果你不希望出现这种“惊喜”一定要显式关闭宽松模式SimpleDateFormat sdf new SimpleDateFormat(yyyy-MM-dd); sdf.setLenient(false); Date birthDate sdf.parse(2024-13-45); // 此时会抛异常第三解析后要不要保留时间部分。如果只是生日你只需要日期如果是订单创建时间就需要完整的时间。这就要求你选择的格式模板要和目标字段匹配否则会出现“日期截断”的问题。我处理用户输入的通用套路是先在前端用JavaScript做一次格式校验后端再校验一次两次都通过才允许解析。后端的代码模板大致长这样String input request.getParameter(birthday); SimpleDateFormat sdf new SimpleDateFormat(yyyy-MM-dd); sdf.setLenient(false); try { Date birthday sdf.parse(input); // 后续处理... } catch (ParseException e) { // 返回错误提示告诉用户格式必须为 yyyy-MM-dd }3. JSP页面上展示日期EL表达式和JSTL才是主流玩法后端处理完了接下来就是页面的活。很多人在这一步开始分叉要么在%%脚本里写out.println(new SimpleDateFormat(...).format(...))要么直接用EL表达式把Date对象渲染出来结果输出一长串英文。正确的做法是用JSTL的fmt:formatDate标签一行搞定而且完全不污染页面结构。3.1 在HTML页面上正确输出Date对象先说结论如果你的页面引入JSTL标签库格式化日期只需这样写% taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt % ... fmt:formatDate value${user.birthday} patternyyyy年MM月dd日 /这里${user.birthday}是从后端传过来的Date对象pattern指定输出的格式输出结果就是“1998年08月08日”这样的中文格式。如果你想要yyyy-MM-dd格式把pattern改一下就成了。这个标签的好处是它不需要你在页面上写任何Java代码页面看起来干净整洁。有的项目用的JSP版本较老可能会遇到标签库URI的问题。常见的URI有http://java.sun.com/jsp/jstl/fmt和http://java.sun.com/jsp/jstl/core如果你用的是Jakarta标准标签库JSTL 1.2也可以写jakarta.tags.fmt。这个细节记不住问题不大真遇到JSPG0048E: Unable to find taglib这类报错再去检查uri也不迟。这里有个小技巧如果你只是想显示“yyyy-MM-dd HH:mm:ss”这种带时间的格式可以直接用patternyyyy-MM-dd HH:mm:ssJava的pattern里HH表示24小时制hh表示12小时制。很多新人分不清HH和hh导致下午的时间显示成凌晨这一点务必注意。3.2 后端给前端传日期数据的几种姿势搞清楚页面怎么写之后还得知道后端怎么把Date对象传过去。最常见的姿势是request.setAttributeUser user userService.getById(1001); request.setAttribute(user, user); // 其中user.getBirthday()返回的是一个Date对象页面里直接用${user.birthday}就能拿到这个Date对象。还有一种是配合Spring MVC使用ModelAndViewModelAndView mv new ModelAndView(user/profile); mv.addObject(user, user); return mv;这两种方式在页面上的写法完全一样。如果你用的是response.sendRedirect跳转后的页面是无法通过request拿到数据的这时候得用session或者URL参数传递但日期数据一般很少走这种渠道就不展开了。这里我想多提醒一句很多毕设项目喜欢在实体类里把日期字段定义为String类型后端直接用SimpleDateFormat转成字符串再传给页面这样一来页面确实简单了直接${user.birthday}输出就是格式化好的字符串。但这种方式有隐患——万一你需要在页面做日期比较比如计算年龄、判断某个日期是否在截止时间之前字符串比较就会非常痛苦。所以实体类该用Date就用Date不要为了图省事统一搞成String。4. 日期比较与计算年龄、时间差、截止时间判断展示日期只是第一步JSP项目里更常见的需求是拿日期做比较和计算。比如根据出生日期计算年龄、计算两个日期之间隔了多少天、判断当前时间是否已经超过了活动截止时间。这些场景听着复杂但本质上就两件事要么算时间戳要么用Calendar做加减。4.1 用时间戳做比较和运算java.util.Date有一个getTime()方法返回自1970年1月1日0点以来的毫秒数。两个日期谁早谁晚直接比较毫秒数就行这个操作简单粗暴而且没有格式化的干扰。我写过一个根据生日计算年龄的示例代码是这样的public int calcAge(Date birthday) { long birthMillis birthday.getTime(); long nowMillis System.currentTimeMillis(); long diffMillis nowMillis - birthMillis; // 转换成天 long days diffMillis / (1000 * 60 * 60 * 24); // 粗略按365.25天/年换算 return (int) (days / 365.25); }这个算法有个问题它用的是平均年算出来的年龄在跨越闰年时会有偏差。更精确的做法是通过Calendar比较年月日。public int calcAge(Date birthday) { Calendar birth Calendar.getInstance(); birth.setTime(birthday); Calendar now Calendar.getInstance(); int age now.get(Calendar.YEAR) - birth.get(Calendar.YEAR); // 如果今年生日还没过则年龄减一 if (now.get(Calendar.MONTH) birth.get(Calendar.MONTH)) { age--; } else if (now.get(Calendar.MONTH) birth.get(Calendar.MONTH) now.get(Calendar.DAY_OF_MONTH) birth.get(Calendar.DAY_OF_MONTH)) { age--; } return age; }两种算法的差异在于时间戳算法对毫秒差直接换算适合“两个时间点相隔多少毫秒/秒/天”这种场景Calendar算法适合“某个日期到今天经过了多少个完整年/月/日”这种日历单位计算。JSP里做年龄计算我推荐用Calendar版本结果更符合直觉。4.2 基于JSP的实际判断场景说完后端再看页面上的判断。假设个人信息展示页面需要显示“会员是否已过期”后端传过来一个expireDate前端需要判断当前时间是否已经超过了这个日期。如果是用JSTL标签做比较可以直接用fmt:parseDate把字符串解析成Date对象再配合EL表达式的比较运算符fmt:parseDate value${expireDateStr} patternyyyy-MM-dd varexpireDate / c:if test${expireDate lt sessionScope.currentDate} 会员已过期 /c:if但这里有个很容易踩的坑EL表达式的比较运算符lt、gt、eq比较的是对象本身如果你的expireDate是Date对象currentDate是字符串那么比较结果永远是false。需要用fmt:parseDate把两边都转成同类型后再比较。这也是我说“实体类尽量用Date类型”的原因之一。如果页面实在复杂需要在JSP里做时间计算我建议的做法是不要硬在页面里算而是把计算结果直接放成一个布尔值或者数字传到后端去判断。比如“是否过期”这个结果完全可以在后端算好塞给前端boolean expired user.getExpireDate().getTime() System.currentTimeMillis(); model.addAttribute(expired, expired);然后页面上只要写${expired}判断true/false即可。JSP页面保持简单这是我在多个项目里总结出来的血泪教训——页面上逻辑越少后期维护越轻松。5. 实战复盘一个“个人信息展示页”的日期处理全过程前面讲了这么多理论我拿一个真实的“jsp个人信息展示页面”场景来完整跑一遍流程。这类页面是很多毕设项目里必备的功能也是网上热度很高的搜索词。它涉及日期处理的全部三类需求显示生日、计算年龄、展示注册时间。5.1 页面需求拆解假设用户信息表user有以下字段id、username、birthday生日、create_time注册时间。页面需要显示用户名生日格式为yyyy年MM月dd日年龄根据生日实时计算注册时间格式为yyyy年MM月dd日 HH:mm会员状态如果注册时间距今超过一年显示“注册满一年”否则显示“新用户”这个页面如果页面脚本写得很乱后面每个字段都要改一遍。我的设计思路是能在后端算的都在后端算页面只负责展示。后端Controller要准备四个值birthdayDate类型、ageint类型、createTimeDate类型、registerOverOneYearboolean类型。5.2 核心代码与关键步骤后端Controller的关键代码Controller public class UserProfileController { Autowired private UserService userService; RequestMapping(/profile) public ModelAndView profile(HttpServletRequest request) { int userId Integer.parseInt(request.getParameter(id)); User user userService.getById(userId); // 1. 生日直接传Date对象给页面 // 2. 年龄用Calendar方法计算 int age calcAge(user.getBirthday()); // 3. 注册时间判断是否满一年 Date createTime user.getCreateTime(); boolean registerOverOneYear false; if (createTime ! null) { Calendar cal Calendar.getInstance(); cal.add(Calendar.YEAR, -1); registerOverOneYear createTime.before(cal.getTime()); } ModelAndView mv new ModelAndView(user/profile); mv.addObject(user, user); mv.addObject(age, age); mv.addObject(registerOverOneYear, registerOverOneYear); return mv; } private int calcAge(Date birthday) { if (birthday null) { return 0; } Calendar birth Calendar.getInstance(); birth.setTime(birthday); Calendar now Calendar.getInstance(); int age now.get(Calendar.YEAR) - birth.get(Calendar.YEAR); if (now.get(Calendar.MONTH) birth.get(Calendar.MONTH)) { age--; } else if (now.get(Calendar.MONTH) birth.get(Calendar.MONTH) now.get(Calendar.DAY_OF_MONTH) birth.get(Calendar.DAY_OF_MONTH)) { age--; } return age; } }页面的profile.jsp写法% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt % html body h2个人信息/h2 p用户名${user.username}/p p生日fmt:formatDate value${user.birthday} patternyyyy年MM月dd日 //p p年龄${age}岁/p p注册时间fmt:formatDate value${user.createTime} patternyyyy年MM月dd日 HH:mm //p c:if test${registerOverOneYear} p classlabel注册满一年/p /c:if c:if test${!registerOverOneYear} p classlabel新用户/p /c:if /body /html这套方案看起来平平无奇但其实每个数据值的传递都是经过设计的Date对象直接给页面用fmt:formatDate格式化避免脚本片段年龄和布尔状态交给后端计算页面只拿最终结果。这样做的好处是如果后续要换成Thymeleaf模板页面改动很小因为数据模型已经定义清楚了。6. 常见坑与排查技巧实录这一节我专门整理一下JSP项目里日期处理最常见的坑以及我实际排查这类问题的一些心得。这些问题在百度上不一定搜得到标准答案但你要是碰上了基本就是下面这几个原因。6.1 SimpleDateFormat线程安全前面提过SimpleDateFormat线程不安全。排查这个问题有一个很典型的信号同一个页面在低并发时日期显示完全正常一遇到高峰期就偶尔出现个别请求的日期是一个远古年份或者“null”。如果代码里你确实把SimpleDateFormat定义成了类的静态变量基本可以锁定就是这个问题。解决方案我已经讲过方法内新建实例或者用ThreadLocal。这里我贴一个ThreadLocal版本的写法private static final ThreadLocalSimpleDateFormat DATE_FORMAT ThreadLocal.withInitial(() - new SimpleDateFormat(yyyy-MM-dd HH:mm:ss)); public static String format(Date date) { return DATE_FORMAT.get().format(date); }注意ThreadLocal的内存泄漏问题不过放在这种场景下影响不大可以放心用。6.2 时区与服务器时间不一致第二个常见坑是时区。尤其是把传统JSP项目打成war包部署到云服务器或者别的机器上之后明明本地运行时间正常服务器的数据库里存的时间却差了8个小时。这类问题绝大多数不是Java代码的锅而是服务器操作系统时区没设对。Linux服务器可以用date命令快速查看当前时间如果显示的是UTC而不是北京时间就需要设置时区。排查思路很简单先看服务器系统时间再看数据库连接串的serverTimezone参数最后检查代码里有没有手动对TimeZone做处理。还有一个容易忽略的地方如果数据库用的是MySQLJDBC连接串里经常要加serverTimezoneAsia/Shanghai。很多老项目用的是旧版本驱动不改这个参数就会读取到错乱的时间。6.3 中文格式与数据库交互第三个坑是中文格式问题。页面上用yyyy年MM月dd日展示没问题但如果你把这种字符串直接存到数据库后续再用SQL的日期函数去做比较数据库根本认不出来。我的原则是数据库存DATE或DATETIME类型Java代码里用Date类型只有到了页面展示层才用格式化字符串。数据在传输过程中始终保持机器可读的格式只有人类需要看的时候才变成中文。另一个和数据库相关的细节使用PreparedStatement的setDate方法时它默认只包含日期不包含时间。如果你要存完整时间应该用setTimestamp。这个坑我以前在写注册时间字段时踩过存进去的时间全部变成了00:00:00排查了很久才发现是setDate闹的。6.4 排查技巧实录最后分享一个实际排查日期问题的通用套路。当一个JSP页面上的日期显示不对我建议按照下面的顺序检查第一步F12看页面到底输出了什么字符串。如果是一片“null”问题在后端数据传没传如果是英文日期问题在页面没做格式化如果是1970年问题八成在时间戳换算。第二步后端打印出Date对象用System.out输出date.toString()确认数据源的值正确。后端如果是对的问题一定在页面层后端如果已经是错的就继续往上层查查SQL语句、查前端提交的参数。第三步检查时间区域。尤其是部署环境和本地环境不一致时这一步能帮你排除掉“看起来一模一样但时间就是不对”的玄学问题。这三步走完绝大多数日期问题都能定位到具体环节。我在用这套方法排查时发现80%的日期问题其实都出在第二步——大家默认后端数据是对的实际却是从DAO取出来的那一刻就已经错了。7. 最后一个实用技巧JSP项目里的“日期工具类”设计回到开头说到的话题JSP虽然老但在毕设、学校作业和一些传统企业里它就是会一直存在。日期处理说难不难说简单也有一堆细节。我最后再分享一个个人很受用的习惯不管项目大小日期处理代码一定要集中封装成一个工具类不要在Controller、Service、JSP页面里到处散落SimpleDateFormat和Calendar代码。我自己设计日期工具类时通常会包含以下方法formatDate(Date, String pattern)把Date按指定格式转字符串parseDate(String, String pattern)把字符串按指定格式转DategetCurrentTime()获取当前时间戳getAge(Date birthday)根据生日计算年龄getDaysBetween(Date start, Date end)计算两个日期之间的天数isOverTime(Date deadline)判断是否已超过截止时间这样做的好处是你在页面上写fmt:formatDate时或者在Controller里处理逻辑时永远只跟一个工具类打交道。排查问题的时候也只需要打开一个文件而不是满项目搜SimpleDateFormat。这个习惯一度让我在维护老项目时省下了大量时间。如果你正在做基于JSP的毕设或者刚接手一个传统JSP项目建议你现在就去看看项目里有多少处日期格式化代码是直接写在页面的脚本片段里的找出来该替换的替换该封装的封装。等你把日期这块理顺了JSP项目里最磨人的一部分工作就解决了。
返回列表