
1. 为什么今天还要聊JSP先把结论摆在前面JSP不是过时的老古董它是一套把Java代码嵌入HTML页面的服务端模板技术全称Java Server Pages。你写一个后缀为.jsp的文件扔到支持Servlet规范的容器里用户访问时容器会把它翻译成Servlet再编译执行最终吐出一段HTML给浏览器。它能做什么最直接的就是做动态网页——根据数据库里的数据渲染列表、根据登录状态显示不同内容、处理表单提交。适合谁看刚接触Java Web的新人、需要维护老系统的后端同学、以及想搞明白“页面到底是怎么被拼出来的”这类底层问题的开发者。我见过太多人一上来就学Spring Boot加前后端分离结果连请求怎么从浏览器走到Java代码、响应怎么回去都说不清楚。JSP恰好是理解这条链路最朴素的入口。它没有魔法容器帮你做的事都摆在明面上翻译、编译、加载、执行。把这套流程吃透后面看任何模板引擎Thymeleaf、FreeMarker都是换汤不换药。这篇内容我会从设计思路讲到实操落地再到踩坑排查尽量把当年我自学时没人告诉我的细节都补上。你不需要有Servlet基础但最好知道HTML和一点点Java。准备好了我们就开始。2. JSP的整体设计与运行思路拆解2.1 它到底解决了什么问题早期做动态网页纯Servlet是唯一选择。但Servlet写HTML有多痛苦你得在Java代码里一行行out.println(html)页面稍微复杂一点代码就没法看前端改个样式还得找后端。JSP的出现就是为了把“页面展示”和“业务逻辑”在书写层面分开HTML照常写HTML需要动态内容的地方用% %插一段Java。它的核心设计哲学是“先翻译后执行”。你写的.jsp文件本身不是最终运行的东西容器比如Tomcat在第一次被访问时会把它翻译成一个Java源文件本质是个Servlet再编译成.class之后每次请求都执行这个编译好的类。所以JSP第一次访问慢后面快这个现象你现在就能解释清楚了。2.2 为什么是“翻译成Servlet”而不是自己搞一套引擎这是理解JSP的关键。容器厂商不想重新发明一套运行时Servlet规范已经定义好了请求响应模型、生命周期、会话管理。JSP只是在这套模型上加了一层“模板语法糖”。翻译后的Servlet里你写的HTML变成了out.write(...)你写的% %脚本片段原封不动变成方法体里的Java代码。这样设计的好处是JSP天然拥有Servlet的一切能力——能拿request、response、session、application能调用任何Java类。代价是它和Servlet容器强绑定换个容器行为可能有细微差别。我个人的判断是理解这层翻译关系比死记JSP语法重要十倍因为所有诡异报错几乎都能从翻译后的代码里找到答案。2.3 三种脚本元素的定位差异JSP的脚本元素就三类但很多人用混。我用一个表格把它们讲清楚元素语法翻译后的位置典型用途脚本片段% 代码 %_jspService()方法体内写逻辑、循环、判断表达式% 值 %out.print(值)输出变量、计算结果声明%! 代码 %类成员位置定义方法、成员变量这里有个新手极易踩的坑声明里定义的是类级别的成员多个用户共享涉及并发安全而脚本片段里的变量是方法级别的局部变量每个请求独立。我当年就因为把计数器写在声明里结果所有用户共用一个值排查了半天。记住这个区别能省你很多时间。2.4 内置对象是白送的工具箱JSP翻译后容器自动帮你准备好了九个内置对象你不用new就能直接用。最常用的几个request拿请求参数、response设置响应、session存会话、application存全局、out输出内容。它们本质就是翻译后Servlet方法里的局部变量容器在方法开头帮你赋值了。理解这一点后你就明白为什么内置对象只能在脚本片段和表达式里用不能在声明里用——因为声明翻译到类成员位置那里根本没有这些局部变量。这个逻辑链条一旦打通很多“为什么报错说找不到符号”的问题就迎刃而解了。3. 核心语法细节与实操要点3.1 指令元素page、include、taglib指令用% %表示作用是告诉容器一些页面级别的信息它不产生输出。page指令最常用比如% page contentTypetext/html;charsetUTF-8 languagejava %。这里contentType决定响应头charset决定编码。我强烈建议每个页面都显式写UTF-8否则中文乱码能折腾你一下午。还有个import属性用来导包多个包用逗号隔开。include指令是静态包含% include fileheader.jsp %它在翻译阶段就把目标文件的内容合并进来最终只生成一个Servlet。这意味着两个文件不能有重复的变量名否则编译报错。它适合包含那种固定不变的页头页脚。taglib指令用来引入标签库最经典的就是JSTL。虽然现在用得少了但维护老项目时你一定会遇到知道它长什么样就行。3.2 动作元素include与forward的区别动作元素用jsp:xxx表示它在请求处理阶段执行。最常被拿来对比的是两个jsp:include pagex.jsp /动态包含运行时把另一个页面的输出结果合并进来各自独立编译成Servlet。jsp:forward pagex.jsp /请求转发当前页面停止输出把请求交给目标页面处理浏览器地址栏不变。静态include和动态include的核心差异在于时机前者是“抄作业”把内容抄进来一起编译后者是“叫外援”运行时把对方的产出拿过来拼。我实测下来如果被包含的页面内容会变、或者需要独立处理逻辑用动态include如果是纯静态的公共片段静态include性能更好。3.3 注释的三种写法与可见性这个细节很多人忽略但排查问题时特别有用HTML注释!-- --会发送到浏览器用户查看源码能看到。JSP注释%-- --%翻译阶段就被丢弃浏览器和最终Servlet里都没有。Java注释//和/* */写在脚本片段里翻译后保留在Servlet源码中但不会输出到页面。注意如果你在JSP注释里写敏感信息它是安全的不会泄露到客户端。但写在HTML注释里等于直接告诉用户你的实现细节别干这种傻事。3.4 实操心得脚本片段里别写太多逻辑我见过最夸张的一个JSP一个文件两千多行里面塞了数据库查询、业务计算、HTML渲染。这种代码没法维护也没法测试。我的建议是JSP里只做“取数据、简单判断、输出”复杂逻辑全部放到Java类里JSP通过脚本片段调用。这样即使以后要换模板引擎业务代码也不用动。4. 从零搭建一个可运行的JSP页面4.1 环境准备与目录结构你需要一个支持Servlet规范的容器Tomcat是最常见的选择。假设你已经装好了项目目录结构大致如下webapp/ ├── WEB-INF/ │ ├── web.xml │ └── classes/ ├── index.jsp └── header.jspWEB-INF目录是安全区浏览器无法直接访问里面的文件所以放配置和类文件。JSP页面放在外面才能被直接访问。web.xml是部署描述符虽然现在很多配置可以用注解替代但了解它没坏处。4.2 第一个页面的完整代码我写一个带表单提交和结果展示的页面把常用语法都串一遍% page contentTypetext/html;charsetUTF-8 languagejava % % page importjava.util.Date % % // 脚本片段获取请求参数 String username request.getParameter(username); String message ; if (username ! null !username.trim().isEmpty()) { message 欢迎回来 username; } % !DOCTYPE html html head titleJSP实操演示/title /head body h2当前时间% new Date() %/h2 form methodpost actionindex.jsp input typetext nameusername placeholder输入用户名 / button typesubmit提交/button /form % if (!message.isEmpty()) { % p stylecolor:green;% message %/p % } % /body /html这段代码里request.getParameter拿表单参数% %输出时间% if %做条件渲染。你把它保存为index.jsp启动Tomcat访问就能看到效果。4.3 翻译后的代码长什么样理解翻译结果是排查问题的钥匙。上面那个页面容器翻译后大致是这样我简化了无关部分public final class index_jsp extends HttpServlet { public void _jspService(HttpServletRequest request, HttpServletResponse response) { JspWriter out response.getWriter(); String username request.getParameter(username); String message ; if (username ! null !username.trim().isEmpty()) { message 欢迎回来 username; } out.write(html...); out.print(new Date()); // ... 后续输出 } }看到没你写的HTML变成了out.write脚本片段原样搬进方法体。所以脚本片段里能用的变量就是方法里能访问的变量。这个对应关系一旦建立你对JSP的理解就从“背语法”升级到“懂原理”了。4.4 参数传递与中文乱码处理表单提交中文乱码是经典问题。原因在于浏览器按页面编码提交容器默认按ISO-8859-1解码。解决办法是在读取参数之前设置编码% request.setCharacterEncoding(UTF-8); String name request.getParameter(name); %注意setCharacterEncoding只对POST请求体有效GET请求的参数在URL里编码由容器配置决定。如果GET也乱码需要在容器的server.xml里给Connector加URIEncodingUTF-8。这个坑我踩过当时只改了页面编码GET请求死活不对最后才发现是容器配置的问题。5. 常见问题与排查技巧实录5.1 报错“找不到符号”怎么定位这个报错几乎都出在脚本片段里用了未定义的变量或者声明里用了内置对象。排查步骤先看报错行号对应到你JSP的哪一段然后确认变量是不是在声明里定义的声明里的成员在方法里能用但内置对象在声明里不能用最后检查import有没有漏。我一般会打开容器的工作目录找到翻译后的.java文件直接看那一行比猜快得多。5.2 页面第一次访问特别慢这是正常现象因为要翻译加编译。后续访问走的是编译好的类速度就正常了。如果你在开发阶段频繁改动JSP每次都要等编译可以配置容器的开发模式让它检测到文件变化自动重新编译。生产环境则建议预编译避免第一个用户承担编译开销。5.3 静态include导致的变量冲突两个文件用静态include合并后如果都有String name编译直接失败。解决办法要么改名要么改用动态include。我个人的习惯是公共片段里尽量不定义变量只做纯输出这样最安全。5.4 常见问题速查表现象可能原因解决方向中文乱码编码未统一页面、请求、容器三处都设UTF-8找不到符号变量作用域错误检查声明与脚本片段的区别第一次访问慢翻译编译开销属正常可预编译变量冲突静态include合并改名或改动态include404错误路径或位置不对确认文件在WEB-INF外500错误脚本片段抛异常看翻译后源码定位行号5.5 独家避坑技巧分享几个文档里不会写的经验。第一开发阶段在page指令里加isErrorPagetrue配合错误页面能把异常堆栈显示出来比看容器日志快。第二脚本片段里用完的数据库连接一定要在finally里关闭JSP不像框架有自动管理泄漏几次连接池就满了。第三别在JSP里写System.out.println调试输出会跑到容器控制台用out.println或者干脆打断点。第四如果页面输出内容特别多注意out的缓冲区大小默认8KB超了会自动flush但如果你在flush之后又调用了response.sendRedirect就会报“响应已提交”的错误这个坑很隐蔽。6. 关于JSP的定位与后续扩展把JSP学明白最大的收获不是会用这个技术本身而是理解了“模板翻译成代码”这套机制。现在主流的模板引擎思路都是一样的定义一套语法运行时翻译成目标语言执行。你懂了JSP的翻译逻辑再看Thymeleaf的自然模板、FreeMarker的指令都能快速上手。如果你手上正好有老系统要维护JSP的读写能力是刚需。如果是新项目我个人的建议是前后端分离加REST接口JSP不作为首选。但作为学习Web请求响应链路的教具它依然是最直观的。我到现在还保留着当年写的第一个JSP页面偶尔翻出来看看能想起很多底层的东西。最后分享一个小技巧如果你想快速看清一个JSP翻译后的全貌去容器的work目录下找对应的.java文件那是理解一切JSP行为的最权威资料。遇到任何想不通的现象先看翻译结果基本都能找到答案。