JSP服务器并不是直接执行JSP文件本身,而是先将它翻译成一个Servlet类的Java源代码,再编译成字节码,最终由JVM加载并执行这个Servlet实例。 整个过程发生在JSP页面的首次请求阶段,理解这一点,相当于抓住了JSP运行机制的核心。

JSP本质是Servlet的“马甲”,服务器做了三步翻译工作
很多新手写JSP时,会把它当成HTML文件来看待,实际上JSP在服务器眼里就是一个披着模板外衣的Servlet,当你把.jsp文件放进Tomcat的webapps目录并启动服务器,浏览器第一次访问它时,服务器会悄悄完成三次转换:
- JSP → .java:容器(比如Tomcat、Jetty)根据JSP语法规则,将页面中的静态HTML和Java脚本片段组合,生成一个名为
xxx_jsp.java的源文件,这个类继承自HttpServlet。 - .java → .class:调用JDK的
javac命令,把这个Java源文件编译成标准的字节码文件xxx_jsp.class。 - 加载并实例化:JVM加载这个class文件,创建Servlet对象,调用其
_jspService()方法处理请求。
关键点在于:服务器执行的是编译后的Servlet实例,而不是原始的JSP文本,这个过程只在第一次请求时发生,后续请求直接复用内存中的Servlet单例,因此JSP首访慢、后续快的原因就在这里。
编译后的Servlet源代码藏在哪个目录
以最常用的Tomcat为例,默认情况下,编译产物存放在Tomcat安装目录/work/Catalina/localhost/应用名/org/apache/jsp/下,比如你的项目叫demo,JSP文件叫index.jsp,你就能看到:
index_jsp.javaindex_jsp.class
如果是Eclipse或IDEA内嵌的Tomcat,这个路径会跑到项目的工作目录下,比如.metadata/.plugins/org.eclipse.wst.server.core/tmp0/work/,建议你亲自去翻一下,看看生成的长相:原来你在JSP里写的HTML标签、JSTL标签、EL表达式,全部变成了out.write()语句和Java代码,这就是“JSP服务器将什么源代码编译后执行”最直白的答案编译后的Servlet Java源码。
JSP生命周期五部曲,编译只是序幕
搞懂生命周期,你在面试或排查故障时就会更有底气,JSP的一生比普通Servlet多了一个“翻译成Servlet”的前置环节:
- 编译阶段:请求到来时,容器检查JSP对应Servlet是否存在且是否比源文件新,若不存在或已过期,就执行上述三步翻译。
- 加载与实例化:通过
Class.forName()加载类,调用无参构造器创建实例。 - 初始化:调用
jspInit()方法,你可以重写它来做全局资源准备,默认空实现。 - 请求处理:每个请求由
_jspService()方法处理,注意,你不能覆写这个方法,它由容器自动生成,你写的<% %>小脚本、<%= %>表达式,全被塞进了这个方法里。 - 销毁:应用卸载时调用
jspDestroy()释放资源。
行业内有个共识:JSP开发中尽量在jspInit()里完成数据库连接等重量级操作,而不是放进_jspService(),否则每个请求都会重复创建连接,这在并发稍高的场景下很容易拖垮数据库。
和原生Servlet相比,JSP编译后有什么区别
| 对比项 | JSP | 原生Servlet |
|---|---|---|
| 源码形式 | .jsp混合HTML与Java |
.java纯Java |
| 编译器处理 | 先变Servlet再编译 | 直接编译 |
| 适合场景 | 页面展示为主 | 逻辑控制为主 |
| 修改代价 | 一改即编译,无需重启 | 改完要手动编译并重启容器 |
| 输出角色 | 相当于PrintWriter的封装 |
直接操作response.getWriter() |
JSP就是Servlet的一种便捷写法,你完全可以直接用Servlet输出HTML,只不过那样要在Java字符串里拼标签,可读性极差,JSP相当于让你用HTML为主体,嵌Java代码,再让服务器自动完成“反转”编译回Servlet。
JSP文件里的哪些部分会被编译成什么样子
为了让你理解“到底编译了什么”,我拆解一个简单示例,创建一个test.jsp如下:

<%@ page contentType="text/html;charset=UTF-8" %>
<%
String name = "小明";
%>
<html>
<body>你好,<%= name %></body>
</html>
当Tomcat把它编译成test_jsp.java后,你会看到大致结构:
public final class test_jsp extends org.apache.jasper.runtime.HttpJspBase {
// 方法
public void _jspService(final HttpServletRequest request,
final HttpServletResponse response)
throws java.io.IOException, ServletException {
// ... 内置对象初始化
String name = "小明";
out.write("<html>");
out.write("<body>你好,");
out.print(name);
out.write("</body>");
out.write("</html>");
}
}
从这个编译结果可以看到三条规律:
- 模板文本(HTML、CSS、JS)变成
out.write()参数。 - JSP表达式
<%= %>变成out.print()参数,会计算并输出结果。 - JSP声明
<%! %>则变成类成员变量或方法。
当你问“jsp服务器将什么源代码编译后执行”,更精确的说法是:将你写在JSP里的指令、脚本、表达式和静态模板,重新编排成一段符合Servlet规范的Java源代码,然后编译执行。
为何JSP修改后无需重启服务器
这归功于容器的自动检测变更机制,当你保存了test.jsp,Tomcat后台会对比JSP文件时间戳和已加载类的创建时间,一旦发现前者更新,就会在下次请求时重新执行整个编译流程,替换掉内存中的旧实例,不过要注意,这个检测不是即时的,默认有频率限制,通常几秒内生效,生产环境不建议频繁上传JSP文件,因为实时重编译会导致首访请求卡顿,严重时可能触发内存中的类加载器泄漏。
常用服务器编译实现细节
不同服务器的编译产物位置和命名略有差异,但原理一致,给你一份速查表:
- Tomcat:
work/Catalina/localhost/<上下文路径>/org/apache/jsp/,类名为<文件名>_jsp。 - Jetty:临时目录下生成,通常位于
tmp/jetty-<端口>-<应用>-webapp,源码保留版本不同。 - Resin:
work/jsp/tmp/下,同样生成xxx_jsp.java。 - WebLogic:默认在
<域目录>/servers/<服务器名>/tmp/_WL_user/下。
绝大多数服务器都支持通过管理后台或配置文件设定开发模式、检查间隔和编译错误展示方式,Tomcat的web.xml里有个development参数,设为false可以提高生产环境性能,但会丢失自动重编译能力。
实操验证:手把手查看JSP编译后的Servlet源码
给你一个可立即操作的路径,配合Windows或Linux都适用:
- 用Tomcat 9启动一个包含
hello.jsp的Web应用。 - 使用浏览器访问该页面,确保编译发生。
- 打开
<Tomcat安装目录>/work/Catalina/localhost/hello/org/apache/jsp/。 - 你会看到
hello_jsp.java和hello_jsp.class两个文件。 - 用文本编辑器打开
hello_jsp.java,搜索_jspService方法,即可看到完整编译逻辑。
如果找不到work目录,可能是被配置覆盖了,你可以在catalina.properties中查看org.apache.jasper.compiler.dumpSmap等参数,或者在启动日志里查看Jasper编译器输出的编译目录。

JSP编译常见报错及调试方向
理解了源码编译过程,你就能轻松定位这些问题:
- “Unable to compile class for JSP”问题出在你的Java片段语法错误,或导入包不存在,查看Tomcat日志,定位到第几行,基本就是
_jspService方法内的对应行。 - “The method getParameter is undefined”你写了一个没设前缀的变量,或者用了未被声明的内置对象,只有
request、response、session、application等九大内置对象是现成的,自定义变量必须先声明。 - JSP修改后不生效时间戳检查失败,重启容器或手动删除
work目录下的对应文件,强制触发重编译。 - 编译超时或内存溢出JSP页面过大或包含了太多动态内容,建议拆分页面,使用include指令或tag文件,减少单文件编译压力。
从性能角度看,JSP编译是一次性开销,因此大型项目通常会在部署脚本中预先访问一次核心页面(服务器预热),或者使用JSP预编译工具(如Tomcat的JspC)来提前把全部JSP编译成class文件,避免线上首访慢、编译错误爆雷。
Q&A:关于JSP编译后执行的常见疑问
JSP编译后的Servlet和普通Servlet可以互相调用吗?
可以,JSP编译后本质就是Servlet,它拥有完整的生命周期和上下文,你完全可以在普通Servlet里通过RequestDispatcher转发到某个JSP,也可以在JSP里通过pageContext.forward()转回普通Servlet,两者共享同一个ServletContext和Session,这是JSP与Servlet能够混用的基础。
jsp服务器将什么源代码编译后执行?是.java还是.class?
这个问题要分层次回答。服务器先将JSP文本编译成.java源码,再将.java编译成.class字节码,最后JVM加载.class并创建Servlet实例执行,严格说法是:服务器执行的是class文件这个最终产物,但class来源于JSP转换出的java源文件,调试时你主要看.java,因为它的逻辑更贴近你写的JSP内容。
JSP首次编译失败后,修复JSP文件还需要重启服务器吗?
不需要,容器会在下一次请求时重新尝试编译JSP源文件,你只需保存修复后的.jsp,等待几秒钟检测周期,再刷新浏览器即可,如果一直报错,可以手动清理work目录下的编译产物,确保不会因为旧的类文件残留导致重复报错。
回到开头的结论:JSP服务器执行的是Servlet的编译产物,这个产物由JSP源码转化而来,你写的每一行模板和逻辑都会被容器重新编排成Java类,理解这条转化链,你就能解释首访慢、改后自动生效、报错行号对不上等所有JSP运行现象,也就能更自信地去排查那些诡异的问题了。
