news 2026/9/10 8:08:17

JSP入门到实践:从运行原理到EL/JSTL与常见问题排查

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JSP入门到实践:从运行原理到EL/JSTL与常见问题排查

还记得你第一次用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类似,但多了一个翻译阶段。完整流程是这样的:

  1. 浏览器发送请求,Tomcat接收到后缀为.jsp的请求后,交给JSP引擎处理。
  2. JSP引擎检查这个JSP文件有没有对应的Java文件,或者JSP文件是否比已有的Java文件更新。
  3. 如果是第一次访问,或者JSP文件被修改过,引擎会把JSP翻译成Java源文件,然后编译成class文件。
  4. 容器加载这个class,创建实例,调用jspInit()方法完成初始化(只会执行一次)。
  5. 每次请求都会调用_jspService()方法,方法内部执行你写在JSP里的脚本代码,然后把生成的HTML响应回浏览器。
  6. 容器销毁时调用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

很多中文乱码问题的根源,就是pageEncodingcontentType里的字符集没有设置正确。我一般建议在JSP文件第一行就写上:

<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>

contentTypecharset影响的是响应内容,告诉浏览器用什么编码来解析;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里的不同角色:

对象类型作用域
requestHttpServletRequest一次请求
responseHttpServletResponse一次请求
outJspWriter页面输出
sessionHttpSession一次会话
applicationServletContext整个应用
configServletConfig配置信息
pageContextPageContext当前页面
pageObject当前Servlet实例
exceptionThrowable错误页面

requestresponsesession这些你在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将数据存储空间分成了四个作用域,用点的形式依次查找:pageContextrequestScopesessionScopeapplicationScope。如果直接用${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,其实responseContentType一变,它能输出的类型多着呢。最常见的一个场景是页面里用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默认配置里对.mp4Content-Typevideo/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)。
  • 服务器发送给浏览器的编码(contentTypecharset)。
  • 浏览器解析页面的编码(取决于响应头里的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:insertth:replace。核心的思想完全相通——数据放域中,模板负责展示,渲染逻辑和业务逻辑分离。你把JSP的阶段学好以后,切到其他模板引擎基本上半天就能上手。

但有一件事必须强调:无论你用什么模板引擎,“把页面当视图、把逻辑留在后端”这个MVC原则是不变的。JSP曾经被滥用,不是因为JSP技术本身有问题,而是因为用的人没有边界感。任何技术都禁不起“无脑往里塞逻辑”这种用法。

7. 写在最后的一点经验

这一系列文章写下来,JSP基础算是告一段落了。我自己带新人的时候,最常说的话就是:如果JSP页面里出现了大段的Java脚本代码,你第一件事不是把代码跑通,而是先停下来想想架构上哪里出了问题。JSP的定位永远是视图层,是给前端展示数据用的,不是让你写业务逻辑的。

你在动手练习的时候,先不要急着上框架,用原生的Servlet加JSP,把一个登录注册、列表分页、增删改查完整做一遍。这个过程看起来“土”,但它能把HTTP协议、请求响应、作用域、JSP翻译机制这些底层的原理彻底打通。多年以后你回头看,这一步走得值。

JSP的技术栈并不复杂,语法标签就那么几个,真正的难点在于理解它的定位,学会在各种技术之间做权衡。下一篇我会接着讲JavaWeb里另一个躲不掉的话题:Session与Cookie的进阶应用。到时候我们再来聊聊登录状态保持、跨域会话共享的问题。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 8:03:07

CANN/ge Graph Engine InferValueRangeFuncRegister API

InferValueRangeFuncRegister构造函数和析构函数 【免费下载链接】ge GE&#xff08;Graph Engine&#xff09;是面向昇腾的图编译器和执行器&#xff0c;提供了计算图优化、多流并行、内存复用和模型下沉等技术手段&#xff0c;加速模型执行效率&#xff0c;减少模型内存占用。…

作者头像 李华
网站建设 2026/9/10 8:03:05

CANN/GE IR输入定义结构体

IrInputDef 【免费下载链接】ge GE&#xff08;Graph Engine&#xff09;是面向昇腾的图编译器和执行器&#xff0c;提供了计算图优化、多流并行、内存复用和模型下沉等技术手段&#xff0c;加速模型执行效率&#xff0c;减少模型内存占用。 GE 提供对 PyTorch、TensorFlow 前端…

作者头像 李华
网站建设 2026/9/10 8:02:11

量化交易必备:AutoHedge自动对冲系统设计与实现

做量化的朋友应该都有过这种体验&#xff1a;策略本身跑得好好的&#xff0c;但隔三差五就要被极端行情教训一顿——单边拉涨不敢追&#xff0c;瀑布式下跌舍不得割&#xff0c;仓位稍微重一点&#xff0c;晚上觉都睡不踏实。我前前后后折腾了大半年&#xff0c;试过手工加减仓…

作者头像 李华
网站建设 2026/9/10 8:01:46

CANN/GE图引擎MallocAdvise接口

MallocAdvise 【免费下载链接】ge GE&#xff08;Graph Engine&#xff09;是面向昇腾的图编译器和执行器&#xff0c;提供了计算图优化、多流并行、内存复用和模型下沉等技术手段&#xff0c;加速模型执行效率&#xff0c;减少模型内存占用。 GE 提供对 PyTorch、TensorFlow 前…

作者头像 李华