还记得你第一次用Servlet往浏览器里输出一整个HTML页面时的心情吗?字符串拼接标签、转义引号、数据混在HTML里改来改去,那时候我就想:要是能直接在HTML里写Java代码就好了。JSP就是为解决这个痛点而生的。
这篇博文是JavaWeb开发系列的第六篇,聊的是JSP基础。我默认看这篇文章的朋友已经掌握了Servlet的基本用法,知道请求响应模型,也了解MVC的思想,正准备进入视图层的开发。如果你常年用前后端分离那套架构,那JSP确实用得不多,但它并没有过时——国内大量存量系统、中小型项目的管理后台、模板渲染场景,JSP依然是主力。这篇文章我会从JSP的运行原理讲起,带你把JSP的基础语法、隐式对象、指令标签过一遍,再用一个完整的个人信息展示页面把知识点串起来,最后把“JSP改了不生效”“中文乱码”“JSON数据输出”这些高频问题一并解决掉。
1. JSP到底是个什么东西:先破除神秘感
1.1 JSP的本质就是Servlet
你可能会觉得JSP是个全新的技术,其实不是。JSP的全称是Jakarta Server Pages,以前叫JavaServer Pages,本质上它就是一个被“包装过”的Servlet。我第一次接触JSP的时候,老师直接在黑板上画了一张图:浏览器发出请求,服务器找到对应的JSP文件,先把JSP翻译成Java文件,再编译成class文件,然后执行,把结果返回给浏览器。
我当时听得一头雾水,直到自己动手把Tomcat的work目录翻出来看了一眼,才恍然大悟。你在work/Catalina/localhost/项目名/org/apache/jsp/目录下,能看到每个JSP文件对应的_jsp.java和_jsp.class文件。打开那个Java文件,你会发现它继承了org.apache.jasper.runtime.HttpJspBase,而这个类间接实现了HttpServlet接口。所以你可以把JSP理解成一个“反过来的Servlet”:Servlet是Java代码里嵌HTML字符串,JSP是HTML里嵌Java代码。
1.2 JSP的生命周期和请求流程
JSP的生命周期和Servlet类似,但多了一个翻译阶段。完整流程是这样的:
- 浏览器发送请求,Tomcat接收到后缀为
.jsp的请求后,交给JSP引擎处理。 - JSP引擎检查这个JSP文件有没有对应的Java文件,或者JSP文件是否比已有的Java文件更新。
- 如果是第一次访问,或者JSP文件被修改过,引擎会把JSP翻译成Java源文件,然后编译成class文件。
- 容器加载这个class,创建实例,调用
jspInit()方法完成初始化(只会执行一次)。 - 每次请求都会调用
_jspService()方法,方法内部执行你写在JSP里的脚本代码,然后把生成的HTML响应回浏览器。 - 容器销毁时调用
jspDestroy()方法。
这里有一个非常多新手踩的坑:第一次访问JSP页面经常特别慢,感觉像卡住了一样。原因就是第一次访问需要完成翻译加编译,后面再访问就直接走class文件了。所以上线之前最好先“预热”一遍JSP页面,把所有页面都访问一次,这样用户看到的就不会是第一印象的“卡顿”。
1.3 JSP和Servlet的分工边界
JSP出现以后,很多初学者容易走向另一个极端:所有代码都写在JSP里,数据库连接也写在JSP里,业务逻辑也写在JSP里,页面变成了一锅粥。我见到过把几百行Java代码写在JSP里的项目,维护起来简直是一场灾难。
按MVC的思想来划分:JSP只负责视图(View)的渲染,也就是数据的展示;Servlet负责控制器(Controller),接收请求、调度业务逻辑;JavaBean或者Service负责模型(Model),处理数据和业务规则。真实项目中你很少会看到Servlet直接输出HTML,也很少会看到JSP直接操作数据库,各司其职才是JSP正确的使用姿势。
2. JSP的核心语法:脚本、指令、动作与隐式对象
2.1 三种脚本元素:声明、表达式、脚本段
JSP提供了三种脚本元素,对应Java代码里的三种场景。
声明用<%! ... %>包裹,用来定义成员变量和方法。声明的内容会翻译成Servlet类的成员变量和成员方法,所以多个请求之间共享一份数据,这也意味着它是线程不安全的,非必要不要用。表达式用<%= ... %>包裹,用来向页面输出内容,翻译后相当于out.print(...),注意表达式结尾不能加分号。脚本段用<% ... %>包裹,可以写任意Java语句,翻译后直接嵌在_jspService()方法里。
看个具体例子:
<%! private String siteName = "会员中心"; private String formatTime(Date date) { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); return sdf.format(date); } %> <% String username = (String) request.getSession().getAttribute("loginUser"); if (username == null) { username = "游客"; } %> <html> <body> <h1><%= siteName %></h1> <p>欢迎你,<%= username %></p> <p>当前时间:<%= formatTime(new Date()) %></p> </body> </html>看到没有,脚本段里写逻辑,表达式负责输出,声明提供公共方法。但我要强调的是,这三种元素都是早期JSP的写法,现在的主流思路是尽量在Servlet里把数据准备好,在JSP里用EL表达式和JSTL标签来展示。脚本元素能不用就不用,实在要用了也别把业务逻辑往里塞。
2.2 page指令:配置页面行为的关键
page指令是JSP里最常用的指令,用来声明页面的属性。常见属性有这些:
| 属性 | 作用 | 常用值 |
|---|---|---|
import | 导入Java类,和Java里的import一样 | java.util.* |
contentType | 设置响应类型和字符集 | text/html; charset=UTF-8 |
pageEncoding | 指定JSP文件本身的编码 | UTF-8 |
session | 页面是否参与会话 | true/false |
isELIgnored | 是否忽略EL表达式 | true/false |
errorPage | 页面出错时跳转的页面 | error.jsp |
isErrorPage | 声明当前页面是错误处理页 | true/false |
很多中文乱码问题的根源,就是pageEncoding和contentType里的字符集没有设置正确。我一般建议在JSP文件第一行就写上:
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>contentType的charset影响的是响应内容,告诉浏览器用什么编码来解析;pageEncoding影响的是JSP文件本身的读取,告诉服务器用什么编码来读这个文件。两个都写UTF-8,再配合request.setCharacterEncoding("UTF-8")和response.setCharacterEncoding("UTF-8"),乱码问题能解决掉八成。
2.3 include指令与include动作的区别
JSP里有两张“包含”的玩法,一个是<%@ include file="..." %>指令,一个是<jsp:include page="..." />动作。两者的区别非常重要。
include指令是静态包含,翻译阶段就把被包含的文件内容“复制粘贴”到当前页面里,最终只生成一个Java文件。这种方式的好处是性能好,因为是直接把内容嵌进去的;坏处是被包含的文件如果有变量,容易和当前页面的变量冲突,而且被包含文件改了以后,当前页面也需要重新编译。
include动作是动态包含,它会在运行时把被包含页面的执行结果“拿过来”嵌入当前页面。每次请求都会单独去执行被包含的页面,所以被包含页面改了以后,只要重新编译它自己就够了。这种方式更灵活,但性能略差一些。
实际项目里,页头、页脚、导航栏这些公共部分,我习惯用include指令来做,因为它们是静态内容,嵌进去以后效率最高。如果是有独立逻辑的动态模块,比如登录状态栏、天气信息这种,就用include动作,这样每个模块可以独立维护。
2.4 九大隐式对象:页面里的“免费劳力”
JSP为我们准备好了九大对象,不需要自己创建,直接在脚本段或表达式里就能用。这九大对象对应Servlet里的不同角色:
| 对象 | 类型 | 作用域 |
|---|---|---|
request | HttpServletRequest | 一次请求 |
response | HttpServletResponse | 一次请求 |
out | JspWriter | 页面输出 |
session | HttpSession | 一次会话 |
application | ServletContext | 整个应用 |
config | ServletConfig | 配置信息 |
pageContext | PageContext | 当前页面 |
page | Object | 当前Servlet实例 |
exception | Throwable | 错误页面 |
request、response、session这些你在Servlet里已经很熟了,重点说说pageContext。它是九大对象里比较“万能”的一个,可以获取其他八个对象,还可以操作各种作用域的数据。比如pageContext.setAttribute("key", value, PageContext.REQUEST_SCOPE),一行代码就能往指定作用域存数据。pageContext.findAttribute("key")会按照page -> request -> session -> application的顺序查找属性,这个特性在后面讲EL表达式时会很有用。
2.5 标准动作标签:JSP自带的组件化方案
JSP标准动作标签以jsp:前缀开头,是JSP规范里自带的一组标签,不需要引入任何外部依赖。常用的有这些:
<jsp:include>:动态包含页面。<jsp:forward>:转发请求到另一个页面或Servlet。<jsp:useBean>:创建或获取一个JavaBean实例。<jsp:setProperty>:设置JavaBean的属性。<jsp:getProperty>:获取JavaBean的属性。<jsp:param>:在include或forward时传递参数。
比如转发并带参数:
<jsp:forward page="/login.jsp"> <jsp:param name="msg" value="请先登录" /> </jsp:forward>使用jsp:useBean时,默认会先查找指定作用域里有没有这个Bean,有就直接用,没有才会新建。这个先查找后创建的特性,在页面里展示数据时候很实用。不过现在的开发中,useBean系列基本被EL表达式和JSTL取代了,我优先推荐大家学EL和JSTL,这些动作标签了解即可。
3. EL表达式与JSTL:让JSP回归模板的本分
3.1 EL表达式:访问数据的便捷通道
EL(Expression Language)表达式是JSP 2.0引入的,用${...}包裹,目的是替代脚本段的取值操作。它的核心作用就是“取数据”,语法非常简洁。
EL内置了一套“点号”规则来访问数据。如果域对象中存了一个User对象,其中有username属性,那${user.username}就能取到这个属性。你没有看错,不需要调用getUsername()方法,EL会自己找到标准的getter或者JavaBean属性映射。
EL将数据存储空间分成了四个作用域,用点的形式依次查找:pageContext、requestScope、sessionScope、applicationScope。如果直接用${user},EL会按照这四个作用域的顺序依次查找叫user的属性,找到哪个用哪个。这就带来一个问题:如果四个作用域都有同名属性,你可能取到的是作用域里最小那个。所以当有同名属性时,建议显式指定作用域,比如${sessionScope.user}。
EL还支持简单的运算,例如算术运算${1 + 2}、比较运算${score > 60}、逻辑运算${empty list}。还有一个超级好用的empty运算符,它既能判断是否为null,又能判断字符串是否为空串,还能判断集合是否为null或者没有元素。我写页面时经常这样做:
<c:if test="${empty userList}"> <tr><td colspan="4">暂无数据</td></tr> </c:if>EL还提供了很多内置对象,比如${pageContext.request.contextPath}可以拿到项目根路径,这个在页面里写绝对路径时太重要了。用相对路径的话,一旦当前URL层级发生变化,资源文件路径就全乱了,所以我都是统一用这个表达式来拼路径。
3.2 JSTL核心标签库:逻辑控制不再写Java
JSTL(JSP Standard Tag Library)是JSP的标准标签库,提供了很多现成标签来处理常见的页面逻辑。使用之前,需要先引入核心标签库的指令:
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>prefix="c"是给这个标签库起的简称,后面的uri是这个库的唯一标识。引入之后就可以用了。
最常用的是<c:if>、<c:choose>和<c:forEach>这三个。
<c:if>是条件判断,但注意它没有else分支。需要if-else效果时用<c:choose>搭配<c:when>和<c:otherwise>:
<c:choose> <c:when test="${user.level == 1}"> <span class="badge badge-primary">普通会员</span> </c:when> <c:when test="${user.level == 2}"> <span class="badge badge-success">高级会员</span> </c:when> <c:otherwise> <span class="badge badge-danger">VIP会员</span> </c:otherwise> </c:choose><c:forEach>是循环遍历,支持集合、数组和Map。比如遍历用户列表:
<c:forEach items="${userList}" var="user" varStatus="status"> <tr> <td>${status.index + 1}</td> <td>${user.username}</td> <td>${user.email}</td> </tr> </c:forEach>varStatus属性非常好用,它暴露了循环的状态,比如index(当前索引,从0开始)、count(循环计数,从1开始)、first(是否是第一条)和last(是否是最后一条)。列表表格里做序号列、条纹背景就靠它了。
JSTL还有格式化标签库(fmt),做日期和数字格式化很好用:
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> <fmt:formatDate value="${user.createTime}" pattern="yyyy-MM-dd HH:mm:ss" /> <fmt:formatNumber value="${user.balance}" pattern="#,##0.00" />视图层尽量只做展示和轻逻辑,这一点一定要记住。有了EL和JSTL,你几乎可以完全告别JSP里的Java脚本代码了,页面干净不少,可维护性也高了不止一个档次。
3.3 MVC中的JSP最佳实践
把前面这些内容串起来,一个合理的MVC数据流长这样:Servlet接收请求,调用Service层获取数据,把数据封装成JavaBean或List,然后用request.setAttribute()存到请求域中,最后转发到JSP。JSP里用EL取数据,用JSTL控制结构,完全不写Java代码。
这个模式的好处是,前端改页面样式时不需要懂Java,后端改逻辑时也不会碰坏页面。分工清晰,每个人改自己擅长的部分。
4. 实战案例:从零搭建一个会员信息展示页面
4.1 准备工作:2023版IDEA创建JavaWeb项目
先说创建项目。现在大家用的IDEA基本都是2021以后的版本,新建JavaWeb项目和以前不太一样。打开IDEA,选择新建项目(New Project),左侧选Jakarta EE,这个选项在较新的IDEA版本里默认就带。然后在Application Server那边选你本地的Tomcat,比如Tomcat 9或Tomcat 10。如果下拉框里没有,点New手动选择Tomcat的安装目录,IDEA会识别到Tomcat的版本信息。
接下来配置项里要注意两个关键选择:
- Template那里可以选
Web Application,这样会自带一个标准的webapp目录结构。 - 如果不用模板直接建普通Java项目,之后再手工添加
webapp目录也行,但比较啰嗦,建议直接用模板。
项目建好以后,默认的webapp/WEB-INF/下会有web.xml文件。如果你的Tomcat版本比较高,web.xml可能不在项目里,因为Servlet 3.0以后支持注解配置,web.xml不是必须的了。但JSP项目我建议保留一个web.xml,后面设置欢迎页和URL映射时方便。
还要检查一下项目依赖里有没有Servlet API和JSP API的jar包。很多新手项目跑起来报ClassNotFoundException: javax.servlet.*,通常就是这个原因。IDEA新建的Jakarta EE项目一般会自动带,但要注意版本匹配问题:Tomcat 9及之前用javax.servlet包名,Tomcat 10开始改成jakarta.servlet包名,这俩不能混用,会直接编译报错。
4.2 构建一个带分页和条件渲染的列表页
光说理论没用,直接做一个例子。假设我有一个旅游管理系统的后台,需要展示会员列表。这个场景非常典型,各家后台管理系统的列表页都长差不多。
先写一个Member实体类:
public class Member { private int id; private String username; private String email; private String phone; private int level; // 1普通 2高级 3VIP private double balance; private Date createTime; // 省略getter/setter }然后是Servlet,负责查数据并转发:
@WebServlet("/member/list") public class MemberListServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { MemberService service = new MemberService(); int page = 1; int pageSize = 10; String pageStr = request.getParameter("page"); if (pageStr != null && !pageStr.isEmpty()) { page = Integer.parseInt(pageStr); } PageResult<Member> pageResult = service.queryPage(page, pageSize); request.setAttribute("pageResult", pageResult); request.getRequestDispatcher("/member/list.jsp").forward(request, response); } }接下来是JSP页面。这里的核心是分页按钮和表格数据展示。分页按钮加上上一页、下一页的链接,上一页的URL拼上page=page-1,下一页拼上page=page+1:
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> <c:set var="ctx" value="${pageContext.request.contextPath}" /> <html> <head> <title>会员列表</title> <link rel="stylesheet" href="${ctx}/css/style.css"> </head> <body> <h2>会员列表</h2> <table border="1" cellpadding="8" cellspacing="0"> <tr> <th>序号</th> <th>用户名</th> <th>邮箱</th> <th>等级</th> <th>余额</th> <th>注册时间</th> </tr> <c:forEach items="${pageResult.list}" var="m" varStatus="st"> <tr> <td>${(pageResult.currentPage - 1) * pageResult.pageSize + st.count}</td> <td>${m.username}</td> <td>${m.email}</td> <td> <c:choose> <c:when test="${m.level == 1}">普通会员</c:when> <c:when test="${m.level == 2}">高级会员</c:when> <c:otherwise>VIP会员</c:otherwise> </c:choose> </td> <td><fmt:formatNumber value="${m.balance}" pattern="¥#,##0.00" /></td> <td><fmt:formatDate value="${m.createTime}" pattern="yyyy-MM-dd" /></td> </tr> </c:forEach> </table> <div class="pagination"> <c:if test="${pageResult.currentPage > 1}"> <a href="${ctx}/member/list?page=${pageResult.currentPage - 1}">上一页</a> </c:if> 第 ${pageResult.currentPage} / ${pageResult.totalPages} 页 <c:if test="${pageResult.currentPage < pageResult.totalPages}"> <a href="${ctx}/member/list?page=${pageResult.currentPage + 1}">下一页</a> </c:if> </div> </body> </html>这个例子虽然短,但把EL取数、JSTL条件判断、循环遍历、格式化标签、动态路径都串起来了,算是JSP基础语法的综合应用。
有一个细节要提醒:String拼接URL时注意page参数的类型转换,如果用户直接在浏览器地址栏把page改成非数字,Integer.parseInt会抛异常。实际项目里一定要做参数校验和异常处理,这里为了演示简洁先不写全。
4.3 JSP里输出JSON数据和导出Excel
有人会觉得JSP只能输出HTML,其实response的ContentType一变,它能输出的类型多着呢。最常见的一个场景是页面里用jQuery发AJAX请求,后端需要返回JSON数据。
JSP页面里输出JSON的办法是这样的:
<%@ page contentType="application/json; charset=UTF-8" pageEncoding="UTF-8" %> <% // 实际项目中:从Service获取数据,然后用Fastjson或Jackson序列化 User user = new User(); user.setId(1); user.setUsername("zhangsan"); // 这里演示直接用字符串,真实场景建议用ObjectMapper String json = "{\"id\":1,\"username\":\"zhangsan\"}"; out.print(json); %>这段代码能跑,但我不推荐用脚本段这种写法。更好的做法是:在Servlet里把数据序列化好,然后设置response.setContentType("application/json; charset=UTF-8"),直接用response.getWriter().write(json)输出。是否需要通过JSP输出JSON,实际意义不大,还会多一次视图层跳转的开销。这个场景更适合在Servlet里完成,大家千万别学我上面那堆脚本式的写法,我只是演示JSP的能力边界。
导出Excel也是类似的思路。把ContentType设成application/vnd.ms-excel,浏览器就会识别为Excel文件,然后下载。你可以在JSP里用循环把表格内容输出为Excel兼容的HTML格式,用response.setHeader("Content-Disposition", "attachment;filename=member.xls")设置下载文件名。这个办法不需要任何第三方库,原理是利用Excel能打开HTML表格的特性,实用性很强。但数据量大的场景(比如上万行),建议直接用POI等专业库来生成真正的Excel文件,性能和格式控制好得多。
4.4 文件上传和MP4视频播放的JSP页面
搜索词里还有两个高频场景:jsp实现mp4视频播放和文件上传下载。我在这里简单讲一下。
视频文件通常不要放进项目目录里,更不要放到WEB-INF下,因为WEB-INF下的资源对浏览器是受保护的,外部访问不到。常规做法是把文件存到一个独立的上传目录,比如D:/upload/或者服务器上的/data/upload/,然后在Tomcat里配置虚拟目录映射,把/upload/**的访问指向物理目录。如果你用的是IDEA内置的Tomcat,配置虚拟目录有点麻烦,我一般直接用项目根目录下的static/upload/来存,然后页面里的src指向${ctx}/static/upload/video.mp4。
播放视频的JSP核心就一个<video>标签:
<video src="${ctx}/static/upload/sample.mp4" controls width="640" height="360"> 您的浏览器不支持HTML5视频播放。 </video>这里最容易出问题的是MIME类型。Tomcat默认配置里对.mp4的Content-Type是video/mp4,大多数情况能正常播放,但如果浏览器不认这个类型,播放器就会黑屏。你可以在web.xml里显式配置一下:
<mime-mapping> <extension>mp4</extension> <mime-type>video/mp4</mime-type> </mime-mapping>文件上传的JSP页面就两件事:表单加enctype="multipart/form-data",然后用Servlet 3.0的Part接口接收文件。JSP本身不负责处理上传,它只是提供一个带文件选择框的表单而已,真正的处理逻辑还是在Servlet里。
5. 高频问题排查实录:改了不生效、乱码与路径问题
5.1 “JSP改了不生效”的五种常见原因
这个拿去搜索,命中率绝对高。每个JSP开发者都经历过“我明明改了代码,刷新页面还是老样子”的绝望时刻。我总结一下常见的五个原因,按排查优先级排:
第一个,浏览器缓存。JSP页面在浏览器侧可能会被缓存,尤其是用GET请求访问时。第一次加载后,浏览器直接把换回的东西存在本地缓存里,你改了服务器端代码,浏览器还在吃老本。解决办法很简单,开发的时候按Ctrl+F5强制刷新,或者在response头里加禁用缓存的配置。
第二个,IDEA的JSP引擎没有重新部署。改完JSP以后,IDEA不一定会自动把新文件同步到Tomcat的部署目录里。你需要看看IDEA右上角的Build菜单,确认已勾选自动构建,或者每次改完手动执行一次Build -> Rebuild Project。
第三个,Tomcat的work目录里有旧的编译产物。JSP翻译成的Java文件和class文件都存在Tomcat的work目录里。如果Tomcat没检测到JSP文件被修改(文件时间戳没变),它会直接用旧的class。这种情况我一般直接删掉work/Catalina/localhost/项目名目录,然后重启Tomcat,让一切重新来。这个方法屡试不爽。
第四个,JSP页面被include到了其他页面。如果你修改的是一个被静态包含的公共文件,比如header.jsp,那么主页面不会自动感知到它的变化。因为静态包含是翻译阶段就把内容复制进去了,主页面重新编译之前,被包含文件的修改不会生效。解决办法是修改主页面或者把公共部分改成动态包含。
第五个,部署位置不对。IDEA有个Deployment的设置,决定了项目的资源部署到哪里。如果这里配置错了,你改的文件根本不会发布到Tomcat的运行目录里。检查路径的方法:在IDEA底部打开Server面板,看它的Deployed资源路径,确认改动同步到了目标目录。
5.2 中文乱码的根源和一次性解决方案
JSP乱码问题,根源就是编码不一致。整个链路上有四个环节需要统一编码:
- JSP文件本身的编码(
pageEncoding)。 - 服务器发送给浏览器的编码(
contentType的charset)。 - 浏览器解析页面的编码(取决于响应头里的
Content-Type)。 - 如果涉及表单提交或AJAX请求,还有请求参数的编码。
排查思路如下:
如果页面上中文显示成???,那是JSP文件本身编码不对,把pageEncoding改成UTF-8,并且确认文件确实是以UTF-8格式保存的。IDEA右下角能看到文件编码格式,确保是UTF-8。
如果页面上的DB数据中文乱码,多半是数据库连接串没有指定编码。JDBC连接URL后面加?useUnicode=true&characterEncoding=UTF-8,MySQL里表字段的字符集也要保证是utf8mb4(MySQL 5.5以后推荐utf8mb4,能存emoji字符)。
如果是表单提交成功后页面数据中文乱码,GET请求修改Tomcat的server.xml里的URIEncoding="UTF-8",POST请求在Servlet或过滤器中调用request.setCharacterEncoding("UTF-8")。Servlet 4.0之后,还可以在web.xml里配置一个编码过滤器,一次配置全局生效:
<filter> <filter-name>encodingFilter</filter-name> <filter-class>org.apache.catalina.filters.SetCharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>encodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>用了这个过滤器以后,整个项目的POST请求编码都会被统一成UTF-8。实测下来很省心。
5.3 import和依赖瘦身:JSP里导包的两个技巧
热搜词里有个jsp jsonarray import,这其实是两个独立的问题:JSP里怎么导入JSON相关的包。
第一个问题,怎么在JSP里导入类。在<%@ page %>指令的import属性里写逗号分隔的类路径就行:
<%@ page import="com.alibaba.fastjson.JSONArray" %> <%@ page import="com.alibaba.fastjson.JSONObject" %>注意import可以分多条写,也可以一条里用逗号分隔多个类。
第二个问题,怎么把依赖的jar包部署到项目里。很多人把jar包下载好了,也加到IDEA的Library里了,代码里import也不报错,但一运行就报ClassNotFoundException。原因很简单:jar包只出现在了编译期的ClassPath里,没有出现在Tomcat运行时的WEB-INF/lib目录下。
解决办法:把jar包复制到webapp/WEB-INF/lib目录下,或者在IDEA里对jar包右键,选择Add as Library,然后确认部署时勾选了Include in project build。用Maven或者Gradle项目的同学,直接声明依赖就够了,构建工具会自动把jar包打进去。我建议从前期就统一用Maven,依赖管理会省很多事。
5.4 页面路径那些事:contextPath的正确用法
绝对路径还是相对路径,这个问题不搞清楚,页面里引CSS、JS、图片、跳转链接时容易出各种“找不到资源”的错。根本原因是,浏览器地址栏里的URL和应用的实际根路径不一定一致。举个例子,项目部署名是travel,你的JSP页面在/member/list.jsp,如果页面里写死href="/css/style.css",浏览器会去请求http://localhost:8080/css/style.css,显然找不到,正确路径是http://localhost:8080/travel/css/style.css。
所以写路径时优先用${pageContext.request.contextPath}来动态拼。比如:
<link rel="stylesheet" href="${ctx}/css/style.css"> <a href="${ctx}/member/list?page=1">会员列表</a> <form action="${ctx}/member/add" method="post">...</form>页面顶部先用<c:set>把这个变量存起来,后面直接用${ctx},页面会清爽很多。还有一个小细节:项目部署名不能频繁改,一旦改了部署名,所有绝对路径都要跟着变。最稳妥的做法是用根路径部署,也就是contextPath为空,这样项目访问地址为http://localhost:8080/,相对路径的写法就跟Standalone的应用差不多了。
6. 关于JSP后续演进:从JSP到模板引擎的迁移思路
学了JSP基础以后,很多人会有一个疑惑:现在新项目还该不该用JSP?我的看法是,JSP的语法和思想值得学,因为你会遇到大量存量代码用JSP写的项目,接手别人的项目连JSP都看不懂才是真麻烦。但如果从零开始一个新项目,又不需要依赖Java服务端渲染的特殊能力,我更推荐用Thymeleaf、Freemarker这类现代模板引擎,它们语法更干净,和前端协作更顺滑。
迁移的过程也简单。JSP里的<%= xxx %>和${xxx},Thymeleaf里用th:text="${xxx}"代替;JSP的<c:forEach>,Thymeleaf里用th:each代替;JSP的<%@ include %>,Thymeleaf里用th:insert和th:replace。核心的思想完全相通——数据放域中,模板负责展示,渲染逻辑和业务逻辑分离。你把JSP的阶段学好以后,切到其他模板引擎基本上半天就能上手。
但有一件事必须强调:无论你用什么模板引擎,“把页面当视图、把逻辑留在后端”这个MVC原则是不变的。JSP曾经被滥用,不是因为JSP技术本身有问题,而是因为用的人没有边界感。任何技术都禁不起“无脑往里塞逻辑”这种用法。
7. 写在最后的一点经验
这一系列文章写下来,JSP基础算是告一段落了。我自己带新人的时候,最常说的话就是:如果JSP页面里出现了大段的Java脚本代码,你第一件事不是把代码跑通,而是先停下来想想架构上哪里出了问题。JSP的定位永远是视图层,是给前端展示数据用的,不是让你写业务逻辑的。
你在动手练习的时候,先不要急着上框架,用原生的Servlet加JSP,把一个登录注册、列表分页、增删改查完整做一遍。这个过程看起来“土”,但它能把HTTP协议、请求响应、作用域、JSP翻译机制这些底层的原理彻底打通。多年以后你回头看,这一步走得值。
JSP的技术栈并不复杂,语法标签就那么几个,真正的难点在于理解它的定位,学会在各种技术之间做权衡。下一篇我会接着讲JavaWeb里另一个躲不掉的话题:Session与Cookie的进阶应用。到时候我们再来聊聊登录状态保持、跨域会话共享的问题。