尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

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

JSTL依赖配置全解:版本对齐、Maven配置与部署排查 JSTL 标签库这个东西属于那种平时不用觉得无所谓一旦用上就再也不想回去写脚本片段的存在。它的依赖配置本身并不复杂但在 web 项目里翻车的概率高得离谱——jar 放进去了页面还是报The absolute uri ... cannot be resolvedMaven 里依赖写了却打不进 war 包换了台 Tomcat 就集体失灵这些场景我几乎每年都要碰上几次。问题的根子在于 JSTL 的依赖配置不是加一个 jar这么简单它同时牵扯到 Servlet 容器的版本、javax 与 jakarta 两套命名空间、JSP 标签库描述文件的 URI 映射以及 IDE 里编译期能识别和运行期能加载这两件经常被混为一谈的事。这篇内容会把这几条线捋清楚先讲版本对齐的对应关系再给 Maven 和非 Maven 两条路线的完整配置然后是部署验证、编译产物反查、报错速查最后是我在实际项目里踩出来的几个细节。不管你是刚在 IDEA 里建第一个 Java Web 项目还是在维护一个跑了七八年的老系统应该都能从中找到对得上的那一段。1. JSTL 到底解决什么问题为什么依赖配置总在同一个坑里翻车1.1 从一段被复制了八遍的循环说起早些年写 JSP列表渲染基本靠脚本片段堆出来。一个订单列表页开头要% page importjava.util.List %中间要% ListOrder list (ListOrder) 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} varorder一行顶替了上面那一整坨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/coreTomcat 9Servlet 4.0JSTL 1.2javaxhttp://java.sun.com/jsp/jstl/coreTomcat 10.1Servlet 6.0Jakarta JSTL 3.0jakartajakarta.tags.coreTomcat 11Servlet 6.1Jakarta JSTL 3.0jakartajakarta.tags.coreJetty 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 不能写providedprovided的语义是编译时需要运行时有容器提供而这里运行时容器不提供写provided的后果就是本地跑没问题、打包后必炸。顺带说一句很多人排查时会去翻 Tomcat 的lib目录发现里面确实有个跟标签相关的东西就想当然认为容器自带。实际情况是容器自带的只有 JasperJSP 引擎和 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.0pom 里需要两个依赖API 包和实现包。properties maven.compiler.source17/maven.compiler.source maven.compiler.target17/maven.compiler.target project.build.sourceEncodingUTF-8/project.build.sourceEncoding /properties dependencies !-- Servlet API容器提供scope 必须是 provided -- dependency groupIdjakarta.servlet/groupId artifactIdjakarta.servlet-api/artifactId version6.0.0/version scopeprovided/scope /dependency !-- JSTL API标签库的接口定义 -- dependency groupIdjakarta.servlet.jsp.jstl/groupId artifactIdjakarta.servlet.jsp.jstl-api/artifactId version3.0.0/version /dependency !-- JSTL 实现包含 tld 文件和具体标签实现类 -- dependency groupIdorg.glassfish.web/groupId artifactIdjakarta.servlet.jsp.jstl/artifactId version3.0.1/version /dependency /dependencies这里的关键点是 JSTL 的两个依赖都不能写provided或者runtime之外的奇怪 scope。API 包提供的是编译期用到的接口和常量实现包提供的是tld描述文件和运行时真正干活的类。有人图省事只写实现包编译期可能会报找不到jakarta.servlet.jsp.jstl.core.Config这类符号只写 API 包则运行时报类找不到。两个都要这是最稳的组合。还有一个容易被忽略的细节打包方式必须是war。如果 pom 里的packaging是默认的jarMaven 不会按 web 项目布局打包webapp/WEB-INF下的内容会全部丢掉JSTL 的 jar 自然也进不去。packagingwar/packaging3.2 为什么 scope 不能写 provided这是个值得单独讲的点。我见过不少项目里 JSTL 依赖被写成provided理由是标签库由服务器提供这个说法是错的而且错得很有迷惑性。provided在 Maven 里的准确含义是编译和测试阶段参与打包阶段排除适合那些容器确实会提供的依赖比如 Servlet API、JSP API。开发时用 IDEA 启动 Tomcat因为这些 API 在 Tomcat 的lib里存在所以页面能正常跑一旦真正打包部署到另一台服务器jar 被排除了容器又不提供标签立刻失效。判断某个依赖该不该写provided我有个简单的方法去目标容器的lib目录里找有没有同名 jar。Servlet API 有写providedJSTL 没有老老实实用默认的compile。这个动作大概十秒钟能省掉后面几小时的排查。另外要注意依赖冲突。如果项目里既引了 jakarta 版本的 JSTL又因为某个老依赖传递引进了 javax 版本的 JSTLwar 包里会同时存在两套 TLDJasper 扫描时可能匹配到错误的那一套症状是页面能打开但标签行为异常或者干脆抛出类转换异常。用mvn dependency:tree看清楚依赖树发现双重存在就加 exclusions 排掉。3.3 老项目Tomcat 8/9的写法与 jar 合并问题维护老项目的时候会遇到 JSTL 1.2它的坐标写法跟新版本完全不同dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.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.2JSP 里的 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/ # 第三方 jarJSTL 就放这里 │ │ └── 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.xmlServer 选择已经配置好的 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 LibraryIDEA 会创建一个库并把它加进当前模块的依赖里。做完这一步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 prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt % % taglib prefixfn urihttp://java.sun.com/jsp/jstl/functions %%-- Tomcat 10/11 的写法对应 Jakarta JSTL 3.0 --% % taglib prefixc urijakarta.tags.core % % taglib prefixfmt urijakarta.tags.fmt % % taglib prefixfn urijakarta.tags.functions %prefix是自己定的短名习惯上c表示 corefmt表示格式化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-urihttp://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 contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urijakarta.tags.core % % taglib prefixfmt urijakarta.tags.fmt % !DOCTYPE html html headtitle订单列表/title/head body table thead trth订单号/thth金额/thth下单时间/thth状态/th/tr /thead tbody c:forEach items${orders} varorder varStatusst tr class${st.index % 2 0 ? odd : even} td${order.orderNo}/td tdfmt:formatNumber value${order.amount} pattern#,##0.00//td tdfmt:formatDate value${order.createTime} patternyyyy-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} trtd colspan4暂无数据/td/tr /c:if /tbody /table /body /html这里面有几个细节值得单独说。varStatus提供循环状态st.index从 0 开始st.count从 1 开始st.first和st.last用于判断首尾做斑马纹和分隔符处理非常方便。c:choose相当于 switchc: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 keyorder.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 片段得显式写escapeXmlfalse而且要非常清楚这段 HTML 的来源是可信的。顺带提一下fn函数库处理字符串时特别顺手% taglib prefixfn urijakarta.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_BASEwork 目录在那个临时目录里而不是你安装目录下的 work。找这个临时目录有个简单办法看 IDEA 控制台启动 Tomcat 时打印的日志里面会有一行形如Using CATALINA_BASE: ...的信息把路径复制出来就能找到。也可以在 Tomcat 运行配置的 Server 标签页里看 Catalina base 的配置项不同 IDEA 版本叫法略有差异但指向的是同一个东西。知道这个位置之后很多疑难问题就变得可观测了。6.2 反查编译后的 Java 类假设页面index.jsp里写了c:forEach items${orders} varorder编译后在 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 vardetailUrl c:param nameid 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-onlytrue/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或页面指令里有没有isELIgnoredtrue老项目从 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.0Tomcat 9 配4.0。改这一行有时候能解决一些说不上来的诡异问题。最后留一个扩展方向。JSTL 在纯 JSP 项目里很顺手但如果项目已经上了前后端分离视图层交给前端框架JSTL 的使用场景会大幅收缩。真要继续用 JSP可以考虑把公共的头部、尾部、分页组件抽成.tag文件或者独立的 JSP 片段用jsp:include组合比在每个页面里重复写c:forEach分页逻辑要省事得多。这是我维护了几个老系统之后觉得性价比最高的一次重构动作。
返回列表