首页 / 资讯中心 / 文章详情

Spring jar包分层清单与classpath依赖冲突排查

Spring jar包分层清单与classpath依赖冲突排查 ★ FEATURED ARTICLE
简介面向 Java Web 开发者与 Spring 初学者的依赖 jar 包合集主要解决入门阶段四处搜集依赖、版本难以对齐的问题。整包围绕 Spring 3.2.8 版本组织既包含 IoC 容器所需的 core、beans、context、expression 基础组件也包含 SpringMVC 的 web 与 webmvc 模块以及持久层搭配的 jdbc、tx 组件此外还提供 c3p0 与 dbcp 两套连接池方案、Oracle 驱动、Junit 单元测试和文件上传相关依赖并附一份 applicationContext.xml 示例配置导入工程后即可对照搭建环境。资源共 21 个文件由 20 个 jar 与 1 个 xml 配置文件组成压缩包体积 7.71MB按模块存放便于按需取用。目前已有 314 人学习下载。对于想完整走通 Spring 与 SpringMVC、JDBC 整合流程或需要对照标准依赖清单排查冲突的读者这份清单式包体可作为现成的起点省去逐个比对版本的时间。1. 一个 lib 目录塞进四十多个 jar 的下午上周帮人看一个跑不起来的老服务lib 目录里放着四十多个 jar 包spring-core、spring-beans 各有两个版本commons-logging 和 spring-jcl 挨着躺在一起。启动第一行就抛出NoClassDefFoundError: org/springframework/core/io/Resource把 classpath 顺序来回换了几遍也没用问题根本不在顺序而是因为缺件和版本冲突。Spring jar 包从来不是下载一个安装包再一路下一步就能装完的东西它按模块拆成十几二十个包核心容器、AOP、Web、数据访问各自负责一部分少一个就启动不了多一个不同版本就会互相冲突。手工搭 SSM 老项目、内网机器连不上仓库、给新人准备一个开箱即跑的示例这三种场景最容易遇到这些问题。这套环境装得对不对差别就在启动那三秒。2. 版本与 JDK 对齐Spring 5.x 和 6.x 的 jar 包不是一套拿到一套 jar 包第一件事不是往 lib 里拷而是先确认它们属于 5.x 还是 6.x以及对应的 JDK 大版本。这两套 jar 的类名、包名看起来几乎一样混着放进同一个 classpath编译期不报错运行期才报错排查成本非常高。Spring 5.x 面向 JDK 8 及以上主流的落点是 JDK 8、11、17Spring 6.x 的门槛直接抬到 JDK 17还需要 Servlet 容器支持 Jakarta EE 9 以上。如果项目跑在 JDK 8 上那基本只能选 5.x这一条就先把范围固定住了。2.1 先在机器上确认 JDK再决定拿哪一套不要凭记忆判断直接敲命令看。同一台机器上装了多个 JDK 的情况太常见IDE 里配的和命令行里的经常不是同一个。# 看当前 shell 生效的 JDK 版本 java -version javac -version # 看编译器实际会用的那个 JDK 安装路径 which javac readlink -f $(which javac) # 如果机器上装了多个 JDK列出常见的安装目录逐个确认 ls -1 /usr/lib/jvm 2/dev/nulljava -version输出里的1.8.0_xxx就是 JDK 811.0.x是 JDK 1117.0.x是 JDK 17。javac -version和java -version必须一致不一致说明 PATH 里混了两个 JDK这时候编译出来的 class 文件版本和运行时要求对不上报的是UnsupportedClassVersionError错误信息里会带两个数字比如class file version 61.0, this version only recognizes up to 52.061 对应 JDK 1752 对应 JDK 8。参数取舍上我一般按项目里最低的那个依赖决定只要有一个第三方包只出了 JDK 8 的字节码整个项目就留在 JDK 8 或 11 上别为了追新把 JDK 升到 17升上去之后还得处理模块化相关的启动参数收益不高。2.2 javax 和 jakarta 的分界线Web 相关 jar 最容易出错5.x 到 6.x 最大的一处不兼容就是 Servlet API 的包名从javax.servlet换成了jakarta.servlet。这意味着spring-webmvc虽然两套都能用但它编译时依赖的 Servlet API 不是同一个。手工配 jar 的时候如果放了javax.servlet-api又用了 Spring 6 的spring-webmvc启动时典型报错是NoClassDefFoundError: jakarta/servlet/Servlet或者ClassNotFoundException: jakarta.servlet.http.HttpServletRequest。项目Spring 5.xSpring 6.x最低 JDKJDK 8JDK 17Servlet API 包名javax.servletjakarta.servletServlet API 示例坐标javax.servlet:javax.servlet-apijakarta.servlet:jakarta.servlet-apiJSP API 包名javax.servlet.jspjakarta.servlet.jsp容器版本参考Tomcat 8/9Tomcat 10 及以上日志桥接spring-jcl或commons-logging只用spring-jcl判断手上的spring-webmvc属于哪一套最省事的办法是直接解开看里面的 import 引用或者看 MANIFEST 里的构建信息# 看 jar 的构建元信息Build-Jdk 和 Implementation-Version 都能读出来 unzip -p spring-webmvc.jar META-INF/MANIFEST.MF # 确认这个 jar 里到底引用的是 javax 还是 jakarta unzip -p spring-webmvc.jar org/springframework/web/servlet/DispatcherServlet.class \ | strings | grep -E javax/servlet|jakarta/servlet | head -n 3unzip -p是把指定文件输出到标准输出而不落盘strings从二进制里提取可读字符串。能 grep 到jakarta/servlet就说明这套是 6.x 的配套的 Servlet API 必须换成 jakarta 版本容器也得换成 Tomcat 10 以上。这一步做完再往下装能省掉后面一大半的排查时间。2.3 一份能对照的 jar 包分层清单Spring 的 jar 包按职责分层装的时候按层往下装缺哪层补哪层比一股脑全拷进 lib 更容易定位问题。下面这张表是我自己整理的分层方式左边是层右边是这个层要放什么、什么场景才需要。分层包含的 jar什么时候必须放核心容器spring-core、spring-beans、spring-context、spring-expression、spring-jcl任何用到 IoC 的项目一个都不能少AOPspring-aop用了Aspect、事务注解、缓存注解就要放切面实现aspectjweaver用注解式切面时必须放否则切面静默失效Web 基础spring-web有 HTTP 请求处理、远程调用才需要Web MVCspring-webmvc、Servlet API写 Controller 才需要数据访问spring-jdbc、spring-tx有事务、有 JdbcTemplate 才需要ORM 集成spring-orm接 MyBatis、Hibernate 之类才需要JSON 序列化jackson-databind、jackson-core、jackson-annotations接口返回 JSON 才需要测试spring-test、测试框架只影响测试代码不打进产物这张表最有用的地方在于反向排查报错里出现哪个包名就回表里找它属于哪一层再看那一层的配套 jar 齐不齐。比如报org/springframework/expression找不到问题一定在核心容器层跟 Web 层没关系先别去动spring-webmvc。3. 手工搭 lib 目录最小可运行组合与加载顺序有些环境就是连不上仓库只能把 jar 拷来拷去这时候得知道最小可运行组合是什么。我的判断标准很简单写一个只加载 IoC 容器、取一个 Bean 的 main 方法能跑通就是最小集跑不通就继续往上加加一层验一层。这样每次只引入一个变量出错的时候能立刻知道是谁的问题。3.1 六个 jar 撑起 IoC 容器只做依赖注入、不碰 Web 和数据访问的话核心容器层加 AOP 层这六个 jar 就够了。少放spring-aop是很常见的错误因为大多数教程只提 core、beans、context、expression 四个但spring-context本身依赖spring-aop缺了它会在启动时抛NoClassDefFoundError: org/springframework/aop/framework/AopProxy报错信息看起来跟 AOP 业务代码毫无关系容易让人往错误方向查。# 按层建目录把 jar 分开放出问题时能一眼看出是哪一层缺东西 mkdir -p lib/core lib/aop lib/web lib/data lib/third cp spring-core.jar spring-beans.jar spring-context.jar \ spring-expression.jar spring-jcl.jar lib/core/ cp spring-aop.jar lib/aop/-p保证父目录不存在时一起建出来重复执行也不会报错。分层放目录的好处是后面写启动脚本时可以把整层目录用通配符拼进 classpath加一层改一行不用一个个文件去数。六个 jar 的职责我按自己的理解记spring-core是最底层的工具类和资源加载spring-beans管 Bean 的定义和装配spring-context是容器本身ApplicationContext就在这里面spring-expression是 SpEL 表达式解析spring-aop提供切面基础设施spring-jcl是日志门面桥接。这六个版本号必须完全一致混版本是后面所有奇怪问题的根源。3.2 第三方依赖怎么补齐日志、JSON、AOP、ServletSpring 自己不打包日志实现5.x 起用spring-jcl替代了commons-logging。这两个包提供的是同一套org.apache.commons.logging接口同时存在于 classpath 里会出现类重复加载表现是日志级别配置不生效或者启动时打一堆警告。我一般只留spring-jcl把commons-logging删掉。# 确认 lib 里没有重复的日志门面 find lib -name *commons-logging* -o -name spring-jcl* # 确认日志实现只有一个这里以常见的 slf4j 加实现为例 find lib -name slf4j-api*.jar -o -name logback-classic*.jar -o -name log4j-*.jar第二条命令如果同时列出 logback 和 log4j 两套实现就得挑一套删掉。多个日志实现共存时日志能不能成功输出取决于 classpath 顺序这类问题很难复现属于需要避免出现的情况。JSON 这块jackson-databind依赖jackson-core和jackson-annotations三个包的版本必须一致。只放 databind 一个编译能过运行到消息转换器时才抛NoClassDefFoundError: com/fasterxml/jackson/core/JsonGenerator。AOP 注解切面还需要aspectjweaver缺了它Aspect标注的类会被当成普通 Bean 注册切面逻辑不执行也不报错属于比较隐蔽的配置问题。3.3 classpath 的三种写法命令行、IDE、可执行 jarclasspath 顺序在正常项目里通常无所谓但只要出现同名类第一个被找到的会生效后面的会被忽略。这也是为什么同一个 lib 目录在 IDE 里能跑、换到命令行就出错——IDE 会按自己的规则重排 classpath。# 命令行启动时按层拼接 classpathLinux/macOS 用冒号分隔 java -cp lib/core/*:lib/aop/*:lib/web/*:lib/data/*:lib/third/*:classes \ com.example.Bootstrap # Windows 下分隔符是分号通配符写法也支持 java -cp lib\core\*;lib\aop\*;lib\web\*;lib\data\*;lib\third\*;classes ^ com.example.Bootstrap通配符lib/core/*只匹配该目录下的 jar不会递归进子目录所以目录分层要平不要再往下套一层。用通配符还有个好处往目录里加 jar 不用改启动命令省掉一类低级错误。打成可执行 jar 的时候Class-Path写在 MANIFEST 里路径是相对 jar 所在目录的多个条目空格分隔且不能写通配符Manifest-Version: 1.0 Main-Class: com.example.Bootstrap Class-Path: lib/core/spring-core.jar lib/core/spring-beans.jar lib/core/spring-context.jar lib/core/spring-expression.jar lib/core/spring-jcl.jar lib/aop/spring-aop.jar这行最容易出错的地方是行宽限制和末尾空格——Class-Path每行不超过 72 字节超了要折行折行后第二行必须以空格开头折错一行整个 jar 就找不到依赖报的还是ClassNotFoundException。4. 交给构建工具把 jar 清单变成可复现的依赖手工拷 jar 的方式能跑但复现不了。换台机器、换个人接手版本就变了。所以只要环境允许我都会把上面那份清单落成 Maven 或 Gradle 的依赖声明lib 目录只作为离线兜底保留。4.1 Maven用属性统一版本用 scope 控制边界Spring 各模块版本必须一致所以版本号不要在每处写一遍只要属性一处管全局。下面是一份只包含核心容器的 POM 片段版本号写成占位复制时换成资源包里实际的三段数字。properties !-- 全局版本号所有 spring 模块统一引用这一处 -- spring.version5.3.x/spring.version project.build.sourceEncodingUTF-8/project.build.sourceEncoding maven.compiler.source8/maven.compiler.source maven.compiler.target8/maven.compiler.target /properties dependencies !-- context 会传递带入 core、beans、expression、aop、jcl写一个就够 -- dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version${spring.version}/version /dependency !-- Servlet API 只在编译期需要容器运行时自带必须标 provided -- dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency !-- 测试作用域不会进产物包 -- dependency groupIdorg.springframework/groupId artifactIdspring-test/artifactId version${spring.version}/version scopetest/scope /dependency /dependenciesmaven.compiler.source/target要和实际 JDK 对齐Spring 5.x 写 8 或 11Spring 6.x 必须写 17写在 8 上编译会直接失败。Servlet API 的provided是关键打进 war 包会导致容器自带的版本和包里的版本打架表现是过滤器、监听器注册异常或者启动时报IllegalStateException。只声明spring-context就能带出核心容器全部依赖这是 Maven 的传递依赖机制在起作用。想看它到底带进来了哪些、有没有重复版本用这条命令# 只看 spring 相关的依赖树确认没有出现两个不同版本 mvn -q dependency:tree -Dincludesorg.springframework # 想看全量加上 outputFile 落地成文件慢慢翻 mvn dependency:tree -DoutputFiletree.txt -DappendOutputtrue-Dincludes支持groupId:artifactId:version这种通配格式只写 groupId 会过滤出该组织下所有条目。输出里如果同一个 artifactId 出现两次不同版本说明有别的依赖在拉旧版本得用dependencyManagement或exclusions把它固定下来。4.2 Gradle用 BOM 统一版本用本地目录兜底Gradle 的写法更简洁Spring 官方提供了 BOM引入之后各模块不用再写版本号。这套写法适合新一点的工程老项目继续用上面的 Maven 方式就好。dependencies { // 引入 BOM 后spring 各模块不需要单独写版本 implementation platform(org.springframework:spring-framework-bom:5.3.x) implementation org.springframework:spring-context implementation org.springframework:spring-webmvc // 本地 lib 目录兜底仓库拉不到的时候用 implementation fileTree(dir: lib/third, include: [*.jar]) compileOnly javax.servlet:javax.servlet-api:4.0.1 } // 打 fat jar 时排除签名文件否则运行会报签名校验失败 jar { from { configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } } exclude META-INF/*.SF, META-INF/*.DSA, META-INF/*.RSA }platform(...)的作用是只约束版本、不引入实际依赖写成enforcedPlatform则会强制覆盖所有传递依赖的版本冲突多的时候用后者正常情况用前者。fileTree兜底那个目录建议只放仓库里没有的内部包或特殊版本别把 Spring 自己的 jar 也扔进去不然 BOM 管的版本和文件里的版本会打架排查起来很麻烦。签名文件排除那段是打 fat jar 的固定动作不加的话运行时报SecurityException: Invalid signature file digest。4.3 离线仓库不可用时的本地 install内网环境里仓库访问不了是常态这时候把 jar 装进本地仓库POM 就能照常写。批量安装用脚本一个包一条命令参数顺序别搞错。# 单个 jar 装进本地仓库坐标必须和 POM 里声明的一致 mvn install:install-file \ -Dfile./lib/core/spring-core.jar \ -DgroupIdorg.springframework \ -DartifactIdspring-core \ -Dversion5.3.x \ -Dpackagingjar # 批量循环安装文件名和 artifactId 一一对应才能自动推出来 for f in lib/core/*.jar; do name$(basename $f .jar) mvn -q install:install-file -Dfile$f \ -DgroupIdorg.springframework -DartifactId$name \ -Dversion5.3.x -Dpackagingjar done-DgroupId、-DartifactId、-Dversion三个参数合起来构成唯一坐标写错任何一个POM 里对应依赖就会解析失败报Could not find artifact。-Dpackagingjar不能省某些版本不写会默认成pom装进去之后依赖能解析但运行期找不到类这种错误最难查。装完最好验证一下# 确认安装结果能列出目录说明坐标已经落盘 ls ~/.m2/repository/org/springframework/spring-core/这一步看起来多余但踩过一次就会习惯性检查——坐标写错的时候Maven 是静默成功不报错的。5. 排查jar 冲突与类加载的四个常见现场以下四条都是我实际遇到过的现场按现象、原因、解决的顺序写。排查思路统一先看报错里的全限定类名确定属于哪个包再回第 2 章那张分层表里定位最后用命令验证。5.1 现象启动报 NoClassDefFoundError但类明明在 jar 里原因jar 在 lib 目录里但没被加进 classpath或者路径写错。还有一种情况是 jar 下载不完整文件大小明显偏小解压时报zip END header not found。解决先确认 jar 完整再确认 classpath 拼对。# 检查 jar 是否完整能列出内容说明文件没坏 jar tf lib/core/spring-context.jar | head -n 5 # 检查目标类是否真的在这个 jar 里 jar tf lib/core/spring-context.jar | grep AnnotationConfigApplicationContext # 打印实际生效的 classpath java -XshowSettings:properties -version 21 | grep java.class.pathjar tf只读目录不动内容速度很快。-XshowSettings:properties会把 JVM 的真实配置打出来java.class.path就是你启动时拼的 classpath中间少了哪层一眼就能看到。我更倾向于用-verbose:class看类是从哪个 jar 加载的输出量大但信息最直接。5.2 现象切面不生效日志里没有任何报错原因aspectjweaver没放进 lib或者spring-aop缺失。Aspect类会被当成普通 Bean 注册切面方法永远不执行也不报错只能从行为上发现。解决把aspectjweaver加进 lib并在配置类上确认开启了切面支持。Configuration ComponentScan(com.example) EnableAspectJAutoProxy(proxyTargetClass true) public class AppConfig { // 开启切面自动装配proxyTargetClass 让没有接口的类也能被增强 }EnableAspectJAutoProxy不加这个注解切面类注册了也不会织入。proxyTargetClass true是为了让没有实现接口的类也能生成增强对象否则遇到没有接口的 Bean 会报BeanNotOfRequiredTypeException。5.3 现象同一个类被加载两次静态变量行为异常原因classpath 里存在两个不同版本的spring-core.jar或者父子类加载器各加载了一份。典型表现是单例 Bean 拿到两个不同实例或者常量类的值前后不一致。解决清掉重复 jar用依赖树定位来源。# 在 lib 目录里找重复的 artifact按文件名前缀分组出现两次的就是重复 find lib -name *.jar | xargs -n1 basename | sed s/-[0-9].*// | sort | uniq -dsed s/-[0-9].*//把版本号截掉只留 artifact 名uniq -d输出出现超过一次的行。这是排查重复依赖最省事的办法比逐个看文件快得多。查出重复之后Maven 项目用dependencyManagement锁版本手工 lib 就删掉旧的那个。5.4 现象UnsupportedClassVersionError编译能过运行不行原因编译用的 JDK 比运行用的新。IDE 里配的是 JDK 17命令行java指向 JDK 8class 文件版本对不上。解决确认运行时 JDK并把source和target统一到运行环境。# 查 class 文件的字节码版本52 是 JDK 855 是 JDK 1161 是 JDK 17 javap -verbose classes/com/example/Bootstrap.class | grep major versionjavap -v输出的major version就是字节码版本号跟java -version对一遍就知道是不是版本错配。POM 里把maven.compiler.source和maven.compiler.target设成运行环境的 JDK 版本别设成开发者本机的版本。6. 验证跑通一个 main 方法加三道检查装完之后别急着接业务代码先用一个最小 main 方法验证容器能起来。这一步花不了五分钟但能提前暴露九成的配置问题。import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Bean; Configuration public class SmokeTest { Bean public String hello() { return spring-ok; } public static void main(String[] args) { // 注解驱动容器加载配置类并触发 Bean 初始化 try (AnnotationConfigApplicationContext ctx new AnnotationConfigApplicationContext(SmokeTest.class)) { // 按类型取 Bean取不到会抛 NoSuchBeanDefinitionException System.out.println(ctx.getBean(String.class)); // 打印容器里所有 Bean 名字数量异常说明扫描范围配错了 System.out.println(ctx.getBeanDefinitionCount()); } } }try-with-resources保证容器关闭时会调用销毁方法避免后台线程挂住。getBeanDefinitionCount()打印的数量可以当基线只注册一个配置类加一个 Bean数量应该是个位数如果打出来几十个说明ComponentScan的包路径写大了把不该扫的类扫进来了。三道检查配合着用检查项命令通过标准依赖唯一性mvn -q dependency:tree -Dincludesorg.springframeworkSpring 各模块只出现一个版本类加载正常java -verbose:class -cp ... SmokeTest | grep springframework关键类来自 lib 下的 jar不是 JDK 内置路径字节码版本javap -verbose SmokeTest.class | grep major与运行时java -version对齐-verbose:class的输出里每一行都会带来源 jar 的路径这是判断类是哪儿来的最直接的办法比看 classpath 列表可靠得多。我自己的习惯是每次拿到一套新的 jar 包先跑java -version和javap -verbose对一遍版本再把上面那个 main 方法跑一次两个都过了才往 lib 里加业务需要的包。以前图快跳过这一步结果在一个内网环境里为jakarta.servlet和javax.servlet混用的问题折腾了一整天从那以后这套检查固定在流程里不管多急都强制走一遍。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站