简介:面向 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、17;Spring 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 8,11.0.x是 JDK 11,17.0.x是 JDK 17。javac -version和java -version必须一致,不一致说明 PATH 里混了两个 JDK,这时候编译出来的 class 文件版本和运行时要求对不上,报的是UnsupportedClassVersionError,错误信息里会带两个数字,比如class file version 61.0, this version only recognizes up to 52.0,61 对应 JDK 17,52 对应 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.x | Spring 6.x |
|---|---|---|
| 最低 JDK | JDK 8 | JDK 17 |
| Servlet API 包名 | javax.servlet | jakarta.servlet |
| Servlet API 示例坐标 | javax.servlet:javax.servlet-api | jakarta.servlet:jakarta.servlet-api |
| JSP API 包名 | javax.servlet.jsp | jakarta.servlet.jsp |
| 容器版本参考 | Tomcat 8/9 | Tomcat 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 的项目,一个都不能少 |
| AOP | spring-aop | 用了@Aspect、事务注解、缓存注解就要放 |
| 切面实现 | aspectjweaver | 用注解式切面时必须放,否则切面静默失效 |
| Web 基础 | spring-web | 有 HTTP 请求处理、远程调用才需要 |
| Web MVC | spring-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、Servlet
Spring 自己不打包日志实现,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、可执行 jar
classpath 顺序在正常项目里通常无所谓,但只要出现同名类,第一个被找到的会生效,后面的会被忽略。这也是为什么同一个 lib 目录在 IDE 里能跑、换到命令行就出错——IDE 会按自己的规则重排 classpath。
# 命令行启动时按层拼接 classpath,Linux/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.version>5.3.x</spring.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <dependencies> <!-- context 会传递带入 core、beans、expression、aop、jcl,写一个就够 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency> <!-- Servlet API 只在编译期需要,容器运行时自带,必须标 provided --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> <!-- 测试作用域不会进产物包 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>${spring.version}</version> <scope>test</scope> </dependency> </dependencies>maven.compiler.source/target要和实际 JDK 对齐,Spring 5.x 写 8 或 11,Spring 6.x 必须写 17,写在 8 上编译会直接失败。Servlet API 的provided是关键:打进 war 包会导致容器自带的版本和包里的版本打架,表现是过滤器、监听器注册异常,或者启动时报IllegalStateException。
只声明spring-context就能带出核心容器全部依赖,这是 Maven 的传递依赖机制在起作用。想看它到底带进来了哪些、有没有重复版本,用这条命令:
# 只看 spring 相关的依赖树,确认没有出现两个不同版本 mvn -q dependency:tree -Dincludes=org.springframework # 想看全量,加上 outputFile 落地成文件慢慢翻 mvn dependency:tree -DoutputFile=tree.txt -DappendOutput=true-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 \ -DgroupId=org.springframework \ -DartifactId=spring-core \ -Dversion=5.3.x \ -Dpackaging=jar # 批量循环安装,文件名和 artifactId 一一对应才能自动推出来 for f in lib/core/*.jar; do name=$(basename "$f" .jar) mvn -q install:install-file -Dfile="$f" \ -DgroupId=org.springframework -DartifactId="$name" \ -Dversion=5.3.x -Dpackaging=jar done-DgroupId、-DartifactId、-Dversion三个参数合起来构成唯一坐标,写错任何一个,POM 里对应依赖就会解析失败,报Could not find artifact。-Dpackaging=jar不能省,某些版本不写会默认成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 2>&1 | 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 8,class 文件版本对不上。
解决:确认运行时 JDK,并把source和target统一到运行环境。
# 查 class 文件的字节码版本,52 是 JDK 8,55 是 JDK 11,61 是 JDK 17 javap -verbose classes/com/example/Bootstrap.class | grep 'major version'javap -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 -Dincludes=org.springframework | Spring 各模块只出现一个版本 |
| 类加载正常 | 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混用的问题折腾了一整天,从那以后这套检查固定在流程里,不管多急都强制走一遍。希望帮到你。
本文还有配套的精品资源,点击获取