news 2026/10/1 5:06:23

JSTL依赖配置全解:版本对齐、Maven配置与部署排查

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JSTL依赖配置全解:版本对齐、Maven配置与部署排查

JSTL 标签库这个东西,属于那种"平时不用觉得无所谓,一旦用上就再也不想回去写脚本片段"的存在。它的依赖配置本身并不复杂,但在 web 项目里翻车的概率高得离谱——jar 放进去了页面还是报The absolute uri ... cannot be resolved,Maven 里依赖写了却打不进 war 包,换了台 Tomcat 就集体失灵,这些场景我几乎每年都要碰上几次。问题的根子在于 JSTL 的依赖配置不是"加一个 jar"这么简单,它同时牵扯到 Servlet 容器的版本、javax 与 jakarta 两套命名空间、JSP 标签库描述文件的 URI 映射,以及 IDE 里"编译期能识别"和"运行期能加载"这两件经常被混为一谈的事。这篇内容会把这几条线捋清楚:先讲版本对齐的对应关系,再给 Maven 和非 Maven 两条路线的完整配置,然后是部署验证、编译产物反查、报错速查,最后是我在实际项目里踩出来的几个细节。不管你是刚在 IDEA 里建第一个 Java Web 项目,还是在维护一个跑了七八年的老系统,应该都能从中找到对得上的那一段。

1. JSTL 到底解决什么问题,为什么依赖配置总在同一个坑里翻车

1.1 从一段被复制了八遍的循环说起

早些年写 JSP,列表渲染基本靠脚本片段堆出来。一个订单列表页,开头要<%@ page import="java.util.List" %>,中间要<% List<Order> list = (List<Order>) request.getAttribute("orders"); %>,然后if (list != null && !list.isEmpty())包一层,再for (int i = 0; i < list.size(); i++)里面写<%= list.get(i).getOrderNo() %>。这段代码写一遍还行,问题是它会在同一个项目里出现几十遍,每换一个实体类就要重写一遍循环结构,空集合判断还会漏掉。更麻烦的是页面一旦复杂起来,脚本里混着 HTML,改一个字段名要在几百行里找getXXX(),做前端的人根本不敢碰。

JSTL 就是把这个套路抽成标签。<c:forEach items="${orders}" var="order">一行顶替了上面那一整坨,<c:if>、<c:choose>把判断也标签化,<fmt:formatDate>负责日期格式化,<fn:length>负责字符串和集合的长度计算。写完之后 JSP 页面里几乎看不到<% %>,前端同学可以自己调整结构,后端只关心把数据塞进 request 域。这是它最核心的价值:把视图层从 Java 代码里剥离出来,让页面回归页面。

1.2 依赖配置翻车的三种典型现场

第一个现场最常见:jar 明明拷进了WEB-INF/lib,访问页面还是抛异常,提示找不到某个绝对 URI。这种情况九成是 jar 的位置或打包方式有问题,比如放在了项目根目录的lib而不是WEB-INF/lib,或者 IDEA 的 Artifact 输出布局里没有把这个库纳进去,编辑器里不报错是因为模块依赖里有,但编译出来的 war 包里没有。

第二个现场更隐蔽:页面能跑,某些标签却报ClassNotFoundException。这通常意味着只引了 API 包没有引实现包,JSTL 1.1 时代 API 和实现是分开的两个 jar,少一个就只在用到具体标签时才炸,平时c:out可能还正常,一用c:forEach就挂。

第三个现场是升级容器之后全线崩溃。项目原来跑在 Tomcat 8 上好好的,换成 Tomcat 10 之后所有 JSTL 标签全部失效,报错信息还是那个熟悉的 URI 找不到。根因是 Tomcat 10 开始全面转向 jakarta 命名空间,旧的javax.servlet.jsp.jstl包名容器根本不认,必须换成 jakarta 版本的 JSTL,连 JSP 里的 taglib URI 都要一起改。这三个现场串起来其实就是一句话:JSTL 的依赖配置是版本、打包、URI 三件事的联合体,缺一环都不行。

2. 版本对齐:Servlet 容器、JSTL、命名空间的对应关系

2.1 javax 到 jakarta 的分水岭

