☰
JSP核心原理与实战:从运行机制到老项目维护
2026/10/16 1:04:15 网站建设 项目流程

1. 为什么今天还要聊JSP

先把话说在前头:如果你现在打开任何一个招聘网站,搜“JSP开发”,出来的岗位数量大概率会让你怀疑自己是不是穿越回了十年前。但有意思的是,我最近帮几个朋友处理他们公司遗留系统的问题,发现JSP的身影远比想象中要多——某中型企业的内部OA系统、某政务单位的数据报送平台、某传统制造业的工单管理系统,后台页面清一色还是.jsp后缀。

这就是我想写这篇东西的原因。JSP(JavaServer Pages)不是一个“过时了所以不值得看”的技术,恰恰相反,它是一块活化石,你理解了它,就理解了Java Web开发的底层逻辑是怎么一步步演化到今天的Spring Boot + Thymeleaf这套体系的。而且现实一点说,你接手一个维护了七八年的老项目,里面全是JSP,你总不能跟老板说“这技术太老了我不干”吧。

这篇内容适合三类人看:第一类是在校学生,课程里还在教JSP,但老师讲得云里雾里,你想真正搞明白它到底怎么跑起来的;第二类是刚入行的Java开发者,公司让你维护老系统,你需要快速上手;第三类是对Web开发演进史感兴趣的人,想搞清楚“为什么当年JSP能火,后来又为什么被替代”。

我会从JSP的本质讲起,把它的运行机制、核心语法、实操流程、常见坑点全部拆开揉碎,配上可以直接复现的代码和配置。你不需要事先精通Servlet,但最好对Java基础和HTML有一点了解,这样读起来会更顺畅。

2. JSP到底是什么:从本质到运行机制拆解

2.1 一句话说清JSP的定位

JSP的全称是JavaServer Pages,直译过来就是“Java服务器页面”。它的核心思路非常朴素:让HTML页面里能嵌入Java代码,由服务器动态生成HTML内容返回给浏览器。

你可以把它理解成一个“会变魔术的HTML”。普通的HTML文件,你写什么浏览器就看到什么,内容固定不变。但JSP文件里可以写Java代码,服务器在执行这个文件的时候,会先跑一遍Java逻辑,把结果拼接到HTML里,最后输出一个“成品HTML”给浏览器。浏览器根本不知道背后发生了什么,它收到的就是一个普通的HTML页面。

举个最直观的例子。假设你要显示当前时间,纯HTML做不到(除非用JavaScript在客户端算),但JSP可以:

<%@ page contentType="text/html;charset=UTF-8" %> <html> <body> <h1>当前服务器时间是:<%= new java.util.Date() %></h1> </body> </html>

浏览器打开这个页面,看到的是“当前服务器时间是:Mon Jan 15 14:32:07 CST 2024”这样的内容。每次刷新,时间都会变。这就是JSP最基础的能力——在HTML中动态输出内容。

2.2 JSP和Servlet的关系:一个硬币的两面

很多人学JSP的时候会困惑:不是已经有Servlet了吗,为什么还要搞个JSP出来?

要回答这个问题,得先看看Servlet是怎么写页面的。假设你要用Servlet输出一个简单的HTML表格,代码大概长这样:

public class UserListServlet extends HttpServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html;charset=UTF-8"); PrintWriter out = resp.getWriter(); out.println("<html>"); out.println("<head><title>用户列表</title></head>"); out.println("<body>"); out.println("<table border='1'>"); out.println("<tr><th>ID</th><th>姓名</th></tr>"); List<User> users = userService.findAll(); for (User u : users) { out.println("<tr>"); out.println("<td>" + u.getId() + "</td>"); out.println("<td>" + u.getName() + "</td>"); out.println("</tr>"); } out.println("</table>"); out.println("</body>"); out.println("</html>"); } }

这段代码能跑,但写起来极其痛苦。每写一行HTML就要包一层out.println(),引号嵌套引号,稍微复杂一点的页面就是灾难。前端想改个样式,后端得跟着改Java代码,分工完全乱套。

