ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

JSTL依赖配置与taglib排错:javax/jakarta版本选型实战

JSTL依赖配置与taglib排错:javax/jakarta版本选型实战 1. 先把 JSTL 依赖配置这件事拆开看JSTLJSP Standard Tag Library这个词只要写过 JSP 页面的人应该都不陌生。它的定位很明确把 JSP 里那些原本要靠% %脚本片段才能完成的循环、判断、格式化、URL 拼接等操作换成标签形式让页面回归模板的角色而不是一堆 Java 代码和 HTML 混在一起。所谓JSTL 依赖配置说白了就是三件事把正确的 jar 放进项目的依赖里、让这些 jar 在打包后确实出现在WEB-INF/lib下、在 JSP 里用正确的taglib指令把标签库引进来。三件事里任意一件没做对页面就会报 500或者更隐蔽地——标签被当成纯文本原样输出到浏览器上。1.1 JSTL、EL、JSP 三者的分工别搞混很多新手会把这三样东西当成一个东西结果排查问题时方向就错了。JSP 是页面模板技术本身由容器比如 Tomcat 里的 Jasper 引擎负责把.jsp编译成.java再编译成.classEL 表达式是 JSP 规范的一部分负责${}这种取值和运算实现通常在容器的 EL 组件里Tomcat 8 以后是自带的而 JSTL 是一个独立的标签库实现它不是 JSP 规范强制要求容器提供的容器不会白送你。这就是为什么你新建一个 Web 项目${user.name}能正常输出但c:forEach一写就报错——EL 是容器给的JSTL 得你自己加依赖。搞清楚这一点后面所有的排查思路都会顺报错说 URI 解析不了那就是 jar 没进来或者 taglib 声明写错了报错说NoClassDefFoundError那就是类没找到多半是只引了 API 包没引实现包或者 scope 配成了provided导致 war 里没有这个 jar。1.2 为什么依赖配置是 JSTL 出问题的高发区JSTL 有一个特殊性它经历过一次改名换姓的大迁移。早期是javax.*命名空间Jakarta EE 9 之后整体迁到了jakarta.*。这次迁移不像 Servlet API 那样只是换个包名JSTL 连 Maven 坐标、jar 结构、甚至 taglib 的 URI 都变了。你在网上搜到的教程可能是 2015 年写的用的是javax.servlet:jstl:1.2而你的项目用的是 Tomcat 10.1 甚至 11跑的是 Servlet 6.x两个世界的东西放一起必然炸。更麻烦的是两种方案的报错信息长得很像都是absolute uri cannot be resolved你不会第一时间意识到是命名空间代际冲突。再加上 Maven 的 scope 机制、IDEA 的 Artifact 配置、war 打包时依赖是否被包含这些环节每一层都可能悄悄把 jar 丢掉。所以这篇内容我打算按选型 → 配依赖 → 写页面 → 部署验证 → 排错的顺序完整走一遍把这几个环节里容易踩的坑都摊开说清楚。2. 版本谱系与坐标选型javax 还是 jakarta选版本这件事没有回旋余地判断依据只有一个你的容器运行的是哪个 Servlet 规范版本。容器定了命名空间就定了JSTL 的坐标和 URI 也就跟着定了。反过来推是行不通的——不能说我想要新语法就硬上 Jakarta 版 JSTL容器不认就是不认。2.1 javax 时代单 jar 与拆分后的 api/impl早期最省事的做法是引一个全家桶 jardependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency这个坐标产出的就是大名鼎鼎的jstl-1.2.jar里面同时包含了 API 接口、实现类包名org.apache.taglibs.standard和 TLD 描述文件一个 jar 解决所有问题。缺点是版本停在 1.2 很久没动而且无法单独升级实现。后来官方把它拆成了两个包API 和实现分离dependency groupIdjavax.servlet.jsp.jstl/groupId artifactIdjstl-api/artifactId version1.2.7/version /dependency dependency groupIdorg.glassfish.web/groupId artifactIdjstl-impl/artifactId version1.2.7/version /dependency拆分的好处是升级灵活坏处是只引了jstl-api而忘了jstl-impl的人一下子多了起来典型症状是启动没问题、页面一访问就ClassNotFoundException: org.apache.taglibs.standard.tag.rt.core.ForEachTag。前面说的只引 API 没引实现就是指这个。2.2 jakarta 时代2.0 与 3.0 的关键差异Jakarta 命名空间下JSTL 的坐标变成了dependency groupIdjakarta.servlet.jsp.jstl/groupId artifactIdjakarta.servlet.jsp.jstl-api/artifactId version3.0.0/version /dependency dependency groupIdorg.glassfish.web/groupId artifactIdjakarta.servlet.jsp.jstl/artifactId version3.0.1/version /dependency注意实现包的artifactId这次把impl后缀去掉了直接叫jakarta.servlet.jsp.jstl很容易和 API 的坐标看串行复制粘贴的时候多核对一眼。这里有个非常关键的坑JSTL 2.0 和 3.0 都跑在 Jakarta 命名空间下但 taglib URI 不一样。2.0 为了兼容老页面仍然沿用http://java.sun.com/jsp/jstl/core这套老 URI到了 3.0URI 改成了jakarta.tags.core。也就是说如果你的依赖是 3.0.x页面里却还写着老 URIJasper 编译时会直接抛异常提示 URI 无法解析。很多人升级依赖之后忘了改页面就被这一条卡住。2.3 Tomcat 版本与 JSTL 方案对照下面这张表建议直接存下来选型时对着查比在网上翻帖子靠谱容器版本Servlet 规范JSP 规范命名空间推荐 JSTL 坐标taglib URITomcat 8.53.12.3javaxjavax.servlet:jstl:1.2http://java.sun.com/jsp/jstl/coreTomcat 94.02.3javaxjavax.servlet.jsp.jstl:jstl-api:1.2.7 org.glassfish.web:jstl-impl:1.2.7http://java.sun.com/jsp/jstl/coreTomcat 10.05.03.0jakartajakarta.servlet.jsp.jstl-api:2.0.0 org.glassfish.web:jakarta.servlet.jsp.jstl:2.0.0http://java.sun.com/jsp/jstl/coreTomcat 10.16.03.1jakartajakarta.servlet.jsp.jstl-api:3.0.0 org.glassfish.web:jakarta.servlet.jsp.jstl:3.0.1jakarta.tags.coreTomcat 116.14.0jakarta同上3.0.xjakarta.tags.core注意Tomcat 从来不自带 JSTL。你在 Tomcat 的lib目录里翻不到任何 jstl 相关的 jar别指望容器帮你兜底。如果项目是 Tomcat 9 想升到 Tomcat 10.1JSTL 这块至少要动四个地方Maven 坐标、web.xml的 schema 版本、JSP 里的 taglib URI、以及所有import javax.servlet.*的 Java 代码。这四件事漏一件都会在启动或首次访问时报错升级前最好列个清单。3. 标准 Web 项目目录结构与依赖落地依赖写进pom.xml只是第一步它得真正落到WEB-INF/lib里才算生效。这一步在 IDEA 里特别容易出岔子因为 IDEA 的运行机制和纯 Maven 打包不完全一样。3.1 Maven Web 项目的标准目录长什么样一个规范的 Maven Web 项目结构大致是这样my-webapp/ ├── pom.xml └── src/ ├── main/ │ ├── java/ Java 源码 │ ├── resources/ 配置文件jdbc.properties 等 │ └── webapp/ Web 根目录 │ ├── WEB-INF/ │ │ ├── web.xml │ │ └── lib/ 运行期 jar打包时自动填充 │ ├── static/ css、js、图片 │ └── index.jsp └── test/ └── java/有两点要特别提醒。第一webapp目录必须放在src/main下面不能放在项目根目录这是 Maven 的约定放错了maven-war-plugin打包时会找不到页面。第二WEB-INF/lib目录你手动建也行不建也行Maven 打包时会根据依赖自动往里塞但不要手动往里丢 jar那样会绕开依赖管理时间一长没人知道哪个 jar 是从哪来的。用 IDEA 2024 或 2025 创建项目时最稳的方式是用 Maven 的 webapp 原型新建项目时选 Maven勾选 Create from archetype选org.apache.maven.archetypes:maven-archetype-webapp。生成之后第一件事是把pom.xml里的packaging确认成war然后去 Project Structure 里把webapp目录标记为 Web Resource Directory、把web.xml路径指对。新版 IDEA 有时候不会自动识别页面放在哪都不对就是这里没配对。3.2 完整的 pom.xml 依赖配置以 Tomcat 10.1 Servlet 6.0 为例一个可以直接抄的pom.xml骨架properties maven.compiler.source17/maven.compiler.source maven.compiler.target17/maven.compiler.target project.build.sourceEncodingUTF-8/project.build.sourceEncoding /properties dependencies !-- Servlet API容器已提供必须 provided -- dependency groupIdjakarta.servlet/groupId artifactIdjakarta.servlet-api/artifactId version6.0.0/version scopeprovided/scope /dependency !-- JSP API同样由容器提供 -- dependency groupIdjakarta.servlet.jsp/groupId artifactIdjakarta.servlet.jsp-api/artifactId version3.1.0/version scopeprovided/scope /dependency !-- JSTL必须打进 war用默认 compile scope -- dependency groupIdjakarta.servlet.jsp.jstl/groupId artifactIdjakarta.servlet.jsp.jstl-api/artifactId version3.0.0/version /dependency dependency groupIdorg.glassfish.web/groupId artifactIdjakarta.servlet.jsp.jstl/artifactId version3.0.1/version /dependency /dependencies build finalNamemy-webapp/finalName plugins plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-war-plugin/artifactId version3.4.0/version /plugin /plugins /build这里的 scope 是最容易配错的地方值得单独讲。jakarta.servlet-api和jakarta.servlet.jsp-api一定要用provided因为 Tomcat 自己带了一套你打进去反而会造成类加载冲突典型的报错是ClassCastException或者方法找不到。而 JSTL 的 API 和实现不能用providedprovided只在编译和测试阶段有效打包时不会进WEB-INF/lib运行期容器找不到 TLD 文件页面直接 500。这个区别很多人第一次配都搞反。3.3 IDEA 里 Artifact 和 WEB-INF/lib 的坑即使pom.xml写对了在 IDEA 里点运行还是可能报 URI 解析不了原因多半出在 Artifact 上。IDEA 跑 Web 项目时并不是直接把源码目录丢给 Tomcat而是按 Project Structure → Artifacts 里定义的输出布局生成一份部署目录通常叫out/artifacts/xxx_war_explodedTomcat 实际跑的是这个目录。如果 Artifact 里没有把 Maven 依赖放进WEB-INF/lib那 Tomcat 就真的看不到 JSTL jar。操作路径是File → Project Structure → Artifacts → 选中你的 war exploded → 右侧看WEB-INF/lib这一层。正常应该能看到一堆 jar。如果是空的点下面的 Add → Library Files把 Maven 依赖加进去或者更省事的办法是删掉这个 Artifact 重新生成IDEA 会从 Maven 模型重新推导。还有个隐蔽的情况pom.xml改了依赖之后 IDEA 没自动刷新Artifact 里还是老的一份这时候手动点一下 Maven 面板的刷新按钮再回来检查。提示排查jar 到底有没有进去的最快方法是去部署目录里直接看文件系统而不是在 IDEA 的界面里猜。找out/artifacts/项目名_war_exploded/WEB-INF/lib/这个路径用文件管理器打开看一眼比任何配置界面都直观。4. JSP 页面里的标签库声明与常用标签实操依赖配好了接下来是页面。taglib指令看起来简单但 URI 写错一个字符就是 500而且不同版本的 URI 完全不同复制别人的代码时一定要连版本一起确认。4.1 taglib 指令的 URI 对应关系声明方式就是页面顶部一行% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urijakarta.tags.core % % taglib prefixfmt urijakarta.tags.fmt % % taglib prefixfn urijakarta.tags.functions %如果是 javax 时代或 JSTL 2.0把上面三行换成% 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 %prefix只是个前缀名理论上是自由取的但强烈建议保持c、fmt、fn这三个约定俗成的写法代码给别人看的时候没人需要额外适应。另外四个库sql和xml现在基本不用了业务逻辑写在数据库层更合适页面里直接写 SQL 是反模式。4.2 core 标签实战条件、循环、URL 拼接core库是使用频率最高的日常 90% 的需求都能覆盖。先看条件判断c:if test${not empty sessionScope.user} p欢迎回来${sessionScope.user.nickname}/p /c:if c:choose c:when test${order.status 1}span classtag待付款/span/c:when c:when test${order.status 2}span classtag已发货/span/c:when c:otherwisespan classtag已完成/span/c:otherwise /c:choose注意c:if没有else分支多分支场景只能用c:choose。而且test属性里的表达式是 EL不需要写${}外层再套引号之外的东西写成testtrue是合法的。初学者常犯的错是写test${order.status} 1把表达式拆到引号外面这样整个字符串会被当成一个变量名去解析。循环是另一个高频用法table c:forEach items${productList} varp varStatusst tr class${st.index % 2 0 ? even : odd} td${st.count}/td tdc:out value${p.name}//td tdfmt:formatNumber value${p.price} pattern#,##0.00//td /tr /c:forEach /tablevarStatus提供四个常用属性index从 0 开始的索引、count从 1 开始的计数、first、last。做斑马纹、判断是否是最后一条加分隔符用这些就够了。begin、end、step可以控制范围例如c:forEach begin1 end5 vari就是固定输出 1 到 5。URL 拼接推荐用c:url配合c:param它会自动帮你做 URL 编码还自动处理上下文路径c:url vardetailUrl value/product/detail c:param nameid value${p.id}/ c:param namefrom valuelist/ /c:url a href${detailUrl}查看详情/a如果项目部署的 context path 不是根路径直接手写/product/detail会丢掉应用前缀导致 404用c:url就自动补上了。这个坑在本地用 IDEA 跑一般 context 是/时看不出来一部署到服务器上换了路径就全挂。4.3 fmt 与 fn日期格式化与字符串处理fmt库解决国际化和格式化问题日期和金额处理几乎必用fmt:formatDate value${order.createTime} patternyyyy-MM-dd HH:mm:ss/ fmt:formatNumber value${order.amount} typecurrency currencySymbol¥/value要求是java.util.Date类型。如果你用的是 Java 8 之后的LocalDateTimeJSTL 3.0 之前是不支持的需要么在 Java 侧转换要么直接用 EL 调方法${order.createTime.format(...)}需要容器支持 EL 3.0。这一点很多从老项目迁过来的同学会踩到页面直接抛类型转换异常。fn库是纯函数集合用在 EL 表达式里c:if test${fn:length(productList) 0}.../c:if p${fn:trim(user.remark)}/p p${fn:escapeXml(comment.content)}/pfn:escapeXml在输出用户输入内容时非常关键它能防止脚本注入比手动写c:out在拼接场景下更方便。不过要注意c:out默认就会转义两者选一个用就行重复转义会导致页面上出现amp;lt;这种奇怪的字符。4.4 用 jsp-config 统一声明可选方案如果项目里 JSP 页面特别多每个文件都写三行taglib也烦。可以在web.xml里做全局声明jsp-config taglib taglib-urijakarta.tags.core/taglib-uri taglib-location/WEB-INF/lib/jakarta.servlet.jsp.jstl-3.0.1.jar/taglib-location /taglib /jsp-config这样一来所有 JSP 都不用写taglib指令了。但说实话我个人不太推荐这种方式。一是taglib-location里写了具体 jar 文件名升级依赖版本时这里必须同步改忘了就报错二是 jar 内部的 TLD 本身就会被容器自动扫描到这层配置属于重复劳动三是页面里少了taglib声明后看单个文件根本不知道用到了哪些库可读性下降。所以我只在维护那种几百个 JSP 的历史项目、实在不想批量改动时才会考虑它。5. 部署到 Tomcat 与产物验证页面写完只是看起来对了真正的问题是容器到底认不认。与其猜不如直接去看编译产物——这是我认为排查 JSP 问题最有效的手段比反复翻日志快得多。5.1 JSP 编译后的文件到底在哪JSP 不是直接运行的容器会把它翻译成 Java 再编译。产物路径遵循这样的规律$CATALINA_BASE/work/Catalina/localhost/contextPath/org/apache/jsp/比如index.jsp在 ROOT 应用下会生成index_jsp.java和index_jsp.class。在 IDEA 里跑项目时CATALINA_BASE通常不是一个固定的 Tomcat 安装目录而是 IDEA 临时生成的工作目录路径大概长这样C:\Users\用户名\AppData\Local\JetBrains\IntelliJIdea2024.3\tomcat\随机串\work\Catalina\localhost\ROOT\org\apache\jsp\找这个目录有个小技巧在 Run 窗口输出的日志里搜Using CATALINA_BASE它会打印出实际使用的路径直接复制过去打开就行比一层层翻目录快。5.2 怎么看 _jsp.java 判断 taglib 有没有解析成功打开index_jsp.java之后重点看两处。第一处是页面里写c:forEach的地方如果 JSTL 正常生效你会看到类似这样的代码org.apache.taglibs.standard.tag.rt.core.ForEachTag _jspx_th_c_forEach_0 new org.apache.taglibs.standard.tag.rt.core.ForEachTag(); _jspx_th_c_forEach_0.setItems((java.lang.Object) org.apache.jasper.runtime.PageContextImpl.proprietaryEvaluate(...));看到org.apache.taglibs.standard.tag.rt.core.ForEachTag这个类名说明 jar 加载正常、TLD 找到了、标签被正确识别了。反过来如果编译出来的_jsp.java里出现了out.write(c:forEach ...)这样的字符串字面量那就说明标签根本就没被解析被 Jasper 当成普通 HTML 文本原样输出了。这种情况通常出现在taglib 指令写在了 HTML 注释里被忽略了、或者 JSP 被当成了静态资源直接返回比如页面扩展名不对、或者 Spring Boot 里没配 JSP 视图解析器。还有一种情况是编译直接失败.java文件压根没生成Tomcat 日志里会有JasperException。这种反而好办异常信息里会明确告诉你哪一行、哪个 URI 解析不了。提示改完配置之后一定要先清理 work 目录再重启。Jasper 有缓存机制旧的编译产物还在的话你看到的可能是上一版的报错白折腾半天。IDEA 里可以配置 Before launch 里加一个清理动作或者干脆手动删掉那个 work 目录。5.3 用 war 包内容和依赖树做双重确认IDEA 里跑通不代表mvn package出来的 war 跑得通这两个路径的依赖处理机制不一样。打包之后建议做两个检查第一展开 war 看内容unzip -l target/my-webapp.war | grep jstl正常输出里应该能看到WEB-INF/lib/jakarta.servlet.jsp.jstl-api-3.0.0.jar和WEB-INF/lib/jakarta.servlet.jsp.jstl-3.0.1.jar。如果没有回去检查 scope 是不是被写成了provided。第二看依赖树有没有重复mvn dependency:tree -Dincludes*jstl*这一步主要是抓双份 jar的情况。比如某个老的公共模块间接带进来了javax.servlet:jstl:1.2你自己又引了 Jakarta 版两个 jar 里都有一份 TLD容器扫描时可能选择到错误的那一份报错信息会非常迷惑。发现之后用exclusions把老的那份排除掉或者在依赖管理里统一锁定版本。6. 高频报错速查与排查技巧前面讲了原理这里做一次集中整理。下面这张表是我自己这几年攒下来的遇到报错直接对着找效率比搜索引擎高。6.1 常见报错与对应原因报错信息关键片段根本原因处理办法The absolute uri: jakarta.tags.core cannot be resolvedjar 没进 WEB-INF/lib或 URI 拼错检查 Artifact 输出布局、核对 URIThe absolute uri: http://java.sun.com/jsp/jstl/core cannot be resolved用了 JSTL 3.0 依赖却写老 URI依赖降到 2.0 或页面改用新 URINoClassDefFoundError: org/apache/taglibs/standard/tag/rt/core/ForEachTag只引了 API 没引实现补上 org.glassfish.web 实现包ClassNotFoundException: jakarta.servlet.jsp.jstl.core.Configjavax 与 jakarta 混用统一命名空间清理旧 jar标签原样输出到页面上taglib 指令缺失或被注释检查页面顶部指令页面报 ClassCastException / 方法不存在servlet-api 没设 provided改 scope 为 provided首次访问 500重启后好了work 目录缓存脏了清理 work 目录重新部署6.2 依赖冲突的定位手法依赖冲突最难的地方在于症状和原因对不上。举个例子页面报的是Config类找不到但你去WEB-INF/lib一看jakarta.servlet.jsp.jstl-api-3.0.0.jar明明在里面。这时候八成是同一份类被两个不同版本的 jar 提供了类加载器加载到了错的那一份。定位思路是先把所有可疑 jar 列出来mvn dependency:tree -Dverbose | grep -i -E jstl|taglibs-Dverbose会显示被省略的冲突依赖和冲突原因。找到之后有两条路如果冲突来自传递依赖在引入它的那个依赖上加exclusions如果是自己的直接依赖写重了直接删掉多余的。另外mvn dependency:analyze也能帮你发现声明了但没用到或者用到了但没声明的依赖属于清理pom.xml的常规手段。还有个小工具值得提一下解压 war 之后把WEB-INF/lib里的所有 jar 用同一套规则列出来文件名 大小跟另一份能正常运行的 war 做对比一眼就能看出差在哪。这个方法比分析依赖树糙但快。6.3 几个容易忽略的细节web.xml 的版本声明要跟着容器走。用 Tomcat 10.1 时web.xml的根元素应该声明 Servlet 6.0web-app xmlnshttps://jakarta.ee/xml/ns/jakartaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttps://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd version6.0如果这里还写着http://xmlns.jcp.org/xml/ns/javaee的 4.0 版本Tomcat 10 会按老规范解析某些标签行为会有差异。一个快速验证方法是看启动日志里有没有 schema 校验的警告。Spring Boot 项目里用 JSP 有额外前提。Spring Boot 默认不推荐 JSP内嵌 Tomcat 下要跑 JSP 需要满足几个条件打包方式必须是war不是jar需要加tomcat-embed-jasper依赖scope 用provided视图解析器要配置spring.mvc.view.prefix和suffix。Spring Boot 3.x 用的是 Jakarta 命名空间JSTL 必须用 3.0 那套坐标。很多人从 Spring Boot 2.x 升级上来就是这一步没改页面直接 500。Nginx 反向代理多个 Web 项目时注意 context path。如果多个应用挂在同一个域名下用不同前缀转发页面里的静态资源路径和c:url生成的链接都会带上 context path。这种情况下用相对路径写死资源地址很容易 404老老实实用${pageContext.request.contextPath}或者c:url让容器帮你拼。JDK 版本和 JSTL 3.0 的匹配。JSTL 3.0 要求至少 Java 11实际项目里建议 17 以上。如果pom.xml的编译级别还是 1.8某些实现代码在编译期就会报版本不兼容。6.4 我踩过的几个真实的坑第一个坑是本地能跑服务器上炸。原因是本地 IDEA 的 Artifact 里手动加过 jar而pom.xml里的 scope 其实是providedCI 打出来的 war 里没有。后来我养成了一个习惯本地跑通之后一定用mvn clean package打一次包解压检查WEB-INF/lib再拿这个 war 部署一次。多花五分钟能省掉一次上线回滚。第二个坑是升级依赖之后忘了清缓存。Maven 依赖换了版本IDEA 里 Artifact 用的是新 jar但 Tomcat 的 work 目录里还是按老 URI 编译出来的 class报错信息和当前代码完全对不上。后来我在 IDEA 的 Run Configuration 里加了一个删除 work 目录的启动前置步骤这类改了没生效的问题基本绝迹。第三个坑是排除依赖不够彻底。有个项目里父pom通过某个公共模块引入了老版 JSTL我在子模块里加了新依赖但继承来的老 jar 还在结果两个 TLD 打架。当时排查了很久最后是在子模块的依赖里加exclusions明确排掉才解决。教训是Java Web 项目里凡是涉及命名空间迁移的依赖都要主动检查传递依赖不能只看自己写的那几行。6.5 版本迁移的推荐顺序如果手上的老项目要迁到新容器我建议按这个顺序动每一步都能单独验证先把容器换成目标版本把javax相关的编译依赖全部改成jakarta坐标不改 JSTL。修改web.xml的 schema 到目标规范版本。全局替换 Java 代码里的javax.servlet为jakarta.servlet。最后再动 JSTL换坐标、换 URI逐个页面测。每次改完都用mvn clean package打一次包确认依赖树干净。把 JSTL 放到最后是因为它的问题最容易通过页面测试发现前面的基础依赖没理顺之前JSTL 的报错会被其他错误淹没你根本分不清是哪一层的问题。最后再分享一个小技巧如果实在找不到问题在哪可以新建一个最小的空白 Web 项目只放一个 JSP、一个c:forEach把依赖配好跑通然后把这个能跑的pom.xml和目录结构作为参照跟出问题的项目做逐项对比。这个最小可复现样例的思路在排查配置类问题时特别管用因为配置问题往往不是某一行错了而是某几行之间的关系不对对着一个能跑的样本比对着文档猜要快得多。
返回列表