Java EE 移交给 Eclipse 基金会之后改名叫 Jakarta EE,随之而来的是包名从javax.*整体迁移到jakarta.*。这个迁移不是改个名字那么简单,它对 JSTL 的影响体现在两个层面:一是实现类的包路径变了,二是标签库描述文件里声明的 URI 变了。Tomcat 作为最常用的 Servlet 容器,从 10.0 版本开始完全按 Jakarta EE 9 及以上规范实现,也就是说 Tomcat 10、11 只认jakarta.servlet.*,而 Tomcat 8.5、9 认的是javax.servlet.*。JSTL 作为运行在容器上的标签库,必须和服务器的命名空间保持一致,否则容器扫描 TLD 时压根匹配不上。

这条分界线在实际操作中的表现非常直接:如果你在 Tomcat 10 上部署一个用javax.servlet:jstl:1.2的项目,页面上所有 JSTL 标签都会失效,控制台抛出The absolute uri: http://java.sun.com/jsp/jstl/core cannot be resolved。反过来,在 Tomcat 9 上用 jakarta 版本的 JSTL,同样跑不起来。所以选型第一步不是挑版本号,而是先确认手里的 Tomcat 是第几代。

2.2 版本对照表与选型建议

把常用的组合整理成一张表,照着抄基本不会出错:

Servlet 容器Servlet 规范JSTL 版本命名空间taglib URI 前缀
Tomcat 8.5Servlet 3.1JSTL 1.2javaxhttp://java.sun.com/jsp/jstl/core
Tomcat 9Servlet 4.0JSTL 1.2javaxhttp://java.sun.com/jsp/jstl/core
Tomcat 10.1Servlet 6.0Jakarta JSTL 3.0jakartajakarta.tags.core
Tomcat 11Servlet 6.1Jakarta JSTL 3.0jakartajakarta.tags.core
Jetty 11 / 12Servlet 5.0 / 6.0Jakarta JSTL 3.0jakartajakarta.tags.core

选型上有两条建议值得记住。新建项目一律从 Tomcat 10.1 起步,直接上 Jakarta JSTL 3.0,不要为了图省事去挑老版本 Tomcat,后面升级更痛苦。维护老项目就老老实实跟着原来的容器走,别单独把 JSTL 升上去,容器和标签库必须是配套的。另外要注意 Spring Boot 内嵌 Tomcat 的情况,Spring Boot 3.x 用的是 Tomcat 10.x,同样是 jakarta 命名空间,如果 JSP 页面里写的是旧 URI,在 Spring Boot 3 里一样会挂。

2.3 Tomcat 为什么不自带 JSTL

经常有人问,Servlet API 是 Tomcat 提供的,为什么 JSTL 不一起提供?原因是 Tomcat 把自己定位成 Servlet 容器,只实现规范强制要求的部分,JSP 标准标签库属于可选组件,不在容器的实现范围内。历史上只有少数应用服务器(比如某些商业容器)会把 JSTL 打进去,Tomcat 从始至终都没有带。这也解释了为什么 JSTL 的依赖 scope 不能写provided:provided的语义是"编译时需要,运行时有容器提供",而这里运行时容器不提供,写provided的后果就是本地跑没问题、打包后必炸。

顺带说一句,很多人排查时会去翻 Tomcat 的lib目录,发现里面确实有个跟标签相关的东西,就想当然认为容器自带。实际情况是容器自带的只有 Jasper(JSP 引擎)和 Servlet/JSP API 的实现,TLD 扫描机制是有的,但具体的c.tld、fmt.tld这些描述文件必须由 JSTL 的 jar 提供。扫描机制和标签库本身是两回事,混淆这两个概念会让人在排查时完全找错方向。

3. Maven 项目中的 JSTL 依赖配置

3.1 现代栈(Tomcat 10/11)的 pom 写法

现在新建项目基本都会用 Maven 或 Gradle 管理依赖,这是最省心的路线。Tomcat 10.1 及以上配合 Jakarta JSTL 3.0,pom 里需要两个依赖:API 包和实现包。

