1. JSP概述:从历史到现实的再认识

如果你在最近几年才开始接触Java Web开发,可能会觉得JSP(JavaServer Pages)是一个有些“古老”甚至“过时”的技术。毕竟,现在的主流是Spring Boot、Vue.js、React这些前后端分离的框架。但有趣的是,在搜索引擎和各大技术社区里,关于JSP的搜索热度依然不低,从“JSP中Ajax方法带参数传递”到“VSCode搭建JSP环境”,再到各种关于Tomcat配置、Session共享的疑难杂症,都说明它依然活跃在许多遗留系统、企业内部应用甚至教学场景中。我见过不少维护着十几年历史系统的工程师,每天还在和JSP页面打交道。所以,理解JSP,不仅仅是理解一段历史,更是掌握一把能打开许多现有系统大门的钥匙。它就像Java Web世界的“拉丁文”,虽然不再是官方推荐的新项目首选,但读懂它,你就能理解Servlet、MVC模式乃至后来诸多框架的设计思想源头。

简单来说,JSP是一种用于创建动态Web内容的技术。它的核心目标是把Java代码嵌入到HTML页面中,让开发者能相对方便地生成动态网页,而不必像纯Servlet那样,用一大堆 out.println(“<html>”) 来拼接HTML字符串。你可以把它想象成一个“模板”,这个模板最终会被服务器转换并执行为一个Servlet。对于初学者而言,JSP降低了动态网页开发的门槛;对于有经验的开发者,理解其运行原理则是解决性能问题、理解Web容器行为的关键。

2. JSP运行原理深度拆解:从.jsp文件到客户端响应

很多人知道JSP最终会变成Servlet,但这个“变”的过程具体是怎样的?里面有哪些关键的步骤和容易踩坑的细节?我们把它拆开来看。

2.1 核心转换过程:翻译与编译

当你把一个 .jsp 文件(例如 index.jsp )部署到Tomcat这类Web容器(也叫Servlet容器)后,第一次有客户端请求这个页面时,会发生一系列幕后操作。这个过程不是魔法,而是有严格规范的。

第一步:翻译(Translation) Web容器(如Tomcat)中的JSP引擎(通常是Jasper)会识别到这个对 .jsp 资源的请求。引擎不会直接执行 .jsp 文件,而是首先将其“翻译”成一个纯Java源代码文件,即一个 .java 文件。这个文件本质上就是一个Servlet类。例如,你的 index.jsp 可能会被翻译成 index_jsp.java ,并通常存放在Tomcat的工作目录(如 work/Catalina/localhost/yourApp/org/apache/jsp )下。这个翻译过程,就是把所有HTML文本转换成 out.write() 语句,把JSP脚本片段( <% ... %> )、表达式( <%= ... %> )、声明( <%! ... %> )等,按照JSP规范转换成对应的Java代码。

注意 :很多初学者遇到的“ClassNotFoundException”或“JSP文件无法访问”错误,根源往往在这里。检查Tomcat的 work 目录权限、磁盘空间是否充足,是排查这类问题的第一步。我遇到过因为磁盘满导致JSP无法生成 .java 文件,从而一直报500错误的情况。

第二步:编译(Compilation) 翻译生成的 .java 文件只是一个文本文件,需要被编译成JVM可以执行的字节码。JSP引擎会调用JDK(或内置编译器)将这个 .java 文件编译成 .class 文件。至此,一个标准的Servlet类就诞生了。这个类继承自 HttpJspBase (Tomcat中的实现,它本身继承自 HttpServlet ),并重写了 _jspService 方法。你写的所有JSP内容,最终都位于这个 _jspService 方法体内。

第三步:加载与实例化(Loading & Instantiation) 编译好的 .class 文件会被Web容器的类加载器加载到内存中,然后像普通Servlet一样,容器会创建它的一个实例。

