
说实话现在再聊 JSP总有种翻旧相册的感觉。JSP 这个名字在 Java 后端开发里曾经是响当当的招牌几乎所有 Java Web 教程都会教你用它做动态页面那时候要说 “Java 做网站”第一反应就是 JSP。可今天新项目里已经很少看到 JSP 的影子了甚至不少年轻开发者只听过名字、没见过真实代码。这篇文章不是给 JSP 唱挽歌而是想认真聊聊它从巅峰滑落的原因是谁替代了它以及现在还在维护 JSP 老项目的人该怎么跟这个“老伙计”相处。如果你是要做技术选型的新手、正在维护遗留系统的开发或者单纯对技术演进感兴趣这篇都值得看完。1. JSP 当年为什么能火起来1.1 那个时代的正确答案服务端渲染就是主流往回看十几年Web 应用的主流形态跟今天完全是两码事。那时候没有前后端分离没有 SPA手机 App 也还没爆发绝大多数 Web 系统都是服务端渲染的前端页面由服务器动态生成用户每次点击浏览器都会向服务器发起请求服务器把处理完的结果渲染成完整 HTML 再返回。JSP 的全称是 JavaServer Pages说白了就是允许开发者在 HTML 里嵌入 Java 代码让页面能够根据请求参数、数据库数据、用户状态动态生成内容。它的底层原理并不神秘JSP 文件会被容器比如 Tomcat先翻译成一个 Servlet 的 Java 源文件再编译成 Class 执行。也就是说JSP 其实是一种“Servlet 的模板化封装”它把 Servlet 里输出 HTML 的繁琐过程简化成了页面写法。在那个年代JSP 几乎是 Java 企业级开发的标准答案。想当年做 OA 系统、ERP、政府门户Java 后端搭配 JSP 页面是标配组合。我印象最深的是 2008 年前后公司接一个业务管理系统前端页面清一色 JSP后台 JavaBean Servlet数据库用 Oracle整套东西跑得很稳。那会儿我身边的同事很少质疑 JSP 的能力因为确实没什么更好的选择。1.2 核心武器在 HTML 里直接写 JavaJSP 当年的核心竞争力就是“门槛低、见效快”。只要你会 HTML再掌握几个 JSP 标签就能把页面跟后端数据打通。最典型的写法是脚本片段和表达式% ListUser users (ListUser) request.getAttribute(users); for (User u : users) { % tr td% u.getName() %/td td% u.getEmail() %/td /tr % } %这段代码放在今天看非常粗糙但在当时这就是日常工作流。从数据库中查出用户列表塞到 request 里页面循环渲染搞定。结合 JSTL 标签库和 EL 表达式还可以进一步去掉大部分脚本片段c:forEach varu items${users} tr td${u.name}/td td${u.email}/td /tr /c:forEach现在网上有个热词叫“jsp个人信息展示页面”这个需求放在 JSP 时代就是上面这个套路。做一个用户中心顶部显示用户名中间是个人的基本资料表格下面可能挂着最近操作记录的列表后端 Servlet 查询数据Forward 到 JSP 页面渲染。一个人包揽前后端大半天就能交付效率和效果在当时都说得过去。1.3 Java 生态光环为什么企业大项目认它JSP 能火不光是本身好用更重要的是它背后站着 Java。那个年代企业级应用采购技术栈里只要有 Java潜意识里就觉着“正规、稳定、可扩展”。Java 有跨平台能力有成熟的类库有大规模并发处理的经验这些都给 JSP 加分。相比之下同时代的 ASP 虽然在微软生态里很流行但平台绑定太紧PHP 上手容易、部署方便但很多大型企业项目对 PHP 的工程化管理、性能调优路径心存疑虑。JSP 的定位正好卡在中间既能写小项目也能撑起大系统而且围绕它有一整套 Servlet、JavaBean、JDBC、XML 配置的完整技术体系。所以无论是银行、电信、政府还是传统企业Java Web 的标配里几乎都会出现 JSP。2. 时代变了JSP 是怎么被一步步抛下的2.1 页面与逻辑耦合成了维护的定时炸弹JSP 快速上手是优点但也埋下了大坑它允许把 Java 代码直接塞进 HTML时间一长页面里的业务逻辑就越堆越多。一个 JSP 文件里能同时出现数据库连接、权限判断、数据格式化、HTML 标签、JavaScript 拼接五颜六色的% %混杂在标签中间。这种代码在项目初期跑得飞快但半年之后再看谁都头疼。改样式可能要顺带解一个 if-else改业务逻辑又得从几百行 HTML 里把 Java 代码捞出来。更难受的是JSP 页面没法做单元测试逻辑正确与否只能靠启动 Tomcat 用浏览器验证。项目规模一大JSP 文件数量动辄上百模块之间的隐式依赖让维护成本直线上涨。工程化思维兴起之后“职责分离”成了铁律。前端负责展示和交互后端负责数据和业务中间用清晰的接口沟通。而 JSP 天然把两件事揉在一起正好站在了这种趋势的对立面。2.2 前后端分离浪潮到来JSP 变成“异类”2013 年之后前端的技术演进明显提速。jQuery 时代还算温和Angular、React、Vue 这些框架强势崛起组件化开发、虚拟 DOM、单页应用SPA的概念开始深入人心。前后端团队逐步分家后端只负责提供 JSON 数据接口前端专注页面渲染和交互再用 Nginx 做静态资源托管。这种架构下JSP 的存在感变得非常尴尬。服务端渲染的整页 HTML 无法直接用于 SPA而且每个页面刷新都要向服务器发起一次完整请求用户体验跟局部刷新、无感切换的 SPA 相比差距明显。移动端爆发更是加剧了这种趋势同一个后端接口可以同时服务 Web、iOS、Android而 JSP 渲染的页面只能在浏览器里看没法复用。我身边一个很真实的例子是 2017 年前后我们团队接手一个电商系统后端 Java前端原来是 JSP。业务方希望把移动端和 PC 端打通讨论之后决定把 JSP 页面全部砍掉后端改造出 REST API前端用 Vue 重写。原因很简单产品需要多端复用JSP 的渲染结果根本供不上移动端使用。2.3 性能、安全、生态三座大山JSP 的硬伤还远不止架构层面的问题。先说性能JSP 每次修改都要被容器重新编译成 Class高并发场景下服务端渲染会持续消耗 CPU 来做字符串拼接与输出而在前后端分离架构里HTML、CSS、JS 都是静态文件可以直接交由 CDN 缓存API 层只传 JSON 数据服务器压力不是一个量级。再谈安全。JSP 里最常见的两个问题一是页面直接拼 SQL极易引发 SQL 注入二是不做输出转义用户输入原样渲染XSS 攻击防不胜防。EL 表达式还出过非常严重的安全漏洞攻击者可以通过精心构造的参数触发任意代码执行。再加上很多老项目把 JSP 文件放在可被直接访问的目录下源码泄露的风险也始终存在。生态方面Spring Boot 时代到来之后JSP 的处境变得更尴尬。Spring Boot 默认支持的是 Thymeleaf官方文档对 JSP 的支持只停留在“可以跑但你需要单独引入额外依赖、修改一堆配置”的程度。嵌入式容器比如内嵌 Tomcat对 JSP 的 JSTL 支持还存在各种坑很多开发者在 Spring Boot 里鼓捣 JSP 失败之后干脆转向 Thymeleaf 或直接上前后端分离。“饿了么 element 图标前端 jsp”这个热词也很有意思。Element UI 是 Vue 的组件库正常情况下它跟前端框架绑定。但当老项目的 JSP 页面需要融入新团队的视觉规范时就会出现这种“在新工具链里找老页面素材”的错位感。这恰恰说明JSP 的生态和现代前端工具链之间已经有了一堵明显的高墙。2.4 人才断层新人不学老人不想写技术被替代人的因素也很关键。现在找一个愿意写 JSP 的 Java 开发者越来越难。新入行的程序员培训机构教的是 Spring Boot、MyBatis、Vue项目实训用的是前后端分离没人教 JSP。而写过 JSP 的老开发转型之后也普遍不想再碰它。模板语法记得不多JSP 特有的编译细节早就忘了大半更关键的是回到旧项目里写 JSP意味着放弃现代开发体验没有热更新、没有友好的调试工具、没有现成的组件库、没有单元测试覆盖。这种落差感导致 JSP 的“存量开发者”也在加速流失。结果就是恶性循环新项目不用 JSP新人就不学 JSP没有人会 JSP企业更不敢在新项目上用 JSP。技术就这样慢慢被边缘化了。3. 别急着说再见现在还在跑 JSP 的角落和求生指南3.1 JSP 没死透因为老系统还活着虽然新项目里 JSP 基本绝迹但遗留系统是个巨大的存在。银行、政务、传统制造企业里很多 2010 年到 2015 年间建设的信息系统至今还在生产环境上稳定运行。这些系统往往稳定、能跑、业务逻辑复杂重构的风险远超收益所以只能继续维护。如果你现在被安排去维护这样的系统JSP 的日常修修补补是逃不掉的。好在这类需求通常不会太复杂比如热词里的“jsp个人信息展示页面”本质上是一个老业务模块的小改动。3.2 实操在 JSP 里安全地做个人信息展示假设你现在的任务是给一个老系统新增一个“个人信息展示页面”后端用 Servlet前端用 JSP。我建议的第一步是把数据获取全部放到后台不要在 JSP 里写任何一行 Java 逻辑。后台 Servlet 大致这样protected void doGet(HttpServletRequest request, HttpServletResponse response) { String userId (String) request.getSession().getAttribute(userId); UserInfo user userService.getUserById(userId); ListOperationLog logs logService.listRecentLogs(userId, 10); request.setAttribute(user, user); request.setAttribute(logs, logs); request.getRequestDispatcher(/userInfo.jsp).forward(request, response); }JSP 页面里只负责展示全部使用 EL 表达式和 JSTL% page contentTypetext/html;charsetUTF-8 pageEncodingUTF-8 % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % html head title个人信息/title /head body div classuser-card h3${user.name}/h3 p邮箱${user.email}/p p手机${user.phone}/p img src${user.avatar} width80 height80 alt头像 / /div h4最近操作记录/h4 table border1 cellpadding4 styleborder-collapse:collapse tr th时间/th th操作内容/th /tr c:forEach varlog items${logs} tr td${log.operateTime}/td td${log.content}/td /tr /c:forEach /table /body /html这两块配合起来页面职责清晰也没有 SQL 拼接风险。有一点要特别提醒老项目里经常在 JSP 里直接用% %输出数据但数据本身没做 HTML 转义这是 XSS 的重灾区。推荐的做法是使用c:out value${user.name}/来输出它默认会转义特殊字符。3.3 实操JSP 里的图片坐标定位怎么做热词里有“jsp图片如何对坐标定位”这个需求通常是给图片添加可点击区域比如户型图上的房间、地图上的热点、产品图上的标签。JSP 里的实现思路跟纯 HTML 没什么本质区别就是用map和area标签只是区域坐标数据可以由后端动态生成。假设你在做一个楼盘户型展示后端返回了每个房间的坐标区域列表img src${housePlanImg} usemap#houseMap alt户型图 / map namehouseMap c:forEach varroom items${roomList} area shaperect coords${room.x1},${room.y1},${room.x2},${room.y2} href${room.detailUrl} alt${room.name} title${room.name} / /c:forEach /map这里的关键点在于坐标数据要从后端业务逻辑计算好再返回不要在 JSP 里用大段 JavaScript 计算坐标。因为一旦把坐标计算逻辑塞进 JSP后续调样式、调尺寸时就会牵一发动全身维护难度成倍上升。还有个常见的坑如果图片是响应式布局直接写死坐标会在缩放时偏移。老项目里最省事的方案是给img加固定宽度或者在渲染时按实际显示比例换算坐标。换算逻辑放后端前端只拿最终坐标。3.4 实操让 JSP 页面加载完自动刷新一次热词“jsp页面让加载完后刷新一次”也是一个经典老需求常见于数据展示大屏、监控页面、或者某些需要“进来就更新”的页面。实现方式有很多种我最推荐的是放在body的onload事件里加延时刷新避免一进页面就打转body onloadsetTimeout(function(){ location.reload(); }, 3000);如果你希望刷新得更干净由服务器重新生成页面内容可以在响应头里加 meta refreshmeta http-equivrefresh content5但这里有个容易被忽视的坑如果页面带表单且用户正在填写自动刷新会把已填数据全丢。所以更稳妥的做法是给刷新操作加一个判断只有页面中某个标识为真时才自动刷新。老项目里可以用 request 的 attribute 做控制c:if test${needAutoRefresh} script window.onload function () { setTimeout(function () { location.reload(); }, 2000); }; /script /c:if另外提醒一下location.reload(true)这种强制从服务器加载的写法在老代码里很常见但在现代浏览器里reload的参数基本被忽略直接用location.reload()就好。4. 还能怎么救老 JSP 系统的渐进式改造路径4.1 别想着推倒重来先想共存面对一个老 JSP 系统最常见的冲动是“赶紧重新写一个”。但在银行、政务这种业务环境里推倒重来 巨大的业务风险、数据迁移成本和测试成本。我更推荐的是渐进式改造先让系统“一边跑一边瘦身”。第一阶段把 JSP 中乱七八糟的脚本片段清除掉业务逻辑全部下沉到后台 Servlet 和 Service 层。JSP 只保留 HTML 展示、EL 表达式和 JSTL 标签。这个阶段不需要改架构但维护体验会明显变好。第二阶段把纯展示的页面逐步迁移到 Thymeleaf。Thymeleaf 的语法跟 JSP 在思路上有些相似同样支持服务端渲染但它是自然模板不需要引入额外的 JSP 容器支持在 Spring Boot 里开箱即用对开发工具的兼容性也好很多。第三阶段如果业务需要多端复用再把核心模块的接口改造成 REST API前端用 Vue 或 React 重写。但这一步不着急按业务优先级逐个切换即可。4.2 过渡方案JSP 页面里局部调用 API如果不想一次性改完可以做一个折中方案老的 JSP 页面继续跑但页面中的部分区块改为通过 JavaScriptfetch调用后端接口来渲染。这种混搭模式虽然在架构上不“纯洁”但在老项目改造过程中却非常实用。div idlatestOrders/div script fetch(/api/orders/latest, { credentials: same-origin }) .then(function (response) { return response.json(); }) .then(function (data) { var html ul; data.forEach(function (item) { html li item.orderNo - item.amount /li; }); html /ul; document.getElementById(latestOrders).innerHTML html; }); /script这种做法的好处是风险可控页面主体结构不变只是局部用新接口替换旧渲染逻辑。等这一块的接口稳定了后续再把整个页面迁到新前端框架已经是水到渠成的事。当然混搭模式也有坑。老系统通常重度依赖 session接口调用时一定要带上credentials: same-origin否则登录状态一丢接口会全部返回 401。还有一处容易踩雷老系统的接口返回值格式五花八门有的是 JSON 包一层{code,msg,data}有的直接返回数组改造前先统一好接口格式不然前端改起来很痛苦。4.3 改造中的关键避坑点老系统改造最怕的就是“改一处坏一片”。我个人的经验是动手前先做三类检查权限控制是否在 JSP 页面上做了硬编码改造后会不会绕过原有权限逻辑。接口依赖的 session、cookie 属性名是否改动特别是跨模块共享的数据。日志链路是否清晰老系统很多磨人的问题都得靠日志定位改造时要顺手把原先散落的日志串起来。数据格式也要留意。老系统里日期往往是yyyy-MM-dd HH:mm:ss字符串新接口如果返回时间戳前端展示就会出偏差。金额字段老系统用 BigDecimal但有的接口转成 String有的转成 double精度丢失的风险要提前评估。5. 实战排雷JSP 老项目最常见的几个疑难杂症5.1 改了 JSP 页面死活不生效这是 JSP 维护中出现频率最高的一个问题。明明改了文件刷新浏览器却还是旧页面。原因基本是 Tomcat 的工作目录里保留了编译缓存。处理方式不复杂找到 Tomcat 的work目录把对应应用的编译产物清空重启容器。如果是在开发环境可以把Context的reloadable属性设置为true这样 JSP 修改后容器会自动检测并重新编译。但生产环境别这么干动态热部署 JSP 在高并发下容易引发不可预期的问题老老实实走发布流程。5.2 EL 表达式取不到值${user.name}渲染出来是空字符串这个问题也很常见。排查方向有三个先看 request 域里有没有这个 attribute 名再看 JSP 页面开头有没有声明 JSTL 标签库最后确认对象属性是否遵循了 JavaBean 规范。很多老代码在后台塞数据时用的是request.setAttribute(userInfo, user)页面里却写${user.name}不匹配自然取不到。还有一种隐蔽情况JSP 页面里同时存在脚本片段和 EL 表达式脚本片段在页面顶部设置了局部变量EL 表达式却取 request 域的值两者互相干扰。这种代码在老项目里很常见我建议遇到就直接把脚本片段删掉统一用 EL。5.3 中文乱码老 JSP 项目的中文乱码大概有四个来源请求参数编码、响应输出编码、数据库连接编码、页面文件本身编码。排查时先看一眼 JSP 顶部的pageEncoding是不是 UTF-8然后看 Servlet 里有没有设置response.setCharacterEncoding(UTF-8)再看数据库连接串里有没有characterEncodingUTF-8参数。一个容易被忽略的地方是老项目很多用的是 Tomcat 8 之前的版本默认请求编码不是 UTF-8。如果改不了容器版本可以在 Servlet 里全局加一个 Filter把所有请求和响应的编码统一设置为 UTF-8。5.4 常见问题速查表症状常见原因处理办法修改 JSP 后页面不变Tomcat 编译缓存未清理清空 work 目录并重启JSP 页面直接下载/输出源码容器未正确配置 JSP 解析器检查 web.xml 中 servlet 映射EL 表达式全部原样输出JSP 未开启 EL 支持检查isELIgnored配置上传文件大小受限容器默认限制调大 maxPostSize/maxSwallowSize页面报 500日志无异常脚本片段里捕获了异常被吞掉搜% try代码块页面样式错乱相对路径在子目录下失效使用${pageContext.request.contextPath}拼接资源路径这堆问题我在老项目维护里基本都撞过一遍。最磨人的不是技术难度而是老代码里的“历史包袱”导致错误定位困难。遇到问题别急着改代码先加日志、抓包、看浏览器的 Network 面板把问题范围缩小到某一层再动手。我的一点真实感受最后一次正经写 JSP是帮一个老系统加一个报表导出功能。前后忙了两个小时大部分时间不是在写代码是在跟它的脚本片段纠缠。修好那一刻我心里没有成就感反而有点感慨技术没有绝对的优劣只有跟时代的匹配度。JSP 陪很多人走过了 Web 开发最粗粝也最有生命力的阶段它确实老了但还没完全退场。如果你恰好还在维护 JSP 老项目别急着全盘否定它跟随系统先跑稳、再一步步改造守住稳定再图创新这才是对老系统负责任的态度。