<properties> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <!-- Servlet API:容器提供,scope 必须是 provided --> <dependency> <groupId>jakarta.servlet</groupId> <artifactId>jakarta.servlet-api</artifactId> <version>6.0.0</version> <scope>provided</scope> </dependency> <!-- JSTL API:标签库的接口定义 --> <dependency> <groupId>jakarta.servlet.jsp.jstl</groupId> <artifactId>jakarta.servlet.jsp.jstl-api</artifactId> <version>3.0.0</version> </dependency> <!-- JSTL 实现:包含 tld 文件和具体标签实现类 --> <dependency> <groupId>org.glassfish.web</groupId> <artifactId>jakarta.servlet.jsp.jstl</artifactId> <version>3.0.1</version> </dependency> </dependencies>

这里的关键点是 JSTL 的两个依赖都不能写provided或者runtime之外的奇怪 scope。API 包提供的是编译期用到的接口和常量,实现包提供的是tld描述文件和运行时真正干活的类。有人图省事只写实现包,编译期可能会报找不到jakarta.servlet.jsp.jstl.core.Config这类符号;只写 API 包则运行时报类找不到。两个都要,这是最稳的组合。

还有一个容易被忽略的细节:打包方式必须是war。如果 pom 里的<packaging>是默认的jar,Maven 不会按 web 项目布局打包,webapp/WEB-INF下的内容会全部丢掉,JSTL 的 jar 自然也进不去。

<packaging>war</packaging>

3.2 为什么 scope 不能写 provided

这是个值得单独讲的点。我见过不少项目里 JSTL 依赖被写成provided,理由是"标签库由服务器提供",这个说法是错的,而且错得很有迷惑性。provided在 Maven 里的准确含义是"编译和测试阶段参与,打包阶段排除",适合那些容器确实会提供的依赖,比如 Servlet API、JSP API。开发时用 IDEA 启动 Tomcat,因为这些 API 在 Tomcat 的lib里存在,所以页面能正常跑;一旦真正打包部署到另一台服务器,jar 被排除了,容器又不提供,标签立刻失效。

判断某个依赖该不该写provided,我有个简单的方法:去目标容器的lib目录里找有没有同名 jar。Servlet API 有,写provided;JSTL 没有,老老实实用默认的compile。这个动作大概十秒钟,能省掉后面几小时的排查。

另外要注意依赖冲突。如果项目里既引了 jakarta 版本的 JSTL,又因为某个老依赖传递引进了 javax 版本的 JSTL,war 包里会同时存在两套 TLD,Jasper 扫描时可能匹配到错误的那一套,症状是页面能打开但标签行为异常,或者干脆抛出类转换异常。用mvn dependency:tree看清楚依赖树,发现双重存在就加 exclusions 排掉。

3.3 老项目(Tomcat 8/9)的写法与 jar 合并问题

维护老项目的时候会遇到 JSTL 1.2,它的坐标写法跟新版本完全不同:

<dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency>

这个坐标只需要一个依赖,因为它本身已经是一个合并包,里面同时包含 API 和实现。但在网上搜教程的时候,你会看到大量文章让你再加一个taglibs:standard:1.1.2,这是 JSTL 1.1 时代的做法,那时候jstl.jar只有 API,实现放在standard.jar里,两个一起用。到了 JSTL 1.2 官方把两者合并了,如果还把standard-1.1.2.jar一起放进去,两个 jar 里都有org.apache.taglibs.standard包下的类,类加载顺序不确定,轻则出现行为诡异,重则抛LinkageError。

这里给一条经验:判断项目用的是哪个版本,看WEB-INF/lib里 jar 的文件名和大小。jstl-1.2.jar通常在 400KB 上下且同时含有javax/servlet/jsp/jstl和org/apache/taglibs/standard两个目录;jstl.jar只有几十 KB 且只有 API 目录。后者必须配standard.jar,前者千万别再配。把这个规则记住,能避免一大类疑难杂症。

还有一点,老项目如果从 JSTL 1.1 迁到 1.2,JSP 里的 taglib URI 不需要改,两个版本对外暴露的 URI 是一样的,改的只是依赖坐标和删掉多余的standard.jar。

4. 非 Maven 项目:手工摆 jar 与 IDEA 里的两处配置

4.1 Java Web 项目的标准目录结构