JSP就是为了解决这个问题而生的。同样的页面,用JSP写:

<%@ page contentType="text/html;charset=UTF-8" %> <%@ page import="java.util.List" %> <html> <head><title>用户列表</title></head> <body> <table border="1"> <tr><th>ID</th><th>姓名</th></tr> <% List<User> users = (List<User>) request.getAttribute("users"); for (User u : users) { %> <tr> <td><%= u.getId() %></td> <td><%= u.getName() %></td> </tr> <% } %> </table> </body> </html>

HTML还是HTML的样子,只在需要动态输出的地方嵌入Java代码。前端工程师能看懂大部分结构,后端工程师专注写逻辑,分工清晰多了。

但这里有一个关键认知需要建立:JSP本质上就是Servlet。服务器在第一次收到对某个JSP页面的请求时,会把这个.jsp文件翻译成一个.java文件(一个Servlet类),然后编译成.class文件,之后就和普通Servlet一样执行了。你写的每一段HTML,最终都会被转换成out.write("...")这样的Java语句。

这个翻译过程是理解JSP一切行为的钥匙。后面讲到的很多“坑”,根源都在这里。

2.3 JSP的生命周期:从文件到响应的完整链路

JSP页面的生命周期分为以下几个阶段,我用一个实际的请求过程来串讲:

阶段一:翻译(Translation)。当你第一次访问index.jsp时,Web容器(比如Tomcat)发现这个JSP还没被翻译过,就会把index.jsp文件解析成一个Java源文件。这个源文件通常放在容器的work目录下,类名一般是index_jsp,继承自HttpJspBase(它又继承自HttpServlet)。

阶段二:编译(Compilation)。翻译出来的.java文件被Java编译器编译成.class文件。如果JSP里有语法错误,这一步就会报错,你会在浏览器上看到一个包含错误详情的页面。

阶段三:初始化(Initialization)。容器加载编译好的Servlet类,创建实例,调用jspInit()方法。这个方法在整个生命周期中只执行一次,你可以在这里做一些初始化工作。

阶段四:执行(Execution)。每次请求到来时,容器调用_jspService()方法处理请求。你写在JSP里的所有代码——脚本片段、表达式、声明——最终都在这个方法里执行。注意,_jspService()方法对应的是Servlet的service()方法,也就是说每个请求都会执行一次。

阶段五:销毁(Destruction)。当Web应用停止或容器关闭时,调用jspDestroy()方法,释放资源。

这里有一个非常重要的推论:JSP中的脚本片段是在多线程环境下执行的。因为Servlet容器通常为每个请求分配一个线程,多个请求可能同时进入同一个JSP的_jspService()方法。如果你在JSP里用了实例变量(在<%! %>声明中定义的变量),就会有线程安全问题。这是新手最容易踩的坑之一,后面会详细展开。

2.4 为什么JSP会被模板引擎替代

理解了JSP的本质,就能理解它为什么逐渐被Thymeleaf、Freemarker、Velocity这些模板引擎替代。

JSP最大的问题是它允许在页面里写任意Java代码。这看起来是优点,实际上是灾难的开始。项目一大,JSP页面里就会出现数据库查询、业务逻辑判断、甚至文件操作。前端改一个按钮的颜色,打开JSP文件一看,里面三百行Java代码,根本不敢动。

模板引擎的思路是:页面就是页面,只负责展示,逻辑归后端。Thymeleaf里你只能写表达式和有限的逻辑标签,想写Java代码?不好意思,不支持。这种“限制”反而让项目结构更健康。

另外,JSP的翻译和编译过程虽然只发生一次,但在开发阶段每次修改JSP都需要重新翻译编译,热部署体验不如现代模板引擎。加上Spring Boot默认使用嵌入式Tomcat,对JSP的支持需要额外配置(打war包或指定特定目录),进一步加速了JSP的退场。