第四步:初始化(Initialization) 容器调用该Servlet实例的 init() 方法进行初始化。对于JSP转换来的Servlet,这通常只发生一次。

第五步:请求处理(Request Handling) 当后续请求到达时,容器不再需要重复翻译和编译,而是直接调用已实例化的Servlet的 _jspService 方法。该方法接收 HttpServletRequest HttpServletResponse 对象,执行其中包含了你原始逻辑的Java代码,生成HTML输出,并通过响应流发送给客户端浏览器。

这个过程解释了为什么第一次访问JSP页面通常比较慢(需要经历翻译和编译),而后续访问就很快(直接执行已编译的类)。也解释了当你修改了JSP文件后,为什么有时需要重启应用或等待容器检测到文件变化并重新翻译编译。

2.2 内置对象揭秘:为什么可以直接用 request out

在JSP页面的脚本片段里,你可以直接使用 request response session out application 等对象,而不需要自己声明或获取。这常常让初学者感到疑惑。其实,这些就是JSP的“内置对象”(Implicit Objects)。

_jspService 方法被生成时,JSP引擎会自动声明并初始化这些对象。我们来看一下它们在生成的Servlet类中大概是什么样子:

public void _jspService(HttpServletRequest request, HttpServletResponse response) throws java.io.IOException, ServletException {
    // 内置对象实际上是方法参数或局部变量
    PageContext pageContext = ...;
    HttpSession session = request.getSession();
    ServletContext application = getServletContext();
    ServletConfig config = getServletConfig();
    JspWriter out = pageContext.getOut();
    // ... 你的JSP内容被转换成使用这些变量的代码
    out.write("<html>\\r\\n");
    out.write("<body>\\r\\n");
    out.write("Hello, ");
    out.print( request.getParameter("name") ); // 对应 <%= request.getParameter("name") %>
    out.write("\\r\\n");
    out.write("</body>\\r\\n");
    out.write("</html>");
}

所以, request 就是 _jspService 方法的参数, session 是通过 request.getSession() 获得的, out JspWriter 实例,它包装了 response.getWriter() 。理解这一点,你就明白了这些对象的生命周期和作用域与它们在Servlet中是完全一致的。例如, session 对象在会话期间有效, application (即 ServletContext )在整个Web应用生命周期有效。

实操心得 :虽然可以直接使用内置对象,但在复杂的JSP页面中,过度混用Java脚本和HTML会导致代码难以维护(这就是所谓的“JSP Model 1”的弊端)。更佳实践是使用JSTL标签和EL表达式来替代大多数脚本片段,让页面更专注于视图展示。例如,用 <c:forEach> 替代Java循环,用 ${user.name} 替代 <%= user.getName() %>

3. JSP与Servlet、Tomcat的三角关系

很多人分不清JSP和Servlet,也搞不懂Tomcat在这里面扮演什么角色。我们可以把它们看作一个协作体系。

Servlet :是Java Web应用的基石,是一个用于处理HTTP请求和响应的Java类。它遵循“一个类,一个URL模式(或一组)”的映射关系,所有的逻辑(业务逻辑、控制逻辑、展示逻辑)最初都挤在Servlet里。

JSP :本质上是Servlet的“语法糖”或“特殊形式”,主要为了解决Servlet在生成动态HTML时过于繁琐的问题。JSP允许你以更接近HTML的方式编写页面,但最终它还是会变成一个Servlet。所以, JSP是一种特殊的Servlet,专为简化视图层开发而生

Tomcat :它是一个Web容器(Servlet Container),也是JSP容器。它的核心职责是:

  1. 管理Servlet/JSP的生命周期(加载、初始化、执行、销毁)。
  2. 提供网络服务,监听端口(如8080),解析HTTP协议。
  3. 提供JSP引擎(如Jasper),负责JSP到Servlet的翻译和编译工作。
  4. 管理会话(Session)、上下文(Context)等。

