
很多朋友一听到 JSP 三个字母第一反应是这都什么年代了还学这玩意儿说实话我在带项目新人的时候也一度这么想。但真正上手做完一个完整项目才发现JSP 作为 Java Web 入门的老牌技术它在教学体系里的位置至今没有同类替代品——不是因为 JSP 多先进而是因为它把网页怎么和后端代码打通这件事以最直观的方式暴露在开发者面前。这篇文章不是课程文档是我基于一个完整的 JSP 入门实战项目——学生信息管理系统——整理出来的全过程记录。项目虽然叫学生信息管理系统但我刻意把它做成了一种可复用的骨架你会看到怎么搭建环境、怎么写页面、怎么做 JDBC 数据交互、怎么理解 JSP 编译机制还会单独拆解几个高频搜索里大家都在问的细节——JSONArray 怎么引入、离开页面的提示怎么屏蔽、文件夹选择怎么做、编译后的 class 文件到底存在哪。适合刚学完 Java 基础、准备用 JSP 做第一个完整项目的同学也适合那些因为维护老系统被迫重新捡起 JSP 的朋友。1. 为什么 2024 年了还要写 JSP这门老技术的入门价值1.1 一个真实的尴尬场景上个月有个读者私信我说公司有个 2015 年上线的内部管理系统用的就是 JSP Servlet Tomcat 6。他接手之后第一件事就是懵双击 JSP 文件浏览器打开是一片空白加了一行 Java 代码报错连个具体行号都看不懂最后连那个离开页面时的确定要离开吗弹窗都不知道该去哪改。这个场景太典型了。现在的教程都在讲 Spring Boot Vue 前后端分离没人教你怎么对付老项目。但现实是国内大量中小企业的内部系统、高校的课程设计、外包交付的遗留代码JSP 存量非常大。你可以不靠它找工作但你得有能力看明白它。1.2 JSP 能帮你搞懂哪些 Java Web 底层机制抛开过时的成见JSP 作为入门项目有几个不可替代的价值。第一它能让你直观理解动态网页到底是什么。JSP 本质上是在 HTML 里嵌入 Java 代码服务端把代码执行完再把纯 HTML 扔给浏览器。这个服务端执行和客户端渲染的边界是后面学所有 Web 框架的地基。第二它能帮你吃透 Servlet 生命周期。每个 JSP 页面最终都会被翻译成一个 Servlet 类翻译、编译、加载、实例化、调用_jspService()方法这一套流程你亲眼看过编译产物之后就不会再忘。第三它能训练你排查问题的基本功。JSP 项目没有 Spring Boot 那么完善的自动配置很多问题都要手动去 Tomcat 目录里翻日志、看编译文件。这种手动挡的调试习惯恰恰是很多直接上手框架的人缺失的。所以我的结论很明确如果你是 Java 初学者与其直接冲进 Spring Boot 的汪洋大海不如先花两周做一个 JSP 实战项目。框架会帮你屏蔽复杂但也屏蔽了理解。2. 实战项目立项做一个学生信息管理系统需要准备什么2.1 项目功能边界与页面规划很多入门者做项目喜欢贪大一上来就想做权限管理、角色体系、多租户结果查资料的时间比写代码的时间还多。我的建议是第一个 JSP 项目把范围控制在一个清晰的小闭环里。我做的这个学生信息管理系统功能边界只有四个学生信息的列表展示新增学生表单提交编辑与删除带操作确认按照姓名或学号模糊查询对应的页面就是三张list.jsp列表 查询、add.jsp新增表单、edit.jsp编辑表单。其中列表页我会单独做一个个人信息展示卡片视图的变体用来讲解 JSP 里循环渲染的两种不同写法。功能虽然简单但覆盖了 JSP 开发的所有核心环节请求转发、参数接收、JDBC 操作、循环与条件渲染、乱码处理、编译机制。项目的目录结构我直接用了 Eclipse Dynamic Web Project 的标准布局StudentManager/ ├── src/ │ ├── com/student/entity/Student.java │ ├── com/student/dao/StudentDao.java │ └── com/student/util/DBUtil.java ├── WebContent/ │ ├── list.jsp │ ├── add.jsp │ ├── edit.jsp │ └── WEB-INF/ │ ├── web.xml │ └── lib/提示不要把 JSP 页面直接扔到 WEB-INF 外面就完事。WEB-INF 下的页面不能通过 URL 直接访问必须经过 Servlet 转发这是一种安全控制手段。但在入门阶段为了少走弯路JSP 放在 WebContent 根目录、直接用浏览器访问是可以接受的。2.2 环境搭建的版本选择环境这块我用的是经典三件套刻意避开了最新版本原因是兼容性资料最好找JDK 1.8没错还在用 8老项目最稳的就是它Tomcat 9.0 或者 Tomcat 8.5Servlet 4.0 规范支持 JSP 2.3Eclipse IDE for Enterprise Java and Web Developers或者 IDEA 社区版加 Smart Tomcat 插件这几样你只要保证 JDK 装好、JAVA_HOME环境变量配好、Eclipse 里能正常启动 Tomcat 就行。有一个特别容易踩的坑Tomcat 的运行环境Runtime Environment必须关联到本地 JDK而不是 JRE。在 Eclipse 的 Window → Preferences → Server → Runtime Environments 里选择你安装的 JDK 路径否则 JSP 编译的时候可能出 Unable to compile class for JSP 的报错。这个错我见过太多次了十有八九是 JRE/JDK 选择的问题。建完项目先在WebContent下新建一个最简单的index.jsp用浏览器访问http://localhost:8080/StudentManager/index.jsp看到页面输出就算通了。2.3 数据库设计与连接工具类数据库我用 MySQL 5.7建库建表语句非常简单CREATE DATABASE student_db DEFAULT CHARACTER SET utf8mb4; CREATE TABLE t_student ( id INT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(20) NOT NULL UNIQUE, name VARCHAR(50) NOT NULL, gender CHAR(1) DEFAULT 男, age INT, major VARCHAR(50) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;JDBC 连接工具类走的是最标准的套路注意看 URL 里的编码参数package com.student.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtil { // 这里写你的数据库账号密码 private static final String URL jdbc:mysql://localhost:3306/student_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai; private static final String USERNAME root; private static final String PASSWORD 123456; static { try { Class.forName(com.mysql.jdbc.Driver); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USERNAME, PASSWORD); } }我还是用老版的com.mysql.jdbc.Driver如果你用的是 MySQL 8 的驱动要换成com.mysql.cj.jdbc.Driver。连接池现在先不引入入门项目直接DriverManager拿连接足够等你理解了连接生命周期再换 Druid 或 HikariCP 不迟。3. 核心开发JSP JDBC 完成页面渲染与增删改查3.1 DAO 层数据操作和页面彻底分离虽然 JSP 允许你在页面里直接写 Java 代码但绝不建议把 JDBC 连接、数据库查询全堆在 JSP 里。那会让页面变成一坨意大利面改一个字段要翻半天。我习惯的做法是写一个简单的 DAO 类把数据访问逻辑全部收敛进去。以查询全部学生为例package com.student.dao; import java.sql.*; import java.util.ArrayList; import java.util.List; import com.student.entity.Student; import com.student.util.DBUtil; public class StudentDao { public ListStudent findAll() { ListStudent list new ArrayList(); String sql SELECT * FROM t_student ORDER BY id DESC; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { while (rs.next()) { Student stu new Student(); stu.setId(rs.getInt(id)); stu.setStudentNo(rs.getString(student_no)); stu.setName(rs.getString(name)); stu.setGender(rs.getString(gender)); stu.setAge(rs.getInt(age)); stu.setMajor(rs.getString(major)); list.add(stu); } } catch (SQLException e) { e.printStackTrace(); } return list; } // 其余 add / update / delete / findByName 方法省略套路一致 }JSP 页面里做数据渲染时先导入java.util.List和我们自己的 DAO然后调用方法拿数据。这里有一个很多人会漏掉的细节JSP 页面顶部需要显式声明使用到的类的 import写法如下% page languagejava contentTypetext/html; charsetUTF-8 pageEncodingUTF-8% % page importjava.util.List % % page importcom.student.dao.StudentDao % % page importcom.student.entity.Student %3.2 列表页的两种循环渲染写法列表页list.jsp的渲染方式我建议新手把两种都练一遍。第一种是经典的% %脚本片段 for循环这是老代码里最常见的table border1 cellpadding8 tr th学号/thth姓名/thth性别/thth年龄/thth专业/th /tr % StudentDao dao new StudentDao(); ListStudent students dao.findAll(); for (Student stu : students) { % tr td% stu.getStudentNo() %/td td% stu.getName() %/td td% stu.getGender() %/td td% stu.getAge() %/td td% stu.getMajor() %/td /tr % } % /table第二种是用 JSTL 标签页面干净很多也是当前维护老项目的主流做法。用 JSTL 需要在WEB-INF/lib下放入jstl.jar和standard.jar然后在页面头部引入% taglib prefixc urihttp://java.sun.com/jsp/jstl/core %c:forEach items${students} varstu tr td${stu.studentNo}/td td${stu.name}/td td${stu.gender}/td td${stu.age}/td td${stu.major}/td /tr /c:forEach注意使用 JSTL 的${}EL 表达式时students这个变量必须通过request.setAttribute(students, list)或pageContext.setAttribute(...)放进某个作用域不能像脚本片段那样直接new StudentDao()调方法。从看得懂老代码的角度你两种都得会从自己写新代码的角度我推荐 JSTL。这也是我个人的判断脚本片段只用于入门理解生产项目里能不用就不用。3.3 表单提交与中文乱码的两道关卡新增页面add.jsp核心是一个 form 表单提交到doAdd.jsp或者一个 Servlet。我用一个专门的doAdd.jsp来处理接收参数和入库它更直观% page languagejava contentTypetext/html; charsetUTF-8 pageEncodingUTF-8% % request.setCharacterEncoding(UTF-8); String studentNo request.getParameter(studentNo); String name request.getParameter(name); String gender request.getParameter(gender); String ageStr request.getParameter(age); String major request.getParameter(major); Student stu new Student(); stu.setStudentNo(studentNo); stu.setName(name); // ... StudentDao dao new StudentDao(); dao.add(stu); response.sendRedirect(list.jsp); %这里最大的坑是中文乱码。乱码本质上是编码不一致导致的涉及两道关卡第一道请求参数的编码。在拿任何request.getParameter()之前必须先调用request.setCharacterEncoding(UTF-8)。但这只对 POST 提交有效GET 请求的参数在 Tomcat 8 之后默认就是 UTF-8Tomcat 7 及之前需要改server.xml里的URIEncodingUTF-8。第二道响应的编码。页面头部contentTypetext/html; charsetUTF-8和pageEncodingUTF-8必须同时都写对。pageEncoding是 JSP 文件本身保存时用的编码contentType是响应给浏览器时的编码。两个不一致生成的 HTML 就是乱码。提示如果你在 JSP 里看到% page contentTypetext/html;charsetGBK %那是老项目常见的 GBK 编码最好统一改成 UTF-8否则和现代前端工具对接时全是乱码。3.4 个人信息展示页面的变体实践热搜词里有个jsp个人信息展示页面我在项目中把它实现成了一个卡片视图变体。和表格不同的是卡片视图更强调 JSP 的片段复用能力。我写了一个student_card.jspf的片段文件存放单个学生的展示卡片 HTML 结构% page contentTypetext/html; charsetUTF-8 pageEncodingUTF-8% div classcard h3% stu.getName() %/h3 p学号% stu.getStudentNo() %/p p性别% stu.getGender() %/p p年龄% stu.getAge() %/p p专业% stu.getMajor() %/p /div然后在主页面里用% include filestudent_card.jspf %引入。注意静态 include 会在编译阶段直接把文件内容嵌入进来相当于把代码原样复制所以这个片段里可以直接使用主页面声明的stu变量。如果你是初学者这个行为可能有点反直觉但理解了 JSP 编译机制后就会恍然大悟——这也是我为什么把编译机制放在后面专门讲。4. 深入 JSP 编译机制class 文件到底生成在哪里4.1 JSP 是怎么变成 Servlet 的讲清楚这个问题热搜里那个jsp编译class文件保存在哪里和web项目配置tomcat后查看jsp编译后的java类两个问题就一起解决了。JSP 文件本身不是直接运行的。当浏览器第一次请求一个 JSP 页面时Tomcat 会经历一个完整的翻译编译流程把.jsp文件翻译成.java文件一个 Servlet 类继承自org.apache.jasper.runtime.HttpJspBase用 javac 把.java编译成.class文件加载该 class创建实例调用其_jspService()方法把页面内容作为响应输出关键在于第 1、2 步生成的中间文件放在哪里。它们并不在你的项目源码目录里而是在 Tomcat 的工作目录下。默认路径是$CATALINA_BASE/work/Catalina/localhost/项目上下文路径/org/apache/jsp/其中$CATALINA_BASE就是 Tomcat 的根目录。比如我的项目上下文路径是/StudentManager那么完整路径就是D:\apache-tomcat-9.0.80\work\Catalina\localhost\StudentManager\org\apache\jsp\这个目录下你能看到list_jsp.java和list_jsp.class这类文件命名规则是JSP 文件名 _jsp。如果 JSP 在子目录下会相应生成多级包路径比如admin/userList.jsp对应的就是org/apache/jsp/admin/userList_jsp.java。4.2 work 目录里到底有什么逐个看我在实际排查项目时经常打开这个目录它真的能解释很多诡异问题。打开list_jsp.java你会发现几件事第一JSP 里写的所有脚本片段都被原封不动地搬进了_jspService()方法里。第二HTML 静态部分变成了out.write()调用每个标签都可能被拆成多次输出。第三JSP 里的声明%! %会被翻译成类的成员变量或方法脚本片段% %翻译成方法内的局部代码表达式% %翻译成out.print()。举个例子我在 3.4 节说的静态 include 为什么能共享变量因为打开编译后的list_jsp.java你会发现被 include 的student_card.jspf内容完全被内联进了_jspService()方法里所以它当然可以使用主页面作用域里的stu变量。这个原理对你排查错误非常有用。比如页面报错时 Tomcat 日志里给的行号有时候指向的是编译后的list_jsp.java的行号而不是 JSP 源文件的行号。如果你不知道去 work 目录看这个 Java 文件光靠 JSP 原文件排查会非常痛苦。4.3 手动触发重新编译的两个手段搞清楚编译机制后你还要知道怎么逼Tomcat 重新编译 JSP。默认情况下Tomcat 开启了 JSP 的自动检测机制development模式它通过文件时间戳判断 JSP 是否被修改有修改就会自动重新翻译编译。这也是为什么你改了 JSP 保存后刷新页面就能看到效果不需要重启 Tomcat。但有些情况自动检测会失灵例如文件时间戳没变但内容变了比如用 SVN 或 Git 切换分支后文件的修改时间可能没刷新你手动删除了 work 目录里的某个_jsp.class但.java还在页面报错后 Tomcat 进入半缓存状态最快的处理办法就是关掉 Tomcat把work/Catalina/localhost/项目名这个目录整个删掉再重启 Tomcat。第一次访问时它会重新生成全部编译文件。这招我称之为JSP 三板斧之一另外两板斧是mvn clean和重启 IDE 缓存对付开发期的诡异问题屡试不爽。4.4 一个容易被忽略的生产部署差异开发环境里 JSP 是动态编译的但生产环境很多团队会把 JSP 预编译成 class 文件放进部署包这样能减少首次访问的延迟也方便检查语法错误。实现方式是使用 Ant 任务或 Maven 插件调用 JSP 编译器的JspC命令。不过入门阶段我不建议你玩预编译先老老实实让 Tomcat 动态编译就好。但有一点要知道生产环境务必把web.xml或context.xml里的开发模式关掉比如在WEB-INF/web.xml中设置servlet servlet-namejsp/servlet-name servlet-classorg.apache.jasper.servlet.JspServlet/servlet-class init-param param-namedevelopment/param-name param-valuefalse/param-value /init-param init-param param-namemodificationTestInterval/param-name param-value3600/param-value /init-param /servlet否则生产环境每次访问都会去检查 JSP 文件时间戳性能差不说万一有人在服务器上误改了 JSP线上直接出问题。5. 三个高频需求落地JSONArray 引入、离开提示屏蔽、文件夹选择这部分我直接对应热搜词里大家搜得最多的几个具体问题来写每个都是真实开发里会碰到的。5.1 在 JSP 页面里正确引入 JSONArray热搜词里的jsp jsonarray import我太熟了很多人想在 JSP 里生成 JSON 数据返回给前端结果引入包的时候一头雾水。JSP 里使用 JSONArray 之前第一步是搞清楚你用哪一个库。常见的有三种库名称包名典型引入坐标json-libnet.sf.jsonnet.sf.json:json-lib:2.4org.jsonorg.jsonorg.json:json:20231013Fastjsoncom.alibaba.fastjsoncom.alibaba.fastjson:fastjson:1.2.83我推荐新手用 org.json依赖少不需要额外引入 commons-beanutils 那一堆东西。把 jar 包放到WEB-INF/lib后在 JSP 头部这样写 import% page importorg.json.JSONArray % % page importorg.json.JSONObject %使用示例% ListStudent students new StudentDao().findAll(); JSONArray arr new JSONArray(); for (Student stu : students) { JSONObject obj new JSONObject(); obj.put(id, stu.getId()); obj.put(name, stu.getName()); obj.put(studentNo, stu.getStudentNo()); arr.put(obj); } out.print(arr.toString()); %警告如果你用的是 json-libnet.sf.json它依赖commons-lang、commons-beanutils、commons-collections、commons-logging四个包少一个直接抛NoClassDefFoundError。新手排查不出原因就卡在这。所以首选 org.json 是最省心的。另一个要注意的是JSONArray和JSONObject的循环引用问题。如果你直接把带双向关联的实体类塞进 JSONObject会抛出JSONException: JSONArray initial value should be a string or collection or array。解决办法是像上面示例那样手动一个个 put 字段别图省事直接把对象丢进去。5.2 屏蔽离开页面提示的正确姿势热搜词里的屏蔽jsp离开页面提示对应的场景是你在 JSP 页面上做了表单填了一半不小心刷新或关闭浏览器弹出确定要离开吗的提示而这个提示在某个条件下根本不该出现。这个提示不是 JSP 的功能是浏览器原生的beforeunload事件触发的。很多人误以为要改 JSP 后端代码其实完全在前端处理。如果你的项目里没有主动绑定这个事件那弹出提示的源头可能是某个第三方 JS 组件或弹窗插件自己注册的。要屏蔽它思路是如果有绑定就解绑或覆盖。在 JSP 页面的head或页面底部加入window.onbeforeunload null;如果原代码是用addEventListener绑定的直接置空onbeforeunload不一定有效因为addEventListener注册的处理器和onbeforeunload属性是两套独立的机制。这时候你得找到绑定源头那通常是这样的代码window.addEventListener(beforeunload, function (e) { e.preventDefault(); e.returnValue 确定要离开吗; });要精确移除需要把回调函数提取成命名函数function leaveConfirmHandler(e) { e.preventDefault(); e.returnValue 确定要离开吗; } // 需要开启时绑定 window.addEventListener(beforeunload, leaveConfirmHandler); // 需要屏蔽时移除 window.removeEventListener(beforeunload, leaveConfirmHandler);还有一种常见需求是只有表单有改动时才提示没有改动就不提示。很多实现会把确认提示无条件绑定导致用户点了一个普通跳转链接也弹窗。正确做法是加一个脏标记let formDirty false; document.getElementById(myForm).addEventListener(input, function () { formDirty true; }); window.addEventListener(beforeunload, function (e) { if (formDirty) { e.preventDefault(); e.returnValue 表单尚未保存确认离开吗; } }); // 保存成功后置回 false function markSaved() { formDirty false; }这个方案在涉政、敏感、低俗内容管控场景也通用——凡是涉及重要表单的页面都建议用脏标记来控制提示而不是无脑绑定。5.3 JSP 场景下的文件夹选择功能jsp选择文件夹这个问题其实也偏前端它对应的是 HTML 5 的webkitdirectory属性。传统input typefile只能选文件但加上webkitdirectory后可以选择整个文件夹浏览器会把文件夹内所有文件的相对路径作为webkitRelativePath属性暴露出来。在 JSP 页面里直接写:form actionupload.jsp methodpost enctypemultipart/form-data input typefile namefolder webkitdirectory button typesubmit上传文件夹/button /form后端接收时要注意JSP 里处理文件上传不能再用request.getParameter()因为enctypemultipart/form-data会让参数以二进制流方式提交。要么用 Servlet 3.0 的MultipartConfigrequest.getPart()要么用 Commons FileUpload 组件。初学者最容易在这里栽跟头一提交就发现getParameter()返回 null。如果你是做老系统的维护还要注意低版本 IE 完全不支持webkitdirectory一般要降级为普通文件选择框。可以用简单判断if (typeof HTMLInputElement.prototype.webkitdirectory undefined) { alert(当前浏览器不支持文件夹选择请使用 Chrome/Edge 或选择单个文件); }6. 实测踩坑记录新手最容易卡住的六个环节做这个实战项目的过程中我把团队新人问过的问题和我自己踩过的坑汇总成了下面的清单按出现频率排序6.1 Tomcat 端口被占用启动 Tomcat 报Port 8080 required by Tomcat v9.0 Server at localhost is already in use多半是之前没关干净或者别的程序占了端口。解法不复杂找到占用进程杀掉或者改 Tomcat 的server.xml里的端口。但有一件事要留意server.xml里有三个端口要一起改HTTP 端口默认 8080、AJP 端口默认 8009、关闭指令端口默认 8005。我见过只改 8080 不改 8005 的结果下次启动照样冲突。6.2ClassNotFoundException: com.mysql.jdbc.Driver这个错误十有八九是 mysql-connector.jar 没有放进WEB-INF/lib。注意不是放在构建路径里就行Web 应用运行时用的是WEB-INF/lib下的 jar。Eclipse 里有时候你把 jar 拖进了项目但没复制到 lib 目录部署的时候就找不到。6.3 修改了 JSP 但页面没变化先排除浏览器缓存CtrlF5 强制刷新再看 Tomcat 的 work 目录下对应的_jsp.java文件时间戳有没有更新。如果时间戳没变按 4.3 节的办法删 work 目录重启。还有一个小众原因Eclipse 的自动部署Auto Reload没开启改完 JSP 不会同步到 Tomcat 的 webapps 目录。6.4 404 页面提示找不到 JSP检查你访问的 URL 和项目上下文路径是否一致。http://localhost:8080/StudentManager/list.jsp中间那段StudentManager是项目部署名。用 Eclipse 部署时右键项目 → Properties → Web Project Settings 里的 Context Root 可能和你预期不一致改了就不会 404。6.5 500 错误 Unable to compile class for JSP按堆栈信息找到具体是哪一行 JSP 语法错误。最常见的是%和%没配对或者页面里写了System.out.println忘记加分号。记住一点JSP 里的脚本片段是 Java 代码语法要求比 HTML 严格得多。6.6 数据库出现?号乱码页面显示正常但插入数据库后中文变成??。这不是页面编码问题是 JDBC 连接 URL 里少了characterEncodingutf8或者数据库表本身不是 utf8 字符集。按 2.3 节的 URL 统一配置同时用ALTER TABLE t_student CONVERT TO CHARACTER SET utf8mb4;修正已有表。7. 以这个项目为跳板下一步怎么走做完学生信息管理系统你已经把 JSP 的核心用法过了一遍。接下来我建议做三件递进的事。第一件把 DAO 层改成 Servlet JSP 的标准 MVC 模式。现在你的页面里还直接调用了 DAO真正的老项目里通常是浏览器请求 ServletServlet 调 Service/DAO再把结果forward到 JSP 渲染。这一步能让你理解请求分发的核心逻辑。第二件引入 JSTL 和 EL把你页面里的% %脚本全部替换掉。替换完成之后你会发现JSP 页面从带 Java 代码的 HTML变成了带标签的 HTML可读性提升一大截。第三件把 JDBC 换成连接池加上 Filter 处理统一编码和登录拦截。这两个东西一加项目的工程化程度就完全不一样了。我个人带了这么多年新人越来越觉得 JSP 就像学车时候的手动挡教练车——你以后可能开自动挡但手动挡教会你的离合、换挡、半联动原理会让你在遇到任何变速箱问题时都不慌。技术会迭代但底层机制不会变。当你某天被一个老系统逼着去work/Catalina/localhost/项目名/org/apache/jsp/目录翻编译文件时你会感谢今天认真看完编译机制的那个自己。