但话说回来,理解JSP对于理解MVC架构、请求响应模型、Session管理这些Web开发核心概念仍然非常有价值。很多现代框架的设计思想,都能在JSP+Servlet的组合里找到影子。

3. JSP核心语法与实操要点全解析

3.1 JSP的四种基本语法元素

JSP页面里可以混写HTML和Java,但Java代码不能随便写,必须放在特定的语法元素里。JSP定义了四种基本的语法元素,每种有不同的用途和执行时机。

第一种:脚本片段(Scriptlet)——<% %>

这是最常用的元素,里面可以写任意Java代码。代码会在_jspService()方法中按顺序执行。

<% String name = request.getParameter("name"); if (name == null || name.isEmpty()) { name = "访客"; } int count = (Integer) application.getAttribute("visitCount"); %>

脚本片段里定义的变量是局部变量,只在当前请求的处理过程中有效。多个脚本片段之间的变量可以互相访问,因为它们最终都在同一个方法体内。

第二种:表达式(Expression)——<%= %>

表达式用于向页面输出一个值。注意,里面不能写分号,因为它的本质是out.print(...)的参数。

<p>欢迎你,<%= name %></p> <p>当前访问次数:<%= count %></p>

表达式等价于out.print(name)。如果表达式的值是null,会输出字符串"null",而不是空字符串,这一点要注意。

第三种:声明(Declaration)——<%! %>

声明用于定义类的成员变量或方法。它会被翻译到Servlet类的方法外面,成为类的成员。

<%! private int globalCount = 0; public String formatName(String name) { return name.substring(0, 1).toUpperCase() + name.substring(1); } %>

注意:声明中定义的变量是实例变量,所有请求共享同一个实例,存在线程安全问题。除非你非常清楚自己在做什么,否则不要用声明定义可变状态。

第四种:指令(Directive)——<%@ %>

指令用于向容器传递一些配置信息,最常见的三种是page、include和taglib。

<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ page import="java.util.List, com.example.model.User" %> <%@ include file="/common/header.jsp" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>

page指令配置当前页面的属性,include指令在翻译阶段把另一个文件的内容包含进来(静态包含),taglib指令引入标签库。

3.2 九大内置对象:不用声明就能用的魔法变量

JSP最方便的地方在于它提供了九个内置对象,你在脚本片段和表达式中可以直接使用,不需要声明。这些对象是容器在调用_jspService()方法时自动传入或创建的。

对象名类型作用域说明
requestHttpServletRequest一次请求封装客户端请求信息
responseHttpServletResponse一次请求封装服务器响应
sessionHttpSession一次会话跟踪用户状态
applicationServletContext整个应用全局共享数据
outJspWriter当前页面向客户端输出内容
pageObject当前页面当前Servlet实例的引用
pageContextPageContext当前页面页面上下文,可访问其他作用域
configServletConfig当前页面Servlet配置信息
exceptionThrowable当前页面异常对象,仅在错误页面可用

这里面最常用的是前四个。request用来获取表单参数、请求头、Cookie;session用来存登录状态、购物车;application用来存全局配置、访问计数;out用来输出内容。

pageContext比较特殊,它提供了一种统一的方式来访问其他三个作用域(request、session、application)的属性:

<% pageContext.setAttribute("msg", "hello", PageContext.REQUEST_SCOPE); pageContext.setAttribute("msg", "world", PageContext.SESSION_SCOPE); String msg1 = (String) pageContext.getAttribute("msg", PageContext.REQUEST_SCOPE); String msg2 = (String) pageContext.getAttribute("msg", PageContext.SESSION_SCOPE); %>

3.3 四大作用域:数据存哪里,活多久

JSP的四大作用域是面试高频考点,也是实际开发中必须搞清楚的概念。它们决定了数据能存活多久、能被哪些页面访问。

page作用域:只在当前页面有效。通过pageContext.setAttribute()设置的数据,在当前页面的脚本片段之间可以访问,但转发或重定向后就没了。实际开发中很少用。