不用构建工具的项目,目录结构就变得格外重要,因为没有任何自动机制帮你把依赖塞到正确的位置。标准的 Java Web 目录结构长这样:

myweb/ ├── src/ # Java 源码,编译后进 WEB-INF/classes │ └── com/example/... ├── webapp/ # 或者叫 WebContent、WebRoot │ ├── WEB-INF/ │ │ ├── web.xml # 部署描述符 │ │ ├── lib/ # 第三方 jar,JSTL 就放这里 │ │ └── classes/ # 编译输出 │ ├── static/ │ │ └── css/app.css │ └── index.jsp └── ...

WEB-INF这个目录有两层含义:一是里面的内容客户端无法直接访问,属于安全边界;二是lib和classes目录对类加载器有特殊意义,容器启动时会自动把它们加入应用的类路径。所以 JSTL 的 jar 必须放在webapp/WEB-INF/lib下,放在项目根目录自己建的lib里是无效的,容器根本不会去扫。我在面试和带新人的时候,见过太多人把 jar 放在webapp/lib(少了一层 WEB-INF),结果排查半天。

4.2 IDEA 2024/2025 建 web 项目并加入 JSTL

先说创建项目。IDEA Ultimate 版里,File → New → Project,左侧选 Jakarta EE(部分版本仍显示 Java Enterprise),右侧勾选 Web application,同时选中 Create web.xml,Server 选择已经配置好的 Tomcat。IDEA 2024 之后的模板里,Additional Libraries 一栏通常只有 Servlet API 之类的选项,JSTL 一般不在列表里,需要自己补。创建完成后项目里会自动生成webapp/WEB-INF/web.xml,还有一个 artifact 配置。

如果你用的是 Community 版,情况不太一样,社区版没有 Tomcat 的运行配置集成,也没有 Jakarta EE 模板。可行的做法是用maven-archetype-webapp原型创建 Maven 项目,再手动把 Tomcat 配到运行配置里(或者在 Maven 里配置一个容器插件来启动)。社区版也能开发,只是少了很多自动化,打包和部署要自己手动来。

加入 JSTL 的 jar 有两种途径。用 Maven 的话照第三章写 pom 就行,IDEA 会自动下载并识别。手工的话,把下载好的jstl-1.2.jar复制到webapp/WEB-INF/lib/目录下,然后在这个 jar 上右键选择 Add as Library,IDEA 会创建一个库并把它加进当前模块的依赖里。做完这一步,JSP 文件里的c:forEach之类的标签就不报红了。

4.3 Project Structure 里必须做两件事

只把 jar 放到WEB-INF/lib还不够,尤其是 Maven 项目。你要打开File → Project Structure,做这两个动作。

第一件事,确认模块依赖里有 JSTL。在 Modules → Dependencies 标签页里应该能看到对应的库,Maven 项目会自动带出来。这一层解决的是"编辑器认不认识这些标签",影响的是代码提示和编译期检查,跟运行时能不能跑没关系。

第二件事,确认 Artifact 的输出布局里有 JSTL 的 jar。切到 Artifacts 标签页,选中xxx:war exploded,右侧会看到输出布局树,展开WEB-INF/lib看看里面有没有 JSTL 的 jar。没有的话,从下方 Available Elements 里找到对应的库,双击加到WEB-INF/lib节点下,然后 Apply。这一层解决的才是"打包时会不会带上",是真正决定部署后能不能跑的一步。

这里有个特别容易踩的坑:Maven 项目里,providedscope 的依赖不会出现在 Artifacts 的 Available Elements 里,因为它本来就不该被打包;而compilescope 的依赖默认会出现在 WEB-INF/lib 下,但如果你之前手动调整过布局,可能会被误删,之后就一直是"本地能跑、打包就炸"。所以每次动了依赖相关配置,最好都去看一眼这个输出布局,养成习惯。

5. taglib 声明与第一个能跑的 JSP 页面

5.1 两种 URI 写法,别混着用

JSTL 的使用从 JSP 顶部的 taglib 指令开始。javax 时代的写法和 jakarta 时代完全不同,必须和依赖版本对应:

<%-- Tomcat 8/9 的写法,对应 JSTL 1.2 --%> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> <%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
<%-- Tomcat 10/11 的写法,对应 Jakarta JSTL 3.0 --%> <%@ taglib prefix="c" uri="jakarta.tags.core" %> <%@ taglib prefix="fmt" uri="jakarta.tags.fmt" %> <%@ taglib prefix="fn" uri="jakarta.tags.functions" %>

prefix是自己定的短名,习惯上c表示 core,fmt表示格式化,fn表示函数,保持这个约定能让别人一眼看懂。URI 是标签库描述文件里声明的唯一标识,容器启动时会扫描所有 jar 里的 tld 文件,把这些 URI 登记进一张映射表,JSP 编译时按 URI 去查表找到对应的实现类。URI 写错了或者 jar 没扫到,查表失败,就会抛出那个经典的绝对 URI 无法解析的异常。

Jakarta JSTL 3.0 里旧的http://java.sun.com/...形式的 URI 已经不再默认注册了,网上那些粘贴过来的老代码直接放到新项目里必然报错。有个应急办法是在web.xml里显式做映射:

<jsp-config> <taglib> <taglib-uri>http://java.sun.com/jsp/jstl/core</taglib-uri> <taglib-location>/WEB-INF/tlds/c.tld</taglib-location> </taglib> </jsp-config>

做法是把 jar 里的c.tld抽出来放到WEB-INF/tlds下,然后手动建立 URI 到文件的映射。我不太推荐长期这么干,因为它掩盖了版本不一致的本质问题,正确做法还是把 JSP 里的 URI 改成新形式。

5.2 核心标签实战:遍历、判断、格式化

先看一个列表渲染的完整例子,这是使用频率最高的场景:

<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="jakarta.tags.core" %> <%@ taglib prefix="fmt" uri="jakarta.tags.fmt" %> <!DOCTYPE html> <html> <head><title>订单列表</title></head> <body> <table> <thead> <tr><th>订单号</th><th>金额</th><th>下单时间</th><th>状态</th></tr> </thead> <tbody> <c:forEach items="${orders}" var="order" varStatus="st"> <tr class="${st.index % 2 == 0 ? 'odd' : 'even'}"> <td>${order.orderNo}</td> <td><fmt:formatNumber value="${order.amount}" pattern="#,##0.00"/></td> <td><fmt:formatDate value="${order.createTime}" pattern="yyyy-MM-dd HH:mm"/></td> <td> <c:choose> <c:when test="${order.status == 1}">待支付</c:when> <c:when test="${order.status == 2}">已支付</c:when> <c:otherwise>已关闭</c:otherwise> </c:choose> </td> </tr> </c:forEach> <c:if test="${empty orders}"> <tr><td colspan="4">暂无数据</td></tr> </c:if> </tbody> </table> </body> </html>

这里面有几个细节值得单独说。varStatus提供循环状态,st.index从 0 开始,st.count从 1 开始,st.first和st.last用于判断首尾,做斑马纹和分隔符处理非常方便。c:choose相当于 switch,c:when依次匹配,c:otherwise兜底,比嵌套c:if清晰得多。fmt:formatNumber的pattern属性用的是 Java 的 DecimalFormat 语法,千分位和精度都能精确控制;fmt:formatDate用的是 SimpleDateFormat 语法,注意它要求value是java.util.Date类型,如果传的是LocalDateTime会直接报错,这是 Java 8 之后非常常见的坑,解决办法是在后端转成 Date 或者用函数标签自己格式化。

fmt库还有一个容易被忽略的用法是国际化。配置资源文件之后,<fmt:setLocale>切换语言,<fmt:setBundle>绑定资源包,<fmt:message key="order.title"/>按 key 取文案。做多语言站点的时候,这套标签比自己在后端拼字符串要干净得多,而且切换语言只需要改setLocale一处。

5.3 c:out 与转义:不要直接输出用户输入

${order.remark}这种写法会把内容原样输出到 HTML 里,如果 remark 里包含用户提交的脚本片段,会直接被执行,这是最典型的跨站脚本问题。JSTL 的c:out默认会做 XML 转义:

<c:out value="${order.remark}" default="无备注"/>

