说实话,在2024年的今天,如果你还在面试里被问起JSP,面试官大概率不是在考你“会不会写JSP”,而是在看你是否真的懂Java Web底层的“过去式”逻辑,以及你有没有踩过那些历史遗留问题的坑。毕竟,纯JSP时代已经远去,但在老项目维护、特定场景渲染、以及理解Spring MVC视图解析机制时,JSP依然是绕不开的基础知识。
今天咱们不背八股文,我把JSP的底层原理、生命周期,还有它和Spring Boot“联姻”时的各种奇葩坑,用大白话给你捋清楚。如果你能把下面这些讲得头头是道,面试官基本就得高看你一眼。
一、JSP到底是个啥?为什么它要存在?
先别急着翻书,咱得从根上理解。
JSP的全称是Java Server Pages。在早期的Java Web开发里,有个特别痛苦的问题:HTML里嵌Java代码,代码里嵌HTML,乱成一锅粥。
你想想,前端同事写了一堆漂亮的HTML,后端同事要在里面插入用户数据。如果用纯Servlet做,你得写满屏的out.println("<div>");、out.println("<span>" + userName + "</span>");……这时候,写代码的人想死,读代码的人也想死。
于是,Sun公司搞出了JSP。它的核心思想很简单:让HTML归HTML,Java归Java。 你在HTML里写特殊的标签(比如<%=%>、<% %>),服务器来帮你把Java逻辑转换成最终的HTML输出。
但这里有个关键点,很多人面试答错了:JSP本质上就是Servlet。
没错,JSP不是独立运行的,它是Servlet的一种“高级形态”或者“预处理形态”。当你第一次访问一个JSP页面时,Web容器(比如Tomcat)会做三件事:
- 翻译(Translation):把
.jsp文件转换成对应的.java源文件(本质上是一个Servlet类)。 - 编译(Compilation):把
.java文件编译成.class字节码文件。 - 执行(Execution):实例化这个Servlet类,调用
service()方法,返回HTML给浏览器。
之后,如果你修改了JSP文件,容器会检测时间戳变化,重新走一遍翻译和编译流程。
所以,当你问JSP的原理,记住一句话:JSP是编译后的Servlet。 这个认知到位了,后面的生命周期就顺理成章了。
二、JSP的生命周期:从出生到“退休”
既然JSP就是Servlet,那它的生命周期其实和Servlet几乎一样,只是多了一个“翻译”阶段。我们可以把它拆成五个核心阶段,咱们一步步拆解,顺便配上点底层细节,这样你面试时能说出花来。
第一阶段:加载与翻译(Load and Translate)
当用户第一次请求某个JSP页面时,Web容器(比如Tomcat)会接收这个请求。但容器不会直接执行JSP,它首先要把JSP页面翻译成一个Servlet源文件。
这个源文件放在哪呢?在Tomcat里,通常位于work/Catalina/localhost/你的项目名/_jsp/目录下。你可以去翻一下,里面有个类似index_jsp.java的文件,打开看看,你会惊讶地发现,原来你写的<h1>Hello</h1>在Java里变成了out.write("<h1>Hello</h1>");你写的<%= userName %>变成了out.print(getUserName())。
这一步是一次性的,除非JSP文件被修改,否则不会重新翻译。
第二阶段:编译(Compile)
翻译完成后,容器调用Java编译器,把.java文件编译成.class文件。这个.class文件就是一个标准的Servlet类,继承了HttpJspBase(而HttpJspBase又继承了HttpServlet)。
第三阶段:实例化与初始化(Instantiate and Initialize)
这一步和Servlet的init()方法对应。容器创建JSP对应的Servlet实例,并调用jspInit()方法。
jspInit()是你可以在JSP里自定义的方法,用<%! %>声明。比如:
<%!
public void jspInit() {
System.out.println("JSP页面初始化了");
}
%>
通常,我们不会重写这个方法,除非需要做一些一次性的资源加载(比如连接池初始化)。但面试时你知道有这个钩子,说明你懂细节。
第四阶段:处理请求(Process Request)
这是JSP最核心的阶段。每次有HTTP请求到达,容器都会调用_jspService()方法。注意,这个方法你不能重写,因为它是JSP规范定义的,负责处理GET、POST等所有HTTP方法。
在这个方法里,容器会为你隐式创建9个对象,也就是常说的“九大隐式对象”:request、response、session、application、out、config、pageContext、exception、page。
你平时用的request.getParameter()、session.setAttribute(),其实都是这些隐式对象提供的方法。理解这一点,你就明白为什么JSP里能直接用这些变量,而不需要先声明。
第五阶段:销毁(Destroy)
当Web应用停止,或者JSP页面被卸载时,容器会调用jspDestroy()方法,释放资源。这对应Servlet的destroy()方法。你可以在这里做最后的清理工作,比如关闭数据库连接。
面试加分项:你可以提到,JSP的生命周期和Servlet极其相似,但JSP多了“翻译”这一环。而且,JSP的实例化是懒加载的——只有第一次访问时才初始化,之后会一直驻留内存。
三、JSP vs. Servlet:谁才是王者?
很多人纠结“JSP好还是Servlet好”,其实这个问题没有标准答案,取决于场景。
- Servlet:擅长业务逻辑控制。比如接收请求、调用Service层、处理数据、设置Model,然后转发给视图。它是MVC模式里的Controller。
- JSP:擅长页面展示。它把HTML结构清晰化,通过EL表达式和JSTL标签库展示数据,是MVC模式里的View。
所以,最佳实践是各司其职:用Servlet(或Spring MVC的Controller)做控制,用JSP做展示。如果你把数据库查询逻辑写在JSP里,那不仅是“代码坏味道”,更是架构上的灾难。
四、JSP和Spring Boot整合:那些让人头秃的坑
好,原理讲完了,咱们进入重头戏。Spring Boot作为现代Java开发的事实标准,它已经不再原生支持JSP了(至少不是开箱即用)。如果你非要在这两者之间搞结合,那坑真的不少。我帮你盘点几个最常见的,以及怎么填。
坑一:Spring Boot默认找不到JSP页面
现象:你写了index.jsp,放在src/main/webapp/目录下,然后启动Spring Boot应用,访问http://localhost:8080/,结果报404,或者显示“Whitelabel Error Page”。
原因:Spring Boot的设计哲学是“约定优于配置”,它默认推荐Thymeleaf、FreeMarker等模板引擎。对于JSP,Spring Boot默认不会去webapp目录下扫描。而且,Spring Boot内嵌的Tomcat对JSP的支持是有条件的。
解决方案:
改变打包方式:你必须把项目打包成
WAR包,而不是默认的JAR包。因为内嵌Tomcat对JSP的支持在WAR包模式下更稳定。在pom.xml里改:<packaging>war</packaging>添加Spring Boot Starter Web依赖:确保有
spring-boot-starter-web。添加Tomcat Embed Jasper依赖:这是关键!JSP需要Jasper引擎来翻译和编译。
<dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-jasper</artifactId> <scope>provided</scope> </dependency>注意,
scope设为provided,因为如果是部署到外部Tomcat,外部Tomcat已经自带了Jasper,不需要重复打包。配置视图解析器:在
application.properties或application.yml里指定JSP的路径。spring.mvc.view.prefix=/WEB-INF/jsp/ spring.mvc.view.suffix=.jsp把你的
.jsp文件放在src/main/webapp/WEB-INF/jsp/目录下。等等,WEB-INF下面的资源是受保护的,用户不能直接通过URL访问,必须通过Controller转发。这其实是好事,安全。
代码示例:
@Controller
public class HelloController {
@GetMapping("/")
public String index(Model model) {
model.addAttribute("message", "Hello, Spring Boot + JSP!");
return "index"; // 会解析为 /WEB-INF/jsp/index.jsp
}
}
坑二:热部署失效,修改JSP不生效
现象:你改了JSP文件里的一个空格,重启应用才生效,或者完全没生效。
原因:Spring Boot默认关闭了JSP的热加载。即使你改了JSP,容器也不会自动重新翻译和编译。
解决方案:
启用DevTools:添加
spring-boot-devtools依赖,并配置重启。<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency>配置JSP模板缓存:在
application.properties里关闭JSP的模板缓存。spring.jsp.init-parameters.development=true或者,更直接的方式,在你的
WebMvcConfigurer配置类里:@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void configureViewResolvers(ViewResolverRegistry registry) { InternalResourceViewResolver resolver = new InternalResourceViewResolver(); resolver.setPrefix("/WEB-INF/jsp/"); resolver.setSuffix(".jsp"); resolver.setOrder(1); registry.viewResolver(resolver); } }但更有效的做法是,在
application.properties里加:server.tomcat.reload=auto这样,Tomcat会在检测到类或JSP变化时自动重启。不过,这会导致整个应用重启,而不是局部热加载。
注意:JSP的热部署始终不如Thymeleaf流畅。如果你追求极致的开发体验,建议考虑迁移到Thymeleaf。
坑三:JSP标签库(JSTL)找不到
现象:你在JSP里用了<c:forEach>或<fmt:formatDate>,结果报javax.servlet.jsp.jstl.core.LoopTagSupport cannot be cast to javax.servlet.jsp.JspTag。
原因:JSTL依赖没有正确引入,或者版本冲突。
解决方案:
在pom.xml里添加JSTL依赖:
<dependency>
<groupId>javax.servlet.jsp.jstl</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
注意,如果你用的是Spring Boot 2.x或3.x,注意包名已经从javax.*迁移到了jakarta.*。如果是Spring Boot 3.x,你需要用:
<dependency>
<groupId>jakarta.servlet.jsp.jstl</groupId>
<artifactId>jakarta.servlet.jsp.jstl</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>org.glassfish.web</groupId>
<artifactId>jakarta.servlet.jsp.jstl</artifactId>
<version>3.0.0</version>
</dependency>
代码示例:
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<c:forEach var="item" items="${items}">
<div>${item.name}</div>
</c:forEach>
坑四:编码问题,中文乱码
现象:JSP页面显示乱码,控制台也乱码。
原因:JSP默认编码可能是ISO-8859-1,而不是UTF-8。
解决方案:
JSP页面头部声明:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>Spring配置编码过滤器:在
application.properties里:spring.http.encoding.force=true spring.http.encoding.charset=UTF-8 spring.http.encoding.enabled=true server.servlet.encoding.charset=UTF-8 server.servlet.encoding.force=true server.servlet.encoding.enabled=true确保IDE和数据库也是UTF-8:这是一个全局问题,不能只改JSP。
坑五:EL表达式不生效
现象:在JSP里写${userName},结果显示的是字面量${userName},而不是变量值。
原因:JSP 2.0之前,EL表达式默认是关闭的。或者,你没有引入正确的JSP规范版本。
解决方案:
确保JSP版本是2.0+:在
web.xml里声明:<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" version="3.1"> </web-app>JSP页面头部:
<%@ page isELIgnored="false" %>默认情况下,JSP 2.0+是开启EL的,所以通常不需要显式设置。但如果遇到了,加上这行就行。
五、为什么现在不推荐在新项目里用JSP?
聊完了坑,咱得说说大趋势。为什么Spring Boot默认不推JSP?为什么现在的新项目几乎不用JSP?
- 前后端分离成为主流:现在的架构,前端用React、Vue、Angular,后端只提供REST API。JSP这种服务端渲染的页面,已经不再需要了。
- 视图层解耦:JSP把HTML和Java逻辑混在一起,维护困难。Thymeleaf、FreeMarker等模板引擎虽然也是服务端渲染,但它们更简洁,更符合“关注点分离”原则。
- 热部署体验差:如前所述,JSP的热部署始终是个痛点。
- Spring Boot原生支持Thymeleaf:Spring Boot对Thymeleaf的支持是开箱即用的,配置简单,文档完善。
建议:如果你是在维护老项目,不得不和JSP打交道,那希望这篇文章能帮你填坑。但如果你是在开启一个新项目,强烈建议直接使用Thymeleaf,或者彻底前后端分离。别再折腾JSP了,除非你的老板说“公司里有500个JSP页面,你得改”。
六、面试实战:如果面试官问“JSP和Servlet的区别”,你怎么答?
别急着背“JSP用于视图,Servlet用于控制”。你可以这样展开:
“从本质上看,JSP是Servlet的一种简化形式。JSP在编译期会被转换成Servlet,所以它们的执行效率是一样的。区别在于开发阶段:JSP更适合写静态HTML和动态内容混合的页面,语法更贴近前端;Servlet更适合写业务逻辑和控制流程。在MVC架构中,我们通常用Servlet(或Spring MVC的Controller)做控制器,用JSP做视图。但现代开发中,JSP已经逐渐被Thymeleaf等模板引擎取代,因为JSP的混合语法难以维护,且热部署体验不佳。”
这样答,既展示了你的历史知识,又体现了你对现代技术的了解,面试官会觉得很全面。
结语
JSP虽然是个“老古董”,但它承载着Java Web发展的厚重历史。理解它的原理和生命周期,不仅能帮你应对面试,更能让你看清Java Web技术的演进脉络。和Spring Boot整合时的坑,每一个都是前人踩过的雷,现在你知道了,就可以绕着走。
最后送大家一句话:技术没有好坏,只有适合与否。 JSP适合它的时代,Thymeleaf适合它的时代,前后端分离也适合它的时代。作为开发者,我们要做的,是理解每个技术背后的设计哲学,然后在合适的场景下做出合适的选择。
希望这篇文章能帮你在面试中游刃有余,也能在实战中少踩几个坑。如果还有疑问,欢迎在评论区交流,咱们一起探讨。