request作用域:一次请求内有效。从客户端发起请求到服务器返回响应,这期间经过的所有页面(包括转发到的页面)都能访问。通过request.setAttribute()设置,通过request.getAttribute()获取。这是MVC模式下Controller向View传数据的主要方式。

session作用域:一次会话内有效。从用户打开浏览器访问网站,到关闭浏览器(或Session超时),这期间的所有请求都能访问同一个session对象。用来存登录用户信息、购物车数据最合适。

application作用域:整个Web应用内有效。从应用启动到应用停止,所有用户、所有请求共享同一个application对象。用来存全局配置、在线人数统计等。

<% // request作用域:转发后还能用 request.setAttribute("username", "张三"); // session作用域:整个会话期间可用 session.setAttribute("loginUser", user); // application作用域:所有用户共享 Integer onlineCount = (Integer) application.getAttribute("onlineCount"); if (onlineCount == null) { onlineCount = 0; } application.setAttribute("onlineCount", onlineCount + 1); %>

实操心得:session虽然方便,但不要什么都往里面塞。session数据存在服务器内存中,用户多了会占用大量内存。而且集群环境下session共享是个麻烦事,需要额外配置。能用request解决的就不要用session。

3.4 include的两种方式:静态包含与动态包含

JSP支持在一个页面中包含另一个页面的内容,有两种方式,区别很大。

静态包含(<%@ include file="..." %>):在翻译阶段把目标文件的内容直接插入到当前JSP中,合并成一个Java文件再编译。这意味着两个页面共享同一个request、response、out对象,变量名不能冲突。

<%@ include file="/common/header.jsp" %>

动态包含(<jsp:include page="..." />):在运行阶段单独执行目标页面,把执行结果包含进来。两个页面各自翻译成独立的Servlet,互不干扰。

<jsp:include page="/common/header.jsp" />

什么时候用哪个?如果被包含的页面是纯静态内容(比如页头、页脚),用静态包含效率更高,因为只编译一次。如果被包含的页面需要独立处理逻辑,或者可能被多个页面以不同方式包含,用动态包含更灵活。

踩过的坑:静态包含时,如果两个页面都定义了同名变量,编译会报错。我曾经在一个页面里静态包含了两个子页面,结果两个子页面都定义了String title变量,直接编译失败。改成动态包含就好了。

4. 从零搭建一个JSP项目:完整实操流程

4.1 环境准备与项目结构

要跑JSP,你需要一个支持JSP的Web容器。最常用的是Apache Tomcat,版本选择上,Tomcat 9对应Servlet 4.0和JSP 2.3,Tomcat 10开始把包名从javax.*改成了jakarta.*,很多老代码会不兼容。如果你维护的是老项目,建议用Tomcat 8或9。

我以Tomcat 9 + JDK 8 + Maven的项目结构来演示。一个标准的JSP Web项目目录结构是这样的:

my-jsp-project/ ├── pom.xml └── src/ └── main/ ├── java/ │ └── com/example/ │ └── UserServlet.java └── webapp/ ├── WEB-INF/ │ ├── web.xml │ └── lib/ ├── common/ │ └── header.jsp ├── index.jsp └── user/ └── list.jsp

关键点:WEB-INF目录下的文件不能被浏览器直接访问,所以把web.xml、类库、以及不希望被直接访问的JSP放在这里。webapp下的其他文件可以通过URL直接访问。

Maven依赖只需要一个:

<dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency>

provided表示这个依赖由容器提供,打包时不需要打进去。

4.2 第一个JSP页面:从请求到响应的完整链路

创建一个index.jsp:

<%@ page contentType="text/html;charset=UTF-8" language="java" %> <!DOCTYPE html> <html> <head> <title>我的第一个JSP页面</title> </head> <body> <h1>Hello, JSP!</h1> <p>服务器时间:<%= new java.util.Date() %></p> <p>你的IP地址:<%= request.getRemoteAddr() %></p> <p>请求方式:<%= request.getMethod() %></p> </body> </html>