所以,关系链是这样的:你编写 .jsp 文件 -> 部署到 Tomcat -> Tomcat在需要时将其翻译编译成 Servlet 类 -> Tomcat加载并执行这个Servlet类来处理请求。

常见配置问题实录

  • “tongweb jsp is missing from the classpath” :这通常出现在一些国产或特定应用服务器(如TongWeb)中,错误提示JSP依赖的库不在类路径下。解决方案是确保Web应用的 WEB-INF/lib 目录下或服务器的共享库中包含 jsp-api.jar servlet-api.jar (或对应版本的依赖)。在Maven项目中,需要检查 provided 范围的依赖是否正确配置。
  • “两个Tomcat部署两个相同的项目,在浏览器中登录一个时,另一个项目的session过期” :这涉及到Session管理。默认情况下,Session是存储在单个Tomcat实例内存中的。两个独立的Tomcat进程内存不共享。如果你需要Session共享,必须借助外部存储,如Redis,并配置Tomcat的Session管理器(如使用 RedissonSessionManager )。这不是JSP的问题,而是分布式环境下的Session一致性问题。
  • 版本匹配问题 :如“jdk和tomcat的版本有要求吗?”答案是肯定的。高版本Tomcat通常需要匹配的JDK版本。例如,Tomcat 10.x需要JDK 11或更高版本;Tomcat 9.x支持JDK 8及以上。不匹配可能导致无法启动或运行时错误。

4. JSP核心语法元素与最佳实践解析

虽然现在不鼓励在JSP中写大量Java代码,但了解其基本语法对于阅读和维护旧代码至关重要。更重要的是,理解如何正确使用它们。

4.1 脚本元素:谨慎使用的“利器”

  1. 脚本片段(Scriptlet) <% Java代码 %> 这是最强大的也是最容易被滥用的部分。里面的代码会原样插入到 _jspService 方法中。它适合执行一些简单的、与视图渲染紧密相关的逻辑,但绝不适合处理复杂业务。

    重要禁忌 :避免在脚本片段中编写数据库访问、复杂的业务计算。这会导致JSP页面职责过重,难以测试和维护。我见过一个JSP文件里有长达数百行的SQL查询和业务逻辑,那简直是维护的噩梦。

  2. 表达式(Expression) <%= Java表达式 %> 用于输出一个Java表达式的值到HTML中。表达式会被计算,结果转换为字符串,然后输出。它相当于 out.print(表达式) 。注意,表达式末尾 没有分号

  3. 声明(Declaration) <%! 变量或方法声明 %> 这里声明的变量或方法,会成为生成的Servlet类的成员变量或成员方法。这意味着它们不在 _jspService 方法内。

    实操心得 :声明成员变量需要非常小心,因为它不是线程安全的!多个请求线程会共享这个成员变量,可能导致数据错乱。除非你非常清楚自己在做什么(比如定义一个常量或同步方法),否则应尽量避免使用声明。

4.2 指令(Directive):控制JSP页面行为

指令为容器提供整个页面的设置信息,不会产生任何输出。

  1. page指令 <%@ page attribute="value" %> 这是最重要的指令。常用属性包括:

    • contentType :设置响应MIME类型和字符编码,如 <%@ page contentType="text/html;charset=UTF-8" %> 。解决中文乱码问题首先检查这里和 pageEncoding
    • pageEncoding :指定JSP文件自身保存时使用的编码,与 contentType charset 保持一致是良好实践。
    • import :导入Java类,多个类用逗号分隔。如 <%@ page import="java.util.List, com.example.User" %>
    • session :默认为 true ,表示页面参与HTTP会话。如果设为 false ,则该页面无法使用 session 内置对象。
    • errorPage / isErrorPage :用于配置错误处理页面。
  2. include指令 <%@ include file="header.jsp" %> 这是静态包含。在 翻译阶段 ,被包含文件的内容会被原封不动地插入到当前JSP中,然后一起被翻译成一个Servlet。适用于包含不会变化的公共片段(如页头、页脚、静态菜单)。

    注意 :因为是在翻译期合并,所以被包含文件中定义的变量在当前文件中可以直接使用。修改被包含文件后,需要等待容器重新翻译包含它的所有JSP页面。

  3. taglib指令 <%@ taglib uri="..." prefix="c" %> 用于引入标签库,如JSTL(JSP Standard Tag Library)。这是现代JSP开发中减少脚本片段的关键,通过类似XML的标签来完成循环、条件判断、格式化等操作。

