先说个写JavaWeb很容易遇到的事:学完Servlet之后,你可能信心满满地想用纯Servlet写一个页面,结果写了一半人就麻了。
输出一行HTML需要写一句out.println("<html>"),输出一个表格要循环拼字符串,前端改了样式,Java代码里的HTML片段也得跟着改。页面越写越大,Java代码和HTML标签缠在一起,调试时一个引号套错,整个响应直接崩掉。这个痛点,经历过的人都懂。而JSP(Java Server Pages)就是用来解决这个问题的——JavaWeb开发系列写到第六篇,终于轮到它了。
这篇不打算按教科书套路,把<%、<%=、<%!这些语法背一遍就完事。我更想讲清楚三件事:JSP到底解决了什么问题、它和Servlet之间究竟是什么关系、以及实际写项目时,哪些用法会反复用到、哪些坑踩一次就能记一辈子。这篇内容适合刚学完Servlet、正处于“会写接口但不会写页面”阶段的JavaWeb初学者,也适合那些用过JSP但一直没想明白它底层是怎么跑起来的同学。
1. 从Servlet拼HTML的噩梦说起:JSP到底解决了什么问题
1.1 纯Servlet输出页面到底有多痛苦
先看一段代码,想象一下用Servlet输出一个“学生信息管理”的页面:
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html;charset=UTF-8"); PrintWriter out = response.getWriter(); out.println("<!DOCTYPE html>"); out.println("<html>"); out.println("<head>"); out.println("<meta charset=\"UTF-8\">"); out.println("<title>学生信息管理系统</title>"); out.println("</head>"); out.println("<body>"); out.println("<h1>学生列表</h1>"); out.println("<table border=\"1\">"); out.println("<tr><th>ID</th><th>姓名</th><th>成绩</th></tr>"); for (Student s : studentList) { out.println("<tr>"); out.println("<td>" + s.getId() + "</td>"); out.println("<td>" + s.getName() + "</td>"); out.println("<td>" + s.getScore() + "</td>"); out.println("</tr>"); } out.println("</table>"); out.println("</body>"); out.println("</html>"); }这个代码能跑,但它有几个很致命的问题:
第一,改样式等于改Java代码。设计师把表格的border改成1px solid #ccc,你就得打开Servlet源码,找到那一行字符串,小心翼翼地改,改错了引号位置,整个页面直接白屏。
第二,代码可读性极差。Java逻辑和HTML标签混在一个方法里,任何一个人接手这种代码都会想跑路。
第三,开发效率低。写完Java代码要编译、要重启容器、要重新部署,才能看效果。改一个标题都要走一遍这个流程,心态很容易崩。
1.2 JSP的设计初衷:把页面从Java代码里解放出来
JSP的思路和Servlet正好相反:它本质上是一个HTML页面,但允许在里面嵌入Java代码片段。写出来的页面长这样:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>学生信息管理系统</title> </head> <body> <h1>学生列表</h1> <table border="1"> <tr><th>ID</th><th>姓名</th><th>成绩</th></tr> <% List<Student> studentList = (List<Student>) request.getAttribute("studentList"); for (Student s : studentList) { %> <tr> <td><%= s.getId() %></td> <td><%= s.getName() %></td> <td><%= s.getScore() %></td> </tr> <% } %> </table> </body> </html>对比一下感受差异:页面结构一眼能看懂,HTML是HTML,Java只负责其中动态变化的部分。前端同事拿到这个文件,改样式直接改,不用关心Java代码;后端同事要改逻辑,也只盯<% %>里面的部分。
这正是JSP的核心定位:让JSP专注做表现层(View),让Servlet专注做控制层(Controller)。Servlet接收请求、处理业务逻辑、准备好数据,然后把数据交给JSP去渲染成HTML页面。这种分工后来演化成了经典的Model 2 / MVC模式,是JavaWeb项目最基本的架构形态。
顺便说一句,JSP最初的设计灵感其实来自对PHP、ASP这类“能在HTML里直接写服务端代码”的脚本语言的反击。Sun公司希望Java生态也能提供一种类似体验的动态页面技术,同时又不想丢掉Java的类型安全和跨平台优势,于是就把“JSP页面翻译成Servlet”这条路走到底了。
2. JSP不是一门新语言,它本质上就是一个Servlet
2.1 Tomcat在后台是怎么处理JSP的
“JSP就是一个Servlet”这句话,很多教材都写过,但真正理解的人不多。我当年学的时候也疑惑:xxx.jsp只是个文本文件,连.java都不是,它凭什么就是个Servlet了?
关键在于Tomcat(更准确地说是Jasper引擎)会在背后做一层翻译。当你第一次在浏览器请求一个JSP页面时,Tomcat做了这些事情:
- 找到对应的
.jsp文件; - 用Jasper引擎把它翻译成一个
.java源文件,这个类会继承org.apache.jasper.runtime.HttpJspBase; - 通过
javac把.java编译成.class文件; - 加载这个class,创建实例,调用它的
_jspService()方法; - 把
_jspService()方法产生的响应返回给浏览器。
所以你看,经过Tomcat这层翻译,JSP最终还是变成一个Servlet。翻译后的文件放在哪?以Tomcat 9为例,默认在Tomcat安装目录下的work/Catalina/localhost/你的应用名/org/apache/jsp/目录里。如果你用IDEA开发时用的是Tomcat,也能在控制台日志里看到Jasper翻译和编译的路径信息。
举个真实例子,你在JSP里写的这段代码:
<%= s.getName() %>Tomcat翻译成Java之后,大致是这样:
out.print(s.getName());而你写的普通HTML片段:
<h1>学生列表</h1>翻译之后大致是:
out.write("<h1>学生列表</h1>\r\n");我们在JSP里写的所有东西,最终都会变成Java代码里的字符串输出。所以JSP从来没有“绕过”Servlet,它就是Servlet的另一种表达形式。
2.2 九大内置对象在Servlet代码里对应什么
很多人一开始学JSP,会被“内置对象”这个说法搞懵:为什么我在JSP里直接用request、response、session,不用定义就能用?
原因很简单:因为这些变量是Tomcat在生成的_jspService()方法开头就替你写好的。翻译后的方法签名长这样:
public void _jspService(final HttpServletRequest request, final HttpServletResponse response) throws java.io.IOException, ServletException { // 以下变量由容器自动注入 final PageContext pageContext; HttpSession session = null; final ServletContext application; final ServletConfig config; JspWriter out = null; final java.lang.Object page = this; // ... 初始化代码 }也就是说,JSP页面里的Java代码,本质上是写在_jspService()方法内部的,所以才能无障碍访问这些变量。九大内置对象和Servlet里的对应关系,我用一张表整理清楚了:
| 内置对象 | 类型 | 在传统Servlet中的等价物 |
|---|---|---|
request | HttpServletRequest | service方法入参 |
response | HttpServletResponse | service方法入参 |
session | HttpSession | request.getSession() |
application | ServletContext | getServletContext() |
config | ServletConfig | getServletConfig() |
out | JspWriter | response.getWriter() |
pageContext | PageContext | 没有直接对应,页面级上下文 |
page | Object | this(当前Servlet实例) |
exception | Throwable | 仅在isErrorPage="true"时可用 |
这张表值得你收藏起来。很多时候你写JSP觉得“玄学”,本质是没搞清这些对象从哪来。一旦明白了它们就是在_jspService()方法里预定义好的局部变量,一切就都顺了。
2.3 为什么JSP不能重写init和destroy方法
这里有个容易被忽略的细节。JSP页面翻译后的Servlet类,会覆盖init()和destroy()方法,名字叫_jspInit()和_jspDestroy(),但_jspService()方法是不能被覆盖的,它是final的。
所以你在JSP里用<%! %>声明的成员变量和方法,会变成翻译后Servlet类的成员。而你在页面里写的那些脚本片段(<% %>),则是塞进_jspService()方法体里。这就导致一个很关键的结论:
JSP里声明的方法和普通Java方法没什么区别,但JSP里写的脚本代码,相当于写在了一个大方法里面,不是类级别的东西。
很多初学者会在JSP里写这样的代码:
<% int count = 0; count++; // 每次访问页面都重新执行,count永远不会累加 %>如果你以为声明在<% %>里的变量是“全局的”,那结果一定出乎你意料——因为每次请求都会调用_jspService()方法,方法里的局部变量每次都会重新初始化。如果你想让它跨请求保存状态,得用<%! %>声明成员变量,或者更靠谱的做法是放在session或application里。
3. JSP基础语法:哪些在实战中反复用到
3.1 三种脚本元素,别混为一谈
JSP脚本元素分三种,作用和使用场景完全不同。很多新手一上来就把它们搞混,写出来的代码报错还找不到原因。逐一过一遍。
声明(Declaration):<%! %>
声明的是JSP翻译后的Servlet类的成员。可以声明方法、成员变量,但注意:它不能直接调用request、response这些内置对象,因为那些是_jspService()方法内的局部变量。
<%! private String formatScore(double score) { return score >= 60 ? "及格" : "不及格"; } %>实际项目中,<%! %>用得很少。稍微复杂点的逻辑都应该放到Java类里,而不是塞进JSP页面。如果你发现自己在一个JSP里写了一堆方法,停下来想想是不是设计出了问题。
脚本段(Scriptlet):<% %>
写在_jspService()方法体内的Java代码。可以定义局部变量、写if/for循环、调用外部Java类,全部没问题。因为它在方法内部,所以能直接使用所有内置对象。
<% // 从request中取出之前setAttribute的数据 List<Student> list = (List<Student>) request.getAttribute("studentList"); if (list != null && !list.isEmpty()) { %> <ul> <% for (Student s : list) { %> <li><%= s.getName() %></li> <% } %> </ul> <% } %>这种写法在早期的JavaWeb项目里很常见,但是维护起来真的很痛苦。一个页面几百行,一半是HTML,一半是Java逻辑,可读性极差。所以现在的主流实践是:脚本段只用于做简单的控制流,不要写复杂业务逻辑。真要写业务,放Servlet、放Service,不要放JSP。
表达式(Expression):<%= %>
等价于out.print(...),把你写的内容输出到页面上。注意结尾一定不能加分号,这是新手的经典报错点。
<%= student.getName() %>翻译成Java之后就是:
out.write(String.valueOf(student.getName()));在EL表达式普及之前,<%= %>是JSP页面输出动态数据的主要方式。现在虽然有了更优雅的${}写法,但<%= %>在维护老项目、或者需要输出Java方法返回值时仍然会用到,该认识还是得认识。
3.2 page指令里的属性,每一个都很关键
JSP指令是写给容器看的“配置信息”,最常用的是page指令。它的属性非常多,但真正在实战中频繁打交道的就这几个:
| 属性 | 作用 | 常用取值 |
|---|---|---|
pageEncoding | 指定JSP文件本身的保存编码 | UTF-8 |
contentType | 设置响应类型和字符集 | text/html;charset=UTF-8 |
import | 导入Java类 | java.util.List |
session | 是否创建/使用session | true/false |
isELIgnored | 是否忽略EL表达式 | false(推荐) |
errorPage | 本页面出现异常时跳转的页面 | error.jsp |
isErrorPage | 声明本页面是错误处理页面 | true时可用exception对象 |
extends | 指定翻译后的父类 | 一般不用,别乱改 |
写JSP页面时,第一行的page指令建议完整写上这样一行:
<%@ page contentType="text/html;charset=UTF-8" language="java" pageEncoding="UTF-8" %>language="java"其实是默认值,写不写都行。但pageEncoding和contentType最好都写上,并且都用UTF-8,这是避免中文乱码的第一道防线。
3.3 include指令和jsp:include动作:两个“包含”不是一回事
JSP里做“页面复用”,最常见的方式是把公共的头尾、导航栏抽出来,然后在需要的页面里包含进来。这个“包含”有两种写法,很多人在这里掉过坑。
静态包含:<%@ include file="header.jsp" %>
这种包含是“翻译期”的。Tomcat在翻译JSP时,会把被包含的文件内容原封不动地复制到当前页面里,也就是说,最终生成的Servlet只有一个。两个页面合在一起编译,如果两边都定义了同名的id属性或变量,会冲突报错。合并后,静态包含能访问当前页面的变量。
动态包含:<jsp:include page="header.jsp" />
这种包含是“运行时”的。它会把请求转发给被包含的JSP,由它自己生成内容,再把响应内容嵌入当前页面。两个页面各自独立编译成Servlet,互不干扰。因为发生了两次请求,被包含页面不能访问当前页面的局部变量。
什么时候用哪个?我个人的经验:如果被包含的内容和当前页面共享大量数据(比如都在同一个request作用域里取值),用静态包含;如果被包含模块相对独立,用动态包含。从CPU开销来看,静态包含更省,因为没有第二次请求和页面自行编译的开销。但从解耦角度看,动态包含更灵活。老项目里头尾导航通常用静态包含,因为内容固定,而且能直接使用页面里的变量。
4. 一个完整的页面传值展示链路:从Servlet到JSP再到浏览器
4.1 先来搭一个用户信息展示的场景
我见过太多初学者学JSP时只盯着“JSP页面怎么写”,却忽略了JSP的数据是从哪来的。这里用一个非常贴合实际的项目场景:个人信息展示页面,把Servlet和JSP串起来跑一遍。
先定义一个JavaBean——学生信息实体类:
package com.demo.entity; public class Student { private int id; private String name; private int age; private String major; // 无参构造,getter/setter,略 }然后是Servlet,它的职责是:接收请求、组装数据、转发给JSP:
package com.demo.servlet; import com.demo.entity.Student; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.ArrayList; import java.util.List; @WebServlet("/student/list") public class StudentListServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 模拟从数据库查询学生列表 List<Student> studentList = new ArrayList<>(); studentList.add(new Student(1, "张三", 20, "计算机科学与技术")); studentList.add(new Student(2, "李四", 21, "软件工程")); studentList.add(new Student(3, "王五", 22, "网络工程")); // 把数据放入request作用域 request.setAttribute("studentList", studentList); // 转发到JSP页面 request.getRequestDispatcher("/jsp/studentList.jsp").forward(request, response); } }最后是JSP页面,它在webapp/jsp/studentList.jsp:
<%@ page contentType="text/html;charset=UTF-8" language="java" pageEncoding="UTF-8" %> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>学生信息列表</title> </head> <body> <h1>学生信息列表</h1> <table border="1" cellpadding="8" cellspacing="0"> <tr> <th>ID</th> <th>姓名</th> <th>年龄</th> <th>专业</th> </tr> <% List<Student> list = (List<Student>) request.getAttribute("studentList"); if (list != null) { for (Student s : list) { %> <tr> <td><%= s.getId() %></td> <td><%= s.getName() %></td> <td><%= s.getAge() %></td> <td><%= s.getMajor() %></td> </tr> <% } } %> </table> </body> </html>浏览器访问http://localhost:8080/你的应用名/student/list,Tomcat会调用Servlet,Servlet往request里塞数据,然后转发给JSP渲染。这就是最经典的“Servlet取数 + JSP展示”协作流程。
4.2 request.setAttribute和request.getParameter的区别
这个点非常基础,但几乎每年都会看到新手踩坑。很多人一拿到request对象就懵:setAttribute和getParameter到底有什么区别?
简单记法:getParameter是用来接收客户端通过URL或表单提交的数据的,这些数据本是字符串,是“请求参数”;setAttribute是服务端在请求处理过程中自己往request对象里挂数据的,是“请求属性”。
// 接收浏览器传来的参数,比如 ?id=1 或者表单里的name=xxx String id = request.getParameter("id"); // 服务端自己把处理结果挂到request上,以便JSP使用 request.setAttribute("studentList", studentList);JSP页面里,想拿参数用${param.id},想拿属性用${studentList},UML上的数据传递口径完全不同。实际项目里最常见的报错场景就是:Servlet里用setAttribute("studentList", list)存了数据,JSP里却写了request.getParameter("studentList")去取,取出来永远是null。
另外注意,用forward转发到JSP时,request对象是同一个,所以setAttribute设置的数据在JSP里能取到。但如果是response.sendRedirect()重定向,那是浏览器重新发起了一个新的请求,原来的request对象就没了,setAttribute的数据自然也就丢了。这也能解释为什么转发(forward)可以在页面间带数据,而重定向(redirect)带不了。
4.3 用EL表达式和JSTL替换脚本段
上面那个JSP示例里,我们用了<%= %>和<% %>脚本,虽然能跑,但页面里的一大堆Java标签实在谈不上优雅。JavaWeb进入JSP 2.0时代以后,官方推荐的方案是EL表达式 + JSTL标签库。
EL表达式最简单的形式是${},直接在页面里取作用域中的数据。上面的<%= s.getId() %>可以改写成${student.id}。它还有自动处理空值的能力,变量不存在时不报错,直接显示空白,比脚本段落友好太多。
不过EL只能做“取数”和简单运算,遇到循环、判断就得靠JSTL。JSTL里有几个核心标签是JavaWeb项目必备的:
<%@ page contentType="text/html;charset=UTF-8" language="java" pageEncoding="UTF-8" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>学生信息列表</title> </head> <body> <h1>学生信息列表</h1> <table border="1" cellpadding="8" cellspacing="0"> <tr> <th>ID</th> <th>姓名</th> <th>年龄</th> <th>专业</th> </tr> <c:forEach items="${studentList}" var="s"> <tr> <td>${s.id}</td> <td>${s.name}</td> <td>${s.age}</td> <td>${s.major}</td> </tr> </c:forEach> </table> </body> </html>用<c:forEach>遍历列表,用${s.xxx}取JavaBean属性,页面里干干净净。这是现代JavaWeb项目里JSP页面的标准长相。如果你学完JSP直接进入Spring MVC,你会发现Thymeleaf等模板引擎的思路也和这个大同小异——先取数据,再通过模板语法渲染。
5. 实际项目里最常见的JSP坑与排查方法
5.1 中文乱码问题的三条链路
中文乱码可能是JavaWeb初学者遇到最多的“玄学问题”了。实际上乱码不玄,关键在于搞清楚数据经历的三个阶段,每个阶段都有一次编码/解码。
第一段:JSP页面文件的保存编码。页面里的中文是以什么编码保存到磁盘上的。如果IDE里文件编码是GBK,但你pageEncoding写的是UTF-8,页面上的中文就会乱。解决方案:IDE全局文件编码设为UTF-8,JSP头部pageEncoding="UTF-8",两处对齐。
第二段:Tomcat向浏览器输出响应时的编码。由contentType="text/html;charset=UTF-8"控制,响应头里会带上Content-Type: text/html;charset=UTF-8,浏览器按这个编码解析页面。这段设置错了,页面里的中文显示成“锟斤拷”(UTF-8字节被GBK解码)或“���”(解码不了的占位符)。
第三段:浏览器提交表单数据时,Tomcat接收请求参数的编码。这是很多人容易漏的地方。POST提交的表单数据,Tomcat默认按ISO-8859-1解码,一旦有中文,到了Servlet里就是乱码。所以Servlet里需要加一行:
request.setCharacterEncoding("UTF-8");注意这行必须在request.getParameter()之前调用,否则无效。GET请求的URL参数编码处理方式又不一样,需要修改Tomcat的server.xml里的URIEncoding="UTF-8",或者在代码里new String(param.getBytes("ISO-8859-1"), "UTF-8")重新解码,比较脏。
把这三条链路捋顺了,中文乱码基本就能根治。
5.2 JSP编译后的class文件到底保存到哪里了
这个点我在带新人时几乎每次都要解释一遍。很多同学写了一个JSP,改了代码,刷新页面发现还是旧的,就开始怀疑人生。其实只要理解了Tomcat对JSP的处理流程,就明白问题出在哪了。
当你第一次访问JSP时,Tomcat的Jasper引擎会把.jsp翻译成.java源文件,再编译成.class。这些文件默认在Tomcat的work目录下:
apache-tomcat-9.0.x/work/Catalina/localhost/你的应用名/org/apache/jsp/打开这个目录,你能看到类似student_005flist_jsp.java这样的文件(Tomcat会把点号转义),里面就是JSP翻译后的完整Servlet源码。当你修改了JSP文件,Tomcat检测到文件时间戳变了,下次访问时会重新翻译、编译。但如果部署时没把最新的.jsp文件拷过去,或者IDE没有重新部署,那Tomcat用的还是旧的.jsp,自然不生效。
在IDEA里排查这类问题,最快的办法是:Build -> Rebuild Project,然后重新部署到Tomcat。如果还是不行,手动到work目录下把对应jsp的.java和.class文件删掉,让Tomcat强制重新翻译编译。我在项目里就碰到过一次,改了JSP怎么都不生效,最后发现是IDEA的部署配置里忘了勾选“更新资源”选项,热部署只更新了类文件,没更新JSP。这个问题很隐蔽,建议你把这篇内容收藏一下,以后遇到“JSP不生效”可以直接按这个思路排查。
另外,有时候我们需要直接看JSP翻译后的Java代码来排查问题。比如你怀疑某个EL表达式是不是写错了,或者某个标签是不是没有生效,直接去work目录打开对应的_jsp.java文件,搜索相关字符串,一眼就能看出问题所在。这比盲猜高效得多。
5.3 页面路径问题:为什么${pageContext.request.contextPath}成了标配
JavaWeb项目部署到Tomcat后,访问路径一般是http://localhost:8080/应用名/资源路径,这个“应用名”就构成了一个上下文路径(contextPath)。问题在于:这个上下文路径在不同机器、不同环境上是可能变的。你今天部署成/myapp,明天生产环境可能叫/app。
如果你在JSP里写死了相对路径,比如href="/css/style.css",那么当应用路径不是根路径时,浏览器会去http://localhost:8080/css/style.css找资源,找不到,页面样式全丢。这是新手特别容易踩的坑。
解决办法是在页面里用EL拼出带上下文路径的URL:
<link rel="stylesheet" href="${pageContext.request.contextPath}/css/style.css"> <script src="${pageContext.request.contextPath}/js/jquery.min.js"></script> <a href="${pageContext.request.contextPath}/student/list">学生列表</a>${pageContext.request.contextPath}会动态解析成当前应用的上下文路径,这样无论部署在哪个环境,路径都不会错。进入Spring MVC时代后,很多模板引擎也有类似的语法,道理是一样的。
5.4 “屏蔽JSP离开页面提示”其实是浏览器的事,不是JSP的事
很多人写JSP时想做一个效果:用户点了页面上的某个按钮要离开当前页面时,弹出一个确认框,问“确定要离开吗?”。于是去搜“屏蔽jsp离开页面提示”,搜到一堆答案,看到onbeforeunload事件才明白,这根本不是JSP的语法能力。
beforeunload是浏览器提供的一个原生JavaScript事件,在窗口、文档或其资源即将卸载时触发。它和JSP没有直接关系,只是因为写页面时用的是JSP文件,初学的人下意识觉得这是JSP提供的能力。你能在JSP里用onbeforeunload,本质上是因为JSP最终输出的是HTML和JavaScript,并不是JSP本身有这个东西。
这个误区的背后其实是一个很实际的问题:初学者经常分不清“服务端技术”和“浏览器技术”的边界。JSP的所有能力都在服务端,它在请求到达时执行,负责生成HTML字符串。而页面加载完成后的所有交互——点按钮、弹窗、离开提示——都是浏览器里的JavaScript和责任范围。理清这个边界,以后学前端框架时就不会再把onclick、onchange这些属性往服务端技术上靠了。
6. JSP不是银弹:什么场景应该果断放弃它
6.1 前后端分离时代,JSP的处境有些尴尬
现在的开发环境和十几年前完全不同了。越来越多的项目走前后端分离的路子:后端只出JSON接口,前端用Vue、React等框架做SPA(单页应用)。在这种架构下,JSP的存在感确实被削弱了。因为SPA只需要一个静态的HTML入口文件,页面内容全部通过JavaScript动态渲染,JSP的作用范围被压缩得很小。
但JSP远没有到“彻底淘汰”的地步。我在实际项目里遇到过不少情况,JSP依然是合理选择:
- 老系统的维护和改造。很多银行、政务、企业内部系统都是十多年前用JSP写的,还在稳定运行。你不需要把它们全部重写,能读懂JSP、能在上面做增量开发,就是生产力。
- 快速原型和后台管理系统。如果一个系统只有几个人用,要求快速上线、服务端渲染,JSP + Servlet的组合其实非常省事,部署也简单。
- SEO有要求的场景。搜索引擎对动态渲染的SPA页面抓取能力有限,而JSP是服务端直接输出HTML,天然对SEO友好。虽然在现代前端技术里可以搞SSR,但JSP作为最传统的服务端渲染方案,依然在某些业务场景有价值。
所以在学的时候,别抱着“学这个是不是过时了”的心态。技术的价值取决于你用它解决什么问题,而不在于它新不新。
6.2 学完JSP之后的路应该怎么走
如果你现在刚把JSP基础学完,下面的路线可以做参考:
第一步,把JSP和Servlet配合起来用,自己写一个完整的小项目,比如学生信息管理系统:实现登录、列表查询、新增、删除、修改。这个阶段不要用任何框架,就用纯JSP + Servlet + JDBC + MySQL,把整个请求-响应的流转过程彻底跑通。
第二步,引入EL和JSTL,把JSP页面里的Java脚本替换掉,体会一下数据展示层的模板化写法,同时也让页面更干净。
第三步,学一下Filter和Listener,它们在JavaWeb里承担请求过滤和监听的作用,很多框架的底层都基于它们实现。
第四步,再去学Spring MVC甚至Spring Boot。有了前面的基础,你会发现注解、控制器、模板引擎这些新知识都是“老朋友换了个马甲”。没有JSP打底,直接冲框架,很容易浮在表面,遇到问题不知道从何排查。
从我个人的实际体会来说,这些年带过不少新人,凡是跳过JSP直接学Spring Boot的,对“请求怎么进来,数据怎么流转,页面怎么输出”这件事往往是模糊的——当然也能写代码,但一旦偏离框架的默认用法就抓瞎。反倒是沉下心来把JSP和Servlet啃过一遍的人,后面学什么都快。JavaWeb这套体系里,JSP可能不是最时髦的,但它绝对是理解整个服务端渲染和MVC架构最好的入门窗口。