default属性在值为 null 时输出兜底文案,比写order.remark == null ? '无备注' : order.remark干净。但要注意escapeXml的默认值是 true,如果你确实需要输出一段后端拼好的 HTML 片段,得显式写escapeXml="false",而且要非常清楚这段 HTML 的来源是可信的。

顺带提一下fn函数库,处理字符串时特别顺手:

<%@ taglib prefix="fn" uri="jakarta.tags.functions" %> 长度:${fn:length(order.items)} 截断:${fn:substring(order.title, 0, 20)} 包含:${fn:contains(order.remark, '加急')}

fn:length对字符串、集合、数组都有效,比在页面里写list.size()灵活。不过要注意,函数标签在 JSTL 里是通过 EL 函数映射实现的,如果容器版本和 JSTL 版本不匹配,可能出现函数不可用的情况,排查思路和前面说的一样,先看 jar 再看 URI。

6. 部署到 Tomcat 后的验证:看编译产物确认标签真的被解析了

6.1 work 目录与 CATALINA_BASE 的关系

JSP 不是解释执行的,第一次访问时会被 Jasper 编译成一个 Servlet 的 Java 文件,再编译成 class 加载。这个中间产物就放在 Tomcat 的 work 目录下,路径规则是work/Catalina/localhost/<应用上下文>/org/apache/jsp/。如果是 IDEA 里启动的 Tomcat,情况稍有不同,IDEA 会创建一个临时的 CATALINA_BASE,work 目录在那个临时目录里,而不是你安装目录下的 work。

找这个临时目录有个简单办法:看 IDEA 控制台启动 Tomcat 时打印的日志,里面会有一行形如Using CATALINA_BASE: ...的信息,把路径复制出来就能找到。也可以在 Tomcat 运行配置的 Server 标签页里看 Catalina base 的配置项,不同 IDEA 版本叫法略有差异,但指向的是同一个东西。知道这个位置之后,很多疑难问题就变得可观测了。

6.2 反查编译后的 Java 类

假设页面index.jsp里写了<c:forEach items="${orders}" var="order">,编译后在 work 目录下会生成index_jsp.java,里面能看到 Jasper 为每个标签生成的私有方法,名字类似_jspx_meth_c_005fforEach_005f0,方法体里会直接 new 出标签的实现类:

private boolean _jspx_meth_c_005fforEach_005f0(JspTag _jspx_th_c_005fforEach_005f0, PageContext _jspx_page_context) throws Throwable { JspWriter out = _jspx_page_context.getOut(); ForEachTag _jspx_th_c_005fforEach_005f0 = (ForEachTag) _jspx_th_c_005fforEach_005f0; _jspx_th_c_005fforEach_005f0.setItems((java.lang.Object) _jspx_page_context.findAttribute("orders")); _jspx_th_c_005fforEach_005f0.setVar("order"); _jspx_th_c_005fforEach_005f0.doStartTag(); // ... }

看到这段代码,说明标签已经被正确解析、实现类也已经加载成功了。如果标签没生效,你会在生成的 Java 文件里看到那段<c:forEach>被当成普通文本原样输出,或者干脆编译失败并抛出无法解析 URI 的异常。这个反查手法在我排查"页面直接显示标签源码"这类怪问题时特别管用——看到 Java 文件里标签变成了out.write("<c:forEach ..."),就能确认是 taglib 指令没生效,而不是浏览器缓存的问题。

还有个小技巧:如果生成的 Java 文件是旧的,可以把 work 目录里的对应应用目录整个删掉再重新访问,强制重新编译。改完 JSP 之后页面没变化,八成也是缓存或增量编译的问题,清一次 work 目录基本就解决了。

6.3 前端挂 nginx、后端多项目时的路径问题

生产环境常见的部署形态是 nginx 在前,Tomcat 在后,一台服务器上跑好几个 web 项目,用不同的路径前缀区分。比如 nginx 里配置/shop/转发到http://127.0.0.1:8080/shop/,/crm/转发到http://127.0.0.1:8081/。这种结构下 JSTL 页面里最忌讳的就是硬编码绝对路径。

正确做法是用c:url生成链接,它会自动带上当前应用的上下文路径:

<c:url value="/order/detail" var="detailUrl"> <c:param name="id" value="${order.id}"/> </c:url> <a href="${detailUrl}">查看详情</a>

如果直接写href="/order/detail",在 nginx 的反向代理下会被解析到根路径,请求打到 nginx 之后没法正确转发到对应的应用。用c:url生成的路径是带上下文的,能规避这类问题,同时<c:param>会自动做 URL 编码,中文和特殊字符都不用手动处理。

另一个坑是会话 Cookie 的路径。Tomcat 默认把 JSESSIONID 的 path 设置为应用的上下文路径,如果 nginx 暴露的路径和上下文路径不一致,浏览器可能不会把 Cookie 带到目标地址,表现为"登录成功后跳转又变回未登录"。解决办法是在web.xml里显式指定 Cookie 路径:

<session-config> <cookie-config> <path>/</path> <http-only>true</http-only> </cookie-config> </session-config>

把路径设成根路径之后,同域名下的所有子路径都能带上这个 Cookie。http-only打开能防止脚本读取会话标识,是个低成本的加固手段。另外如果 nginx 做了静态资源直出,注意别把WEB-INF目录暴露出去,最好在 nginx 里显式拒绝访问以/WEB-INF/开头的路径。

7. 报错速查与排查顺序

7.1 四类高频报错的具体处理

绝对 URI 无法解析,报错信息通常是The absolute uri: ... cannot be resolved。按这个顺序查:jar 是否在WEB-INF/lib下、jar 版本和容器命名空间是否匹配、JSP 里的 URI 写法是否对应这个版本、打包产物里是否真的包含这个 jar。四条对完基本能定位。

找不到实现类,形如ClassNotFoundException: org.apache.taglibs.standard.tag.rt.core.ForEachTag。这基本就是缺实现包,检查是不是只引了 API 包,或者 jar 被误设成了provided。

页面把标签当文本输出,浏览器里直接看到<c:forEach ...>的源码。这通常是 taglib 指令缺失或写错,也可能是 JSP 配置里 EL 表达式被禁用了。检查web.xml或页面指令里有没有isELIgnored="true",老项目从 JSP 1.2 规范升级过来时容易出现这个问题。

中文输出乱码,fmt:formatDate出来的月份变成问号,或者页面编码不对。检查三个地方:JSP 页面头部的pageEncoding和contentType里的 charset、项目源码编码(IDEA 设置里 File Encodings 统一成 UTF-8)、Tomcat 连接器的 URIEncoding。

7.2 排查速查表

现象最可能的原因处理动作
绝对 URI 无法解析jar 没打进 war 包检查 Artifacts 输出布局的 WEB-INF/lib
所有标签失效且换了容器命名空间不匹配换对应的 JSTL 版本并改 URI
只有部分标签报类找不到缺少实现包补上实现依赖
页面源码中出现标签文本taglib 指令缺失或 EL 被禁用补指令、清 work 目录
本地正常、服务器失效依赖 scope 写成 provided改成默认 compile
日期格式化报类型错误传入 LocalDateTime后端转成 java.util.Date
循环里内容错位var 属性名和 EL 表达式不一致逐行核对属性名

7.3 我自己固定的排查顺序

遇到 JSTL 相关问题,我现在基本按固定顺序走,一般不超十分钟。第一步看 war 包里的实际内容,用解压工具打开WEB-INF/lib,这一步能直接排掉百分之六十的"打包没带上"问题。第二步看容器版本和 jar 的对应关系,确认命名空间一致。第三步清 work 目录,重新访问页面,看编译生成的 Java 文件里标签到底变成了什么。第四步才是去看依赖树,排查有没有版本冲突。这个顺序的原则是:先确认事实,再推理原因,别一上来就猜。

8. 几个我踩过之后才记住的细节

第一个细节是关于 IDEA 的 red code 和运行时错误的区别。IDEA 里 JSP 标签报红,说明模块依赖里没有对应的库,影响的是开发体验;而运行时能不能跑,取决于 war 包里有没有 jar。这两件事互相独立,很多人看到编辑器不报红就以为万事大吉,结果打包之后才发现问题。我的习惯是:编辑器里红不红先不管,打包之后一定解压看一眼WEB-INF/lib,以那个为准。

第二个细节是别在WEB-INF/lib里同时放多个版本的 JSTL。有人在升级过程中图省事,新 jar 拷进去旧的没删,容器扫描 TLD 时先扫到哪个不确定,行为随机,可能今天正常明天就出问题。清理依赖的时候一定要下狠手,只留一个版本。

第三个细节关于web.xml的版本声明。Servlet 3.0 以上的规范支持注解和更宽松的配置,如果web.xml的version属性还是 2.3,容器的行为会退化到很古老的模式,一些默认配置会变,标签库的扫描策略也可能受影响。新建项目建议直接用匹配容器规范的版本,比如 Tomcat 10.1 配6.0,Tomcat 9 配4.0。改这一行有时候能解决一些说不上来的诡异问题。

最后留一个扩展方向。JSTL 在纯 JSP 项目里很顺手,但如果项目已经上了前后端分离,视图层交给前端框架,JSTL 的使用场景会大幅收缩。真要继续用 JSP,可以考虑把公共的头部、尾部、分页组件抽成.tag文件或者独立的 JSP 片段,用<jsp:include>组合,比在每个页面里重复写c:forEach分页逻辑要省事得多。这是我维护了几个老系统之后,觉得性价比最高的一次重构动作。

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

200K上下文实战指南:Qwen2.5+TGI+PDF2Markdown长文本处理全栈方案

1. 这不是“平替”&#xff0c;是重新定义长文本处理边界的实战方案最近在几个技术社群里&#xff0c;频繁看到有人发截图&#xff1a;“升级&#xff01;ChatGPT4.0最强平替&#xff0c;可处理200k上下文”——标题很抓眼球&#xff0c;但点进去发现要么是模糊的演示视频&…

作者头像 李华
网站建设 2026/10/1 5:04:39

企业AI落地关键:QuickBlue应用底座如何连接、编排与治理

咱们直接聊点实际的&#xff1a;最近我团队在给几家制造业和零售客户搭AI应用底座&#xff0c;几乎每一家都会问同一个问题——“我们到底要不要自己弄一个QuickBlue这样的东西&#xff1f;还是直接调大模型API就完事了&#xff1f;”我的回答永远是&#xff1a;如果你只想做个…

作者头像 李华
网站建设 2026/10/1 5:03:17

Vivado增量实现实战:复用布局布线,加速FPGA时序收敛

做 FPGA 的人对下面这个场景应该都不陌生&#xff1a;一个 30 万 LUT 的工程&#xff0c;综合加实现一次要跑八个多小时&#xff0c;结果你只改了两行状态机代码&#xff0c;或者只是把某个计数器的位宽从 16 调到 17 位&#xff0c;整条流程又得从头再来一遍。等一晚上&#x…

作者头像 李华
网站建设 2026/10/1 5:02:45

基于8300张头盔检测数据集的YOLO目标检测全流程实战

1. 8300张头盔检测数据集到底能解决什么实际问题第一次拿到这个数据集的时候&#xff0c;我脑子里冒出来的第一个念头不是"怎么训模型"&#xff0c;而是"这8300张图到底覆盖了多少种真实路况"。做过智慧交通项目的人都知道&#xff0c;头盔检测这个任务看起…

作者头像 李华
网站建设 2026/10/1 5:02:20

LLM工业落地:十个值得做的应用场景与工程实践

LLM这波浪潮在办公协同、代码生成、内容创作这些线上场景里已经卷出花了&#xff0c;但真正往工厂车间、产线设备、工艺配方这些硬骨头场景里扎的&#xff0c;其实还处在很早期的阶段。我过去一年多接触了不少制造企业做AI落地的项目&#xff0c;说实话&#xff0c;PPT上“AI赋…

作者头像 李华
网站建设 2026/10/1 5:02:13

Claude Code 接入第三方 API 全攻略:DeepSeek、Qwen、GLM 配置指南

1. 为什么我要折腾 Claude Code 桌面版接入第三方 APIClaude Code 刚出那阵子&#xff0c;我身边不少朋友第一反应是“这玩意儿是不是又得订阅”。确实&#xff0c;官方默认走的是订阅账号体系&#xff0c;但它的底层其实是一个标准的 API 客户端&#xff0c;只要你能给它一个兼…

作者头像 李华