4.3 动作(Action):动态行为标签

动作是在 请求处理阶段 执行的,以XML标签形式书写。

  1. <jsp:include> :动态包含。它会在当前页面执行时,将另一个资源(JSP或Servlet)的 输出结果 包含进来。被包含的资源独立编译和执行。

    • 与静态包含的区别 <%@ include %> 是源代码级别的合并, <jsp:include> 是运行时输出的合并。后者更灵活,开销稍大,且被包含页面的变量不共享。
  2. <jsp:useBean> <jsp:setProperty> <jsp:getProperty> :用于操作JavaBean。在早期Model 1架构中常用,现在基本被EL表达式和MVC框架取代。

  3. <jsp:forward> :将当前请求转发给另一个资源(JSP或Servlet)处理。类似于Servlet中的 RequestDispatcher.forward() 。转发后,由目标资源生成响应,地址栏URL不变。

4.4 EL表达式与JSTL:告别脚本的现代方式

这是让JSP保持生命力的关键扩展。

EL(Expression Language) ${expression} 提供了一种简洁、强大的方式来访问作用域(page, request, session, application)中的属性、JavaBean属性、集合元素等。它自动处理null值,比 <%= %> 更安全、更优雅。 例如: ${user.name} 等价于 <%= user.getName() != null ? user.getName() : "" %>

JSTL(JSP Standard Tag Library) : 通过 taglib 指令引入后,可以使用一系列标准标签。

  • 核心标签库(Core) :前缀通常为 c 。包含 <c:if> , <c:forEach> , <c:choose> , <c:when> , <c:otherwise> , <c:set> , <c:out> 等。 <c:out> 默认会对输出进行XML转义,防止XSS攻击,比直接使用 ${} <%= %> 更安全。
  • 格式化标签库(Fmt) :用于国际化消息、格式化日期和数字。
  • 函数标签库(Functions) :提供一些字符串处理函数,如 fn:contains , fn:substring 等。

最佳实践建议 :在新的或需要维护的JSP页面中, 彻底放弃脚本片段(Scriptlet) 。所有数据展示用EL表达式,所有逻辑控制用JSTL标签。如果页面逻辑依然复杂,那说明这部分逻辑应该移到后端的Servlet或Controller中,JSP只负责接收模型数据并渲染视图。这才是健康的MVC模式。

5. 开发环境搭建与常见问题实战排查

即便只是维护老项目,一个顺手的开发环境也能极大提升效率。这里以最通用的 IntelliJ IDEA + Tomcat 组合为例,讲解关键配置和避坑点。

5.1 环境搭建要点

  1. 项目创建与模块设置

    • 在IDEA中创建Java Enterprise项目,选择 Web Application 模板。
    • 确保模块的 Facets 中包含了 Web ,并且 Web Resource Directories 指向了正确的目录(通常是 src/main/webapp web )。这是IDEA识别JSP和应用结构的基础。
  2. Tomcat服务器配置

    • Run -> Edit Configurations ,添加 Tomcat Server -> Local
    • Deployment 选项卡,通过 + -> Artifact 添加你的Web应用构件。这里的关键是 Application context ,它决定了你的应用根路径(如 /myapp )。
    • Server 选项卡,可以配置端口、启动超时时间等。如果遇到“Application Server was not connected before run configuration stop”这类错误,通常是因为Tomcat实例没有成功启动,检查JDK版本兼容性和端口占用。
  3. 依赖管理

    • 如果使用Maven,在 pom.xml 中,JSP和Servlet相关的API依赖作用域应设为 provided ,因为它们由Tomcat等容器提供。
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
        <version>4.0.1</version> <!-- 版本需与Tomcat匹配 -->
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>javax.servlet.jsp</groupId>
        <artifactId>javax.servlet.jsp-api</artifactId>
        <version>2.3.3</version>
        <scope>provided</scope>
    </dependency>
    <!-- JSTL依赖,作用域通常是compile -->
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>jstl</artifactId>
        <version>1.2</version>
    </dependency>
    

