Spring jar包依赖管理、版本对齐与启动报错排查
简介:面向Java后端初学者与刚接触Spring框架的开发者,这份jar包合集把搭建开发环境时最常缺失的依赖一次性补齐,省去逐个版本比对与反复查找的麻烦。压缩包共21个文件,以20个jar为主体、另附1份xml配置示例,整体约7.71MB,体积轻巧,便于随项目携带或做离线依赖备份,解压后按模块分目录放置,方便按需取用。内容覆盖spring-core、beans、context、expression等核心容器组件,spring-web与spring-webmvc构成的MVC层,spring-jdbc、spring-tx配合c3p0、dbcp等连接池方案,以及文件上传、junit单元测试、Oracle驱动等常用配套,基本能支撑一个传统SSM小项目的编译与启动。全部jar统一为3.2.8.RELEASE版本,彼此配套验证过,有助于规避版本混用引发的类缺失、注解不生效与启动报错。目前已有314人学习,可对照随包的applicationContext.xml示例快速验证环境是否可用,也适合练习手工管理依赖、排查jar缺失类问题。
1. 从一次启动报错说起:Spring jar包不是“凑齐就行”
Spring jar包这件事,最容易被误解成“把 lib 目录塞满就能跑”。我见过一个老项目启动时抛NoClassDefFoundError: org/springframework/core/io/Resource,lib 里明明躺着 spring-core,翻依赖树才发现同时存在两个大版本,构建工具按声明顺序选了旧的那个。Spring 的 jar 包不是孤立文件,核心容器、AOP、Web、事务、测试各自有依赖边界,版本错一位,运行期就翻车。这份资源把 Spring 常用模块按版本分目录整理,适合还在维护非 Maven 老工程、需要离线组包、或者想弄清每个 jar 到底负责什么的人。下面从依赖图、落地方式、版本边界和排查顺序拆开说。
2. 拆开 Spring 的依赖图:核心容器、AOP 与 Web 各要哪些 jar
2.1 五个核心 jar 的职责边界
手工组包前先分清职责,不然很容易把 spring-web 当成 Web 项目必选,却漏掉 spring-expression。最常用的核心五件套是 spring-core、spring-beans、spring-context、spring-expression、spring-aop。spring-core 提供资源加载、类型转换和基础工具类;spring-beans 负责 BeanFactory、依赖注入和 Bean 生命周期;spring-context 是 ApplicationContext、事件机制、国际化、注解扫描的入口;spring-expression 是 SpEL 表达式解析;spring-aop 负责代理和切面基础设施。只写一个AnnotationConfigApplicationContext做最小启动,这五个 jar 基本齐全,Spring 5.x 还要带上 spring-jcl 处理日志桥接,Spring 4.x 则通常需要 commons-logging。
再往上走,spring-tx 管声明式事务和事务抽象,spring-jdbc 管 JdbcTemplate 和异常转换,spring-orm 负责 ORM 集成,spring-web 提供 Web 层基础能力,spring-webmvc 才是 DispatcherServlet、HandlerMapping 那一套 MVC 实现。很多人把 spring-web 和 spring-webmvc 混成一个包,结果在非 MVC 场景里多带了一堆类,或者在 MVC 场景里只放了 spring-web,启动时找不到 DispatcherServlet。我的习惯是先把项目按“纯容器 / 容器加事务 / 容器加 Web MVC”分三档,再按档位取 jar,不靠感觉凑。
2.2 用 Maven 生成可离线拷贝的依赖树
如果手头有一个能跑的 Maven 工程,最稳的方式不是到处找 jar,而是让构建工具把运行期依赖复制出来。下面这段命令先打印 Spring 相关依赖树,再把运行期依赖复制到lib目录。-Dincludes=org.springframework用来过滤只看 Spring 组织下的坐标,-DoutputDirectory=lib指定输出目录,-DincludeScope=runtime表示只要运行期需要的包,测试包不会混进来。
# 进入工程根目录,先看 Spring 相关依赖从哪来 mvn dependency:tree -Dincludes=org.springframework # 把运行期依赖复制到 lib,适合离线组包或老工程迁移 mvn dependency:copy-dependencies \ -DoutputDirectory=lib \ -DincludeScope=runtime \ -DexcludeGroupIds=org.springframework.boot第一行命令的输出重点看+-和\-的层级,某个 Spring jar 如果是被第三方库间接带进来的,后面会标(version managed from ...)或(omitted for conflict)。第二行命令执行完,lib里会出现工程实际解析后的 jar,比手工猜版本可靠。参数上,outputDirectory不要指向src或WEB-INF/lib的原路径,先复制到临时目录,核对完再拷贝,避免把旧版本残留一起带进去。excludeGroupIds那行只是示例,如果工程没有 Spring Boot 依赖可以删掉,不要照抄。
提示:复制依赖前先执行一次
mvn clean,否则target下的旧 class 和旧 jar 可能影响判断。
2.3 手工组包时的最小集与扩展集
没有构建工具的老工程,只能手工往WEB-INF/lib或项目lib里放 jar。下面这张表是我常用的分组方式,按场景取用,不要一次全塞。表里的版本列不写死小版本,实际取包时同一目录下所有 Spring jar 必须来自同一个大版本。
| 场景 | 必需 jar | 说明 |
|---|---|---|
| 纯 IoC 最小启动 | spring-core、spring-beans、spring-context、spring-expression、spring-aop、spring-jcl | Spring 5.x 用 spring-jcl;Spring 4.x 换成 commons-logging |
| 加事务 | 上述最小集 + spring-tx | 只用@Transactional时还要确认事务管理器实现 |
| 加 JDBC | 最小集 + spring-tx + spring-jdbc | 数据库驱动和连接池按项目现有依赖补 |
| 加 Web MVC | 最小集 + spring-web + spring-webmvc + servlet-api | servlet-api 用 provided 语义,不打进最终包 |
| 加测试 | 上述按需 + spring-test | 测试依赖不要进入运行期 lib |
手工组包最容易犯的错,是把不同版本目录里的 jar 混在一起。比如 spring-core 用 5.3.x,spring-webmvc 用 4.1.x,编译期可能过得去,运行期一到NoSuchMethodError就现原形。我的做法是先在资源目录里选一个版本文件夹,把这个文件夹里的 Spring jar 全部拷到一个临时目录,再按上表删掉不需要的模块,最后用 2.2 的依赖树或第 6 章的清单脚本核对一遍。这样至少保证来源一致,不会出现“这个 jar 是从别的项目顺手拿的”这种血泪经验。
3. 把 jar 包放进工程:构建工具与普通工程两种落地方式
3.1 构建工具里的坐标与 scope 写法
Maven 工程不要直接往lib里丢 jar,优先写坐标。下面这段pom.xml片段用属性统一管理 Spring 版本,依赖只声明实际用到的模块。spring-context会传递带入 core、beans、expression、aop 和 jcl,所以不需要重复声明;spring-webmvc会带入 spring-web;spring-jdbc会带入 spring-tx。spring-test的 scope 设为test,避免打进运行期包。servlet-api 的 scope 设为provided,因为运行期由 Servlet 容器提供。
<properties> <!-- 示例版本,按项目实际版本替换,同一工程内保持一致 --> <spring.version>5.3.30</spring.version> </properties> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>${spring.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> </dependencies>这段配置的关键不是 jar 数量,而是 scope。compile是默认值,会进入编译和运行期;runtime编译期不需要但运行期需要;test只在测试类路径;provided编译期需要、运行期由容器提供。servlet-api 如果写成默认 scope,打 war 包时会和容器自带的类重复,轻则警告,重则类加载顺序错乱。spring-test 如果误写成 compile,测试框架会被带进生产包,增加不必要的类路径。改完坐标后执行mvn dependency:tree,确认没有同组织不同版本的 Spring jar 同时出现。
3.2 非构建工具工程的 lib 目录与 Build Path
普通 Java 工程或老式 Web 工程,jar 要放在两个位置之一:普通 Java 工程放项目根目录下的lib,然后在 IDE 的 Build Path 里 Add JARs;Web 工程放WEB-INF/lib,让打包工具自动带入。注意区别:Add External JARs 引用的是本机绝对路径,本地能跑,换机器或打成 war 包就可能丢;Add JARs 引用工程内相对路径,可移植性更好。下面这段命令用来检查WEB-INF/lib里到底有哪些 Spring jar,以及 jar 是否完整。
# 列出 Web 工程 lib 下的 Spring jar find WEB-INF/lib -maxdepth 1 -name 'spring-*.jar' -print | sort # 检查单个 jar 是否可读,并看关键类是否存在 jar tf WEB-INF/lib/spring-core-5.3.30.jar | grep 'org/springframework/core/io/Resource.class'第一行用find只列一层,避免把子目录里的备份 jar 也算进来;sort是为了让同版本文件相邻,方便肉眼发现重复。第二行用jar tf看 jar 内条目,grep的目标类换成你报错信息里缺失的类。如果jar tf直接报错,说明 jar 下载不完整或传输损坏,这种情况在小文件上不明显,大 jar 上很常见。非构建工具工程还要注意:IDE 的 Build Path 里可能残留已删除 jar 的引用,编译期不报错,运行期才抛NoClassDefFoundError,所以每次增删 jar 后都去 Build Path 的 Libraries 页签核对一遍。
3.3 启动验证:三行代码跑通容器
jar 放好后不要直接上 Web 容器,先用一个最小 Java 类验证 IoC 容器能不能启动。下面这段代码用AnnotationConfigApplicationContext注册一个配置类,再从容器里取 Bean。scan的包名换成你自己的包,register的配置类里至少有一个@Bean或@Component。
import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; public class SpringJarSmokeTest { @Configuration static class Config { @Bean public String hello() { return "spring-jar-ok"; } } public static void main(String[] args) { try (AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Config.class)) { System.out.println(ctx.getBean(String.class)); } } }这段代码的验证目标很窄:spring-context、spring-core、spring-beans、spring-expression、spring-aop和日志桥接是否都在类路径上。try-with-resources用来确保容器关闭,避免非 Web 场景下线程不退出。编译命令用javac -cp "lib/*" SpringJarSmokeTest.java,运行用java -cp ".:lib/*" SpringJarSmokeTest,Windows 下把:换成;。如果编译报package org.springframework.context.annotation does not exist,先看lib通配符是否被 shell 展开;如果运行报日志类缺失,回到 2.1 检查 spring-jcl 或 commons-logging 是否匹配大版本。
4. 版本对齐与第三方依赖:别让 spring-core 和 spring-context 各走各的
4.1 Spring 5 与 6 的 JDK、命名空间分界
Spring jar 包的版本选择,第一道分水岭是 JDK 和 Servlet 命名空间。Spring 5.x 面向 JDK 8 及以上,Web 层使用javax.servlet命名空间;Spring 6.x 要求 JDK 17 及以上,Web 层切换到jakarta.servlet。这不是换个包名那么简单,HttpServletRequest、ServletException这些类的全限定名都变了,Spring 6 的 spring-web 不能和 javax 时代的容器接口混用。下面这张表是组包时的快速对照,实际小版本按项目依赖管理走。
| 大版本 | 最低 JDK | Servlet 命名空间 | 日志桥接 | 典型场景 |
|---|---|---|---|---|
| Spring 4.x | JDK 6/7/8 | javax.servlet | commons-logging | 老系统维护 |
| Spring 5.x | JDK 8+ | javax.servlet | spring-jcl | 主流存量项目 |
| Spring 6.x | JDK 17+ | jakarta.servlet | spring-jcl | 新工程或升级工程 |
表格里最容易踩坑的是“JDK 够但命名空间不对”。比如项目已经升到 JDK 17,但 Servlet 容器还是 javax 接口,这时把 Spring 6 的 spring-web 放进去,编译期可能因为缺少 jakarta.servlet-api 报错,运行期则可能因为容器加载不到jakarta.servlet.Servlet直接启动失败。反过来,Spring 5 项目里混入 jakarta.servlet-api,也会出现类能找到但方法签名对不上的情况。我的习惯是先确定 Servlet 命名空间,再选 Spring 大版本,最后才看小版本,顺序反了就会给自己留后悔药。
4.2 日志、JSON、Servlet API 的常见搭配
Spring 5.x 自带 spring-jcl,用来桥接常见日志门面,所以不需要再额外放 commons-logging。如果项目里已经有 commons-logging,并且和 spring-jcl 同时出现,可能出现日志工厂类冲突,表现为启动时日志初始化异常或重复绑定。处理方式不是删掉所有日志,而是保留项目统一的日志实现,把重复的门面 jar 排除掉。JSON 处理方面,Spring MVC 的@ResponseBody需要 JSON 库支持,坐标按项目现有依赖填,不要为了一个注解把不同大版本的 JSON 库混在一起。Servlet API 始终用 provided 语义,不要打进 war 包。
下面这段pom.xml片段演示排除传递依赖的写法。假设某个第三方库间接带入了旧版 spring-core,可以在该依赖下加exclusions,把旧坐标排掉,再由项目统一声明 Spring 版本。exclusion的groupId和artifactId必须写全,只写 artifactId 不生效。
<dependency> <groupId>com.example</groupId> <artifactId>some-legacy-lib</artifactId> <version>1.0.0</version> <exclusions> <exclusion> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> </exclusion> <exclusion> <groupId>org.springframework</groupId> <artifactId>spring-beans</artifactId> </exclusion> </exclusions> </dependency>这段配置只排掉旧库带进来的 Spring 坐标,不改变旧库本身。参数上,com.example和some-legacy-lib是占位,实际替换成你工程里的依赖坐标;version也按实际写。排除后必须再跑一次依赖树,确认旧版本没有从其他路径绕回来。有些库会把 Spring 坐标 shade 进自己的 jar,这种情况下exclusions无效,只能用jar tf看类路径,或者换一个不内嵌 Spring 的版本。
4.3 用依赖树和 jar 清单做版本审计
版本审计不要只看pom.xml,要看实际解析结果。下面两条命令,第一条输出包含版本冲突的详细依赖树,第二条把 Spring 相关 jar 的坐标和版本列出来。-Dverbose会显示被省略的冲突项,-Dincludes过滤组织名,输出更干净。
# 查看 Spring 相关依赖的冲突与省略原因 mvn dependency:tree -Dverbose -Dincludes=org.springframework # 只输出 Spring 依赖的 groupId:artifactId:version 列表 mvn dependency:list -DincludeGroupIds=org.springframework -DoutputFile=spring-deps.txt第一条命令重点看omitted for conflict with,它告诉你哪个版本被扔掉了、换成了哪个版本。第二条命令生成spring-deps.txt,适合贴到构建日志里做归档。参数上,includeGroupIds只过滤组织,不会过滤 artifactId,所以输出里会包含所有 Spring 模块;outputFile不要写到target以外的地方,避免污染源码目录。审计完再对照 2.3 的表格,看项目实际用到的模块是否多带了 Web 或测试包。多带不一定报错,但会增加类路径冲突概率,尤其是老工程里同时存在多个 JSON 库和日志门面时。
5. 避坑与排查:ClassNotFoundException、NoSuchMethodError 到底看哪里
5.1 现象一:启动报 NoClassDefFoundError 指向 spring-core
现象是启动时抛java.lang.NoClassDefFoundError: org/springframework/core/io/Resource,但lib里能看到 spring-core 的 jar。原因通常有三类:jar 根本没被加到运行期类路径、jar 文件损坏、或者同名类被另一个旧版本 jar 抢先加载。解决顺序是先看运行命令的-cp是否包含lib/*,再用jar tf检查目标类是否存在,最后用依赖树或find查重复 jar。不要一上来就换版本,先确认类到底在不在。
# 查 lib 下所有 spring-core,看是否有多个版本 find . -name 'spring-core-*.jar' -print # 确认目标类在 jar 内存在 jar tf lib/spring-core-5.3.30.jar | grep 'org/springframework/core/io/Resource.class'第一行输出如果出现两个不同版本,运行期加载哪个取决于类路径顺序,这就是典型的“看起来有、实际用错”。第二行没有输出,说明 jar 不完整或 artifactId 拿错了,重新取包。参数上,find的起点按工程根目录调整,grep的类名换成报错信息里的类。处理完重复 jar 后,删掉target或输出目录里的旧 class,再重启验证。
5.2 现象二:NoSuchMethodError 指向 spring-core 或 spring-context
现象是编译通过,运行时报NoSuchMethodError,堆栈指向某个 Spring 类的某个方法。原因几乎都是版本混用:编译期用新版本,运行期类路径里放了旧版本;或者两个第三方库分别依赖不同大版本,构建工具选了其中一个,但另一个库的字节码按另一个版本编译。解决方式是用依赖管理锁定版本,把传递进来的旧版本排掉,再清理lib里多余 jar。
<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-framework-bom</artifactId> <version>${spring.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>这段配置用 BOM 统一 Spring 模块版本,scope写import,type写pom。参数上,spring.version要和项目实际大版本一致,不要在一个工程里同时管理 5.x 和 6.x。BOM 只能约束 Maven 解析,不能清理已经手工拷进WEB-INF/lib的旧 jar,所以非构建工具工程还要配合 5.1 的find命令删重复。血泪经验是:NoSuchMethodError的堆栈第一行不一定是最早出问题的地方,往下翻到Caused by,找到真正加载旧类的调用链,再决定排哪个坐标。
5.3 现象三:Web 项目启动报 Servlet 相关类缺失
现象是启动时抛NoClassDefFoundError,类名指向javax.servlet或jakarta.servlet下的接口。原因分两种:servlet-api 的 scope 写成provided但容器没有提供对应版本,或者 Spring 大版本与命名空间不匹配。Spring 5 配 javax 容器,Spring 6 配 jakarta 容器,混用会在类加载阶段直接失败。解决时先看容器支持的命名空间,再改 Spring 版本或 servlet-api 坐标,不要同时放两套 API。
<!-- Spring 5 + javax 容器 --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> <!-- Spring 6 + jakarta 容器时换成下面这组,二选一,不要同时存在 --> <!-- <dependency> <groupId>jakarta.servlet</groupId> <artifactId>jakarta.servlet-api</artifactId> <version>6.0.0</version> <scope>provided</scope> </dependency> -->这段配置的关键是“二选一”。provided表示编译期可见、运行期由容器提供,所以不要把 servlet-api 打进WEB-INF/lib。如果容器本身版本较旧,只支持 javax,就不要硬上 Spring 6 的 Web 模块。参数上,4.0.1和6.0.0是示例坐标,实际按容器支持的规范版本替换。检查 war 包时用jar tf your.war | grep servlet,如果看到 servlet-api 被打进去,说明 scope 或打包插件配置有问题。
5.4 现象四:日志门面类缺失或日志重复绑定
现象是启动时报日志门面类缺失,或者日志输出出现重复行、绑定多个实现。原因在 Spring 4 和 Spring 5 的日志桥接差异:Spring 4 通常依赖 commons-logging,Spring 5 自带 spring-jcl。把 Spring 5 的 jar 和 commons-logging 混放,或者从 Spring 4 升到 5 时忘了删旧门面,都可能出问题。解决方式是按大版本保留一套日志桥接,其他门面 jar 排除掉,不要用“多放几个总有一个能用”的思路。
# 查 lib 下日志门面相关 jar,按实际文件名调整关键字 find WEB-INF/lib -maxdepth 1 \( -name '*jcl*.jar' -o -name '*commons-logging*.jar' \) -print这条命令只做一件事:把日志门面 jar 列出来。Spring 5.x 工程里看到 spring-jcl 就够了,再出现 commons-logging 就要判断是不是旧依赖带进来的。排除时用 4.2 的exclusions写法,或者手工组包时直接不拷旧门面。注意不要为了消除警告把日志实现也删掉,门面和实现是两回事,删错会出现“没有日志输出”而不是“没有冲突”。
5.5 现象五:本地能跑、打成 war 包后 404 或 500
现象是本地 IDE 启动正常,部署 war 包后接口 404,或者启动直接 500。原因通常是依赖没有打进WEB-INF/lib:本地用 Build Path 引用了外部 jar,打包时没带进去;或者 Maven 的 scope 写成provided,但运行环境并没有提供这个依赖。解决时先解压 war 包看WEB-INF/lib,再对照本地类路径,缺哪个补哪个。
# 解压 war 到临时目录,只看 WEB-INF/lib 下的 Spring jar unzip -q your.war -d /tmp/war-check find /tmp/war-check/WEB-INF/lib -maxdepth 1 -name 'spring-*.jar' -print | sort第一行-q静默解压,-d指定临时目录,避免污染工程。第二行只列 Spring jar,看数量和大版本是否与本地一致。如果 war 包里没有 spring-webmvc,但本地 IDE 能跑,说明本地类路径引用了外部 jar,打包没带上;如果 war 包里有 servlet-api,说明 scope 或打包配置把 provided 打进去了。参数上,/tmp/war-check换成可写目录,Windows 下用对应临时路径。每次改完打包配置,重新解压核对一遍,不要只看构建成功日志。
6. 离线包验收与版本升级:我给团队用的一套三步校验法
6.1 用脚本核对 jar 清单和版本一致性
拿到一份离线 Spring jar包,先别急着启动,跑一遍清单脚本。下面这段脚本列出lib下所有 Spring jar,提取版本号,排序后统计每个版本出现次数。出现两个大版本就标红处理,不要靠肉眼扫。
#!/usr/bin/env bash set -euo pipefail LIB_DIR="${1:-lib}" find "$LIB_DIR" -maxdepth 1 -name 'spring-*.jar' -print \ | sed -E 's#.*/spring-[a-z]+-([0-9]+\.[0-9]+\.[0-9]+).*#\1#' \ | sort | uniq -c | sort -nrLIB_DIR默认取lib,也可以传WEB-INF/lib。sed的正则只提取x.y.z形式的小版本,遇到带RELEASE或SNAPSHOT的文件名要按实际调整。输出里如果同一版本计数和 jar 数量对不上,说明有 jar 文件名不规范,需要人工看一眼。这个脚本不判断依赖是否完整,只解决“版本是否混用”,配合第 3 章的最小启动用例才算闭环。
6.2 最小启动用例与 Web 冒烟测试
最小启动用例通过后,再做一次 Web 冒烟:只验证 DispatcherServlet 能否初始化、一个静态资源或健康检查接口能否返回 200。不要一上来就跑全量业务,先看类路径有没有硬伤。下面这段命令用curl检查一个简单端点,-i打印响应头,-s静默进度条,-o /dev/null丢弃响应体。
curl -i -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8080/health返回200只说明容器和 MVC 初始化链路通了,不代表事务、数据库连接池没问题;返回404先看 war 包上下文路径和web.xml或注解配置;返回500去容器日志找第一个Caused by。参数上,端口和路径按项目实际替换,/health可以换成任意能触发 MVC 的只读端点。
6.3 升级 Spring 版本时先动哪几个坐标
升级时不要一次改所有 jar,按顺序动:先统一spring.version属性,再跑依赖树,再清理lib旧 jar,最后跑最小用例和 Web 冒烟。Spring 5 升 6 时,先确认 JDK 17 和 jakarta 命名空间,再改 servlet-api 坐标,最后才改业务代码里的javax导入。升级顺序错了,排查成本会翻倍。
| 步骤 | 动作 | 验证点 |
|---|---|---|
| 1 | 改版本属性 | 依赖树里同一组织只有一个版本 |
| 2 | 清旧 jar | find无重复大版本 |
| 3 | 跑最小用例 | 容器能启动并取到 Bean |
| 4 | 跑 Web 冒烟 | 端点返回预期状态码 |
| 5 | 看日志 | 无门面冲突、无类加载异常 |
从那以后我每次拿到一份离线 Spring jar包,都先跑清单脚本,再跑最小启动用例,不给自己留“应该是版本问题”的后悔药。希望帮到你。
本文还有配套的精品资源,点击获取