把项目部署到Tomcat,访问http://localhost:8080/my-jsp-project/index.jsp,你就能看到页面了。

现在我们来验证一下“JSP就是Servlet”这个结论。去Tomcat的work目录下找找,路径大概是work/Catalina/localhost/my-jsp-project/org/apache/jsp/,你会看到一个index_jsp.java文件。打开它,你会看到类似这样的内容:

public final class index_jsp extends org.apache.jasper.runtime.HttpJspBase { public void _jspService(HttpServletRequest request, HttpServletResponse response) { JspWriter out = response.getWriter(); out.write("<html>\n"); out.write("<head>\n"); out.write("<title>我的第一个JSP页面</title>\n"); out.write("</head>\n"); out.write("<body>\n"); out.write("<h1>Hello, JSP!</h1>\n"); out.write("<p>服务器时间:"); out.print(new java.util.Date()); out.write("</p>\n"); // ... 省略 } }

你写的HTML变成了out.write(),你写的<%= %>变成了out.print()。一切都说通了。

4.3 表单处理与请求参数获取

JSP处理表单是最常见的场景。假设有一个登录页面login.jsp:

<%@ page contentType="text/html;charset=UTF-8" %> <!DOCTYPE html> <html> <head><title>登录</title></head> <body> <form action="doLogin.jsp" method="post"> 用户名:<input type="text" name="username"><br> 密码:<input type="password" name="password"><br> <input type="submit" value="登录"> </form> </body> </html>

处理登录的doLogin.jsp:

<%@ page contentType="text/html;charset=UTF-8" %> <% request.setCharacterEncoding("UTF-8"); String username = request.getParameter("username"); String password = request.getParameter("password"); if ("admin".equals(username) && "123456".equals(password)) { session.setAttribute("loginUser", username); response.sendRedirect("welcome.jsp"); } else { request.setAttribute("errorMsg", "用户名或密码错误"); request.getRequestDispatcher("login.jsp").forward(request, response); } %>

这里有几个关键点。第一,request.setCharacterEncoding("UTF-8")必须放在获取任何参数之前,否则中文会乱码。第二,登录成功后用sendRedirect重定向,防止刷新页面重复提交表单。第三,登录失败用forward转发,这样可以在目标页面通过request作用域拿到错误信息。

实操心得:POST请求的中文乱码可以通过setCharacterEncoding解决,但GET请求的参数在URL里,setCharacterEncoding对GET无效。Tomcat 8以后默认URI编码是UTF-8,所以GET请求一般不会乱码。如果你用的是更老的Tomcat,需要在server.xml的Connector标签上加URIEncoding="UTF-8"。

4.4 用JSTL和EL表达式告别脚本片段

写多了<% %>你会发现页面越来越乱。JSP 2.0引入了EL表达式(Expression Language)和JSTL标签库,让页面可以完全不写Java代码。

EL表达式的基本语法是${},可以直接访问作用域中的属性:

<p>欢迎你,${sessionScope.loginUser}</p> <p>用户名:${requestScope.user.name}</p> <p>年龄:${user.age + 1}</p>

EL会自动从page、request、session、application四个作用域中按顺序查找属性,找到就返回。你也可以指定作用域前缀,比如${sessionScope.loginUser}。

JSTL提供了常用的标签,最常用的是c:forEach和c:if:

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <c:if test="${not empty errorMsg}"> <p style="color:red">${errorMsg}</p> </c:if> <table> <tr><th>ID</th><th>姓名</th><th>邮箱</th></tr> <c:forEach items="${userList}" var="user" varStatus="status"> <tr> <td>${user.id}</td> <td>${user.name}</td> <td>${user.email}</td> </tr> </c:forEach> </table>

用JSTL需要引入两个jar包:jstl.jar和standard.jar(JSTL 1.2只需要jstl-1.2.jar)。Maven依赖:

<dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency>

踩过的坑:EL表达式在JSP 2.0之前默认是不开启的,需要在page指令里加isELIgnored="false"。不过现在用的Tomcat 7以上都默认开启了,一般不用管。另外,EL表达式不能直接调用Java方法(除非用EL函数),这是它和脚本片段的重要区别。

5. 常见问题与排查技巧实录

5.1 中文乱码问题全解

中文乱码是JSP开发中最常见的问题,没有之一。乱码的本质是编码和解码用了不同的字符集。我整理了一个速查表:

现象原因解决方案
页面显示乱码JSP文件保存编码与page指令声明不一致统一用UTF-8保存文件,page指令加contentType="text/html;charset=UTF-8"
POST参数乱码请求体编码不是UTF-8获取参数前调用request.setCharacterEncoding("UTF-8")
GET参数乱码URL编码不是UTF-8Tomcat 8+默认UTF-8;老版本在Connector加URIEncoding="UTF-8"
响应乱码响应头Content-Type没指定charsetresponse.setContentType("text/html;charset=UTF-8")
数据库乱码数据库连接URL没指定编码JDBC URL加?useUnicode=true&characterEncoding=UTF-8

我个人的习惯是:JSP文件第一行永远写<%@ page contentType="text/html;charset=UTF-8" %>,Servlet里第一行永远写request.setCharacterEncoding("UTF-8")和response.setContentType("text/html;charset=UTF-8")。养成肌肉记忆,能省掉大量排查时间。

5.2 404和500错误的排查思路

404 Not Found:服务器找不到你请求的资源。排查顺序:第一,检查URL路径是否正确,注意大小写;第二,检查JSP文件是否在正确的目录下,WEB-INF下的文件不能直接访问;第三,检查web.xml中的servlet映射是否正确;第四,检查项目是否成功部署,Tomcat的webapps目录下有没有你的项目文件夹。

500 Internal Server Error:服务器内部错误。这个错误通常会在页面上显示Java异常堆栈,仔细看第一行和Caused by部分。常见原因:JSP语法错误(少了个%>)、Java代码抛异常(空指针最常见)、类找不到(jar包没引入)。

排查技巧:Tomcat的logs目录下有日志文件,localhost.YYYY-MM-DD.log记录了详细的错误信息。如果页面上看不到完整堆栈,去日志里找。另外,把work目录下翻译出来的.java文件打开看看,很多时候问题一目了然。

5.3 线程安全问题:JSP里最容易忽视的坑

前面提过,JSP的_jspService()方法是多线程执行的。如果你在<%! %>声明里定义了实例变量,多个请求会同时读写这个变量,导致数据错乱。

<%! private int count = 0; %> <!-- 危险! --> <% count++; // 多线程下结果不可预测 out.println("访问次数:" + count); %>

正确的做法是:需要计数就用application作用域配合synchronized,或者用AtomicInteger:

<%! private AtomicInteger count = new AtomicInteger(0); %> <% int current = count.incrementAndGet(); out.println("访问次数:" + current); %>

但更好的做法是:不要在JSP里定义任何实例变量。所有变量都定义在脚本片段里(局部变量),或者放在request/session/application作用域中。这样天然就是线程安全的。

5.4 JSP页面修改后不生效怎么办

有时候你改了JSP文件,刷新浏览器却发现还是旧内容。原因可能是:第一,浏览器缓存了页面,按Ctrl+F5强制刷新;第二,Tomcat没有重新翻译JSP,检查work目录下的文件时间戳;第三,如果你在IDE里改了文件但没有重新部署,需要重新发布项目。

Tomcat默认会检查JSP文件的时间戳,如果发现文件更新了会自动重新翻译。但这个检查有延迟(默认是development模式下每秒检查一次)。如果实在不生效,可以删除work目录下的对应文件夹,强制重新翻译。

实操心得:开发阶段建议在web.xml里配置<jsp-config>的development模式为true,这样修改JSP后能更快生效。生产环境则应该关闭development模式,避免频繁检查影响性能。

6. 从JSP到现代Web框架的演进思考

6.1 JSP+Servlet+JavaBean的MVC雏形

在Spring MVC普及之前,Java Web开发的主流模式是JSP+Servlet+JavaBean,也就是所谓的Model 1和Model 2。

Model 1模式下,JSP页面既负责展示又负责处理逻辑,一个页面里既有数据库查询又有HTML输出。小项目还行,大项目就是灾难。

Model 2模式引入了Servlet作为控制器:Servlet接收请求、调用JavaBean处理业务、把结果存到request作用域、转发到JSP页面展示。这就是MVC的雏形。

// UserServlet.java - 控制器 public class UserServlet extends HttpServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { List<User> users = userService.findAll(); // Model req.setAttribute("userList", users); req.getRequestDispatcher("/user/list.jsp").forward(req, resp); // View } }
<!-- list.jsp - 视图 --> <c:forEach items="${userList}" var="user"> <tr><td>${user.id}</td><td>${user.name}</td></tr> </c:forEach>

这个模式已经很像后来的Spring MVC了。理解了这个,再学Spring MVC的@Controller、ModelAndView、ViewResolver,会发现概念都是相通的。

6.2 前后端分离对JSP的冲击

JSP的衰落和前后端分离的兴起几乎是同步的。前后端分离的核心思想是:后端只提供数据接口(通常是JSON格式),前端用Vue、React等框架渲染页面。JSP这种“后端渲染HTML”的模式就显得笨重了。

在前后端分离架构下,后端返回的是这样的数据:

{ "code": 200, "data": [ {"id": 1, "name": "张三", "email": "zhangsan@example.com"}, {"id": 2, "name": "李四", "email": "lisi@example.com"} ] }

前端拿到数据后用JavaScript渲染页面。后端不再关心HTML长什么样,前端不再依赖后端的模板。分工更清晰,开发效率更高。

但这不意味着JSP就完全没有价值了。在一些内部管理系统、后台报表页面中,JSP的开发效率反而比前后端分离更高——不需要单独起前端项目,不需要定义接口文档,一个JSP页面搞定所有事情。所以很多公司的内部系统至今还在用JSP。

6.3 老JSP项目维护的实用建议

如果你接手了一个JSP老项目,以下是我总结的几条实用建议:

第一,先搞清楚项目的技术栈版本。Servlet是2.5还是3.0?JSP是2.0还是2.3?Tomcat是7还是8?这些决定了你能用什么特性,不能用什么特性。

第二,不要试图重写。老项目能跑就不要动,重写的风险远大于收益。在现有基础上做增量修改,逐步替换。

第三,把业务逻辑从JSP中抽离。如果JSP里有数据库操作,先把它移到Servlet或Service层。JSP只负责展示。

第四,引入JSTL和EL。如果项目还在用<% %>写循环和判断,逐步替换成<c:forEach>和<c:if>。页面会清爽很多。

第五,注意安全。老项目最常见的安全问题是SQL注入和XSS。JSP中输出用户输入的内容时,用<c:out>标签或fn:escapeXml()函数转义,防止XSS攻击。

<!-- 不安全 --> <p>${userInput}</p> <!-- 安全 --> <p><c:out value="${userInput}" /></p>

最后分享一个小技巧:如果你需要在JSP页面中调试,可以在脚本片段里用out.println()输出变量值,或者用System.out.println()输出到Tomcat控制台。虽然原始,但在没有调试器的环境下非常管用。另外,<%= request.getAttribute("javax.servlet.error.message") %>可以在错误页面显示详细的错误信息,排查问题时很有帮助。

这个内容后续还可以这样扩展:如果你想深入了解JSP的底层实现,可以去读Tomcat的Jasper模块源码,看看JSP是怎么被解析成Java代码的。如果你对现代模板引擎感兴趣,可以对比一下Thymeleaf和JSP在Spring Boot中的集成方式,理解为什么Spring Boot官方推荐Thymeleaf而不是JSP。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询