5.2 高频问题排查实录

根据提供的热词,我整理了几个最常见的问题场景和解决思路:

问题现象 可能原因 排查步骤与解决方案
Tomcat启动成功,但访问JSP报404 1. JSP文件未放在Web应用的正确目录下(应在 webapp 或其子目录)。
2. web.xml 中配置了错误的欢迎页或Servlet映射覆盖了JSP。
3. 应用上下文路径(Context Path)配置错误。
1. 检查JSP文件物理位置。
2. 检查 web.xml ,确保没有 *.jsp 的Servlet映射拦截了请求。
3. 在IDEA的Run Configuration或Tomcat的 server.xml / context.xml 中检查应用上下文路径。
JSP页面中文乱码 1. JSP文件自身保存编码与 pageEncoding 指令不符。
2. page 指令的 contentType 未设置或字符集错误。
3. Tomcat服务器连接器(Connector)未配置URI编码。
4. 数据库连接字符集不匹配。
1. 确保JSP文件以UTF-8保存,并添加 <%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8"%>
2. 在Tomcat的 server.xml <Connector> 标签中增加 URIEncoding="UTF-8"
3. 检查获取请求参数时是否使用了 request.setCharacterEncoding("UTF-8") (对POST有效)。
“java.lang.OutOfMemoryError: Java heap space” 1. 应用内存泄漏(如不当使用 <%! %> 声明大对象、静态集合持续增长)。
2. JSP页面过大或编译生成的类过大。
3. Tomcat分配的堆内存不足。
1. 使用Profiler工具(如VisualVM)分析内存堆转储,查找泄漏点。
2. 拆分过大的JSP页面。
3. 调整Tomcat启动参数( CATALINA_OPTS JAVA_OPTS ),增加 -Xmx (最大堆内存)值。
修改JSP后,浏览器刷新看不到变化 1. 浏览器缓存。
2. Tomcat未启用开发模式(热部署)。
3. 旧的 .class 文件被缓存。
1. 强制刷新浏览器(Ctrl+F5)。
2. 检查Tomcat的 context.xml ,确保 <Context reloadable="true"> (生产环境慎用)。
3. 手动删除Tomcat的 work 目录下对应应用的缓存文件,重启Tomcat。
“The absolute uri: [http://java.sun.com/jsp/jstl/core] cannot be resolved” 1. 未导入JSTL的jar包。
2. jar包版本与 taglib 指令中的URI不匹配。
3. jar包未放在正确位置( WEB-INF/lib )。
1. 确认 jstl-xxx.jar standard-xxx.jar (对于旧版本)已添加到项目依赖并部署到 WEB-INF/lib
2. 对于JSTL 1.2,使用URI http://java.sun.com/jsp/jstl/core 。检查版本对应关系。
在JSP中使用Ajax传递参数出错 1. 参数未正确序列化或编码。
2. 后端Servlet/JSP未正确设置响应类型。
3. 路径错误。
1. 使用 JSON.stringify() 发送复杂数据,并设置 contentType: 'application/json'
2. 后端确保 response.setContentType("application/json;charset=UTF-8") ,并使用如Gson库输出JSON字符串。
3. 检查Ajax请求的URL是否为正确的后端处理地址。

5.3 性能调优与安全考量

即使只是维护,了解一些调优和安全点也很有必要。

性能方面

  • 预编译JSP :在生产环境中,可以在应用启动或部署时,通过工具(如Ant任务、Maven插件)或Tomcat配置( jspPrecompilation )预编译所有JSP,避免第一个用户触发编译带来的延迟。
  • 精简JSP页面 :避免在JSP中进行大量计算和IO操作。移除无用的导入和标签库。
  • 合理使用包含 :静态包含( <%@ include %> )在翻译期合并,适合不变的片段;动态包含( <jsp:include> )更灵活但稍有开销,根据场景选择。

安全方面

  • XSS防护 :永远不要信任用户输入。使用JSTL的 <c:out> 标签输出用户数据,它会进行HTML转义。如果确实需要输出原始HTML(如富文本编辑器内容),必须进行严格的白名单过滤。
  • 禁用脚本 :如果确定不再需要,可以在 web.xml 中全局禁用JSP脚本,强制使用EL和JSTL。
    <jsp-config>
        <jsp-property-group>
            <url-pattern>*.jsp</url-pattern>
            <scripting-invalid>true</scripting-invalid>
        </jsp-property-group>
    </jsp-config>
    
  • 错误信息暴露 :确保生产环境的 web.xml 中配置了自定义错误页面( <error-page> ),避免将包含堆栈跟踪的详细错误信息直接暴露给用户。

6. JSP在现代开发中的定位与迁移思考

时至今日,全新的项目几乎不会选择JSP作为主要视图技术。前后端分离架构(前端使用Vue/React/Angular,后端提供RESTful API)已成为绝对主流。那么,JSP还有价值吗?有的,主要在以下几个场景:

  1. 遗留系统维护 :大量银行、电信、政府、传统企业的内部系统基于Struts、Spring MVC等框架,使用JSP作为视图层。理解和维护它们是许多Java工程师的日常工作。
  2. 快速原型或简单内部工具 :对于一个小型、无需复杂交互的内部管理页面,用JSP快速撸一个可能比搭建完整的前后端分离项目更快捷。
  3. 教学与理解原理 :学习JSP是理解Java Web开发演进史、Servlet容器工作原理、MVC模式起源的绝佳途径。

如果你面对一个JSP老项目,是该重构还是该迁移?

我的建议是分步骤、看情况:

  • 第一步:规范化与安全加固 。在现有架构下,先做最低成本的改进:引入JSTL和EL全面替换脚本片段;配置安全响应头;添加CSRF防护;对输出进行转义。
  • 第二步:模块化剥离 。如果系统庞大,可以尝试将新的功能模块采用前后端分离的方式开发,通过子域名或路径与老系统集成。逐步蚕食,而非一次性重写。
  • 第三步:视图技术平移 。如果决定重构但保留后端MVC框架(如Spring MVC),可以考虑将JSP页面逐步重写为Thymeleaf或FreeMarker模板。这些模板引擎更现代、功能更强,且与Spring生态集成更好,但模板渲染的基本思路是相通的。
  • 彻底重写 :只有当业务逻辑也极度混乱,且公司有足够资源时,才考虑用全新的前后端分离架构重写。这是一项系统工程,需要谨慎评估。

最后,关于学习路线,如果你是一名初学者,我建议的路径是: Servlet -> JSP(理解原理和基本语法即可) -> JSTL+EL -> Spring MVC -> 前后端分离框架 。跳过JSP直接学Spring Boot也可以,但当你遇到那些“古老”的术语和概念时,回头看看JSP,你会对Web开发有更立体、更深刻的理解。技术的新旧更替是常态,但理解底层原理和设计思想,是工程师应对变化最坚实的底气。在维护那些布满岁月痕迹的JSP页面时,我常想,它们不仅是代码,更是一个时代的开发思想与局限性的缩影,读懂它们,也就读懂了今天我们为何要如此设计软件。

更多推荐