☰
Spring Core 5源码编译实战:基于JDK11与Gradle的断点调试及替换指南
2026/10/1 4:36:55 网站建设 项目流程

1. 动机与整体路线:为什么非要自己编译一遍SpringCore

要说清楚这件事,得先承认一个事实:绝大多数Java程序员用了好几年Spring,但你问他Spring容器到底是怎么把Bean创建出来的,他只能说出大概流程。原因很简单,看源码和调试源码是两回事。看源码是在别人写好的注释和代码里“猜上下文”,调试源码是真正跟了一遍执行链路,每个变量、每次调用、每个代理对象的产生过程都逃不过你的眼睛。

自己编译Spring源码、再替换到项目里,这事的核心价值有三个:第一是能在本地对Spring容器做断点级调试,BeanDefinition的加载、Bean的生命周期、AOP代理的创建,全都能一步步跟下来;第二是可以在源码里加日志、加自己的逻辑,做深度定制;第三是能真正理解Spring的模块划分和构建体系,以后遇到框架层面的问题,不再是两眼一抹黑。

需要提前说明的是,“编译SpringCore5源码”这个目标,完整说法是“用IDEA + JDK11 编译Spring Framework 5.x的spring-core模块,并把编译产物替换到业务项目里”。精简一点就是,你用Spring的源代码,自己打包出一份可以替代官方jar的产物,然后让Maven或Gradle项目加载它。

适合看这篇文章的人,我大致分三类:一是正在啃Spring源码、想要打断点调试的学习者;二是遇到Spring官方jar无法满足定制需求、想改源码的开发者;三是想搞清楚Gradle多模块项目和IDEA配合机制的人。如果你只是想用Spring,那这篇文章对你帮助有限,建议直接跳过。

整体路线其实就四步:选对版本、配好环境、编译spring-core、替换依赖。其中版本匹配是最大的坑,后面会花很大篇幅讲。从我的实践来看,如果你JDK、Gradle、Spring源码版本没对齐,那么编译报错几乎是必然的,而且报错信息非常误导人,比如什么“Unsupported class file major version 61”——这其实是JDK17编译的类,不是源码本身的问题。所以先别急着怀疑源码,按下面这套路线走。

2. 环境准备:JDK11与Spring 5.x的版本匹配是大事

2.1 为什么额外地强调JDK11

Spring Framework 5.x从5.1开始全面支持JDK11,但这里有个微妙的地方:Spring 5.2以前的版本虽然能跑在JDK11上,但官方构建时用的还是JDK8,编译产物和Gradle插件体系都没有完全适配。真正把JDK11作为一等公民,是从5.2开始的。所以你如果选的是5.1.x,硬要用JDK11编译,Gradle版本、插件兼容性会折腾你很久;选5.2.x及以上,配合JDK11就顺滑很多。

另外提一句,JDK11和Spring Boot 2.2.x、Spring Framework 5.2.x是官方推荐组合。做这个事的时候,尽量把环境往官方推荐组合靠,别用自己的“喜欢”去挑战官方已验证过的构建链。

Windows和macOS我都试过,建议直接用OpenJDK或Temurin的JDK11。Oracle JDK11也行,但下载麻烦一点。装完之后确认两件事:IDEA里Project Structure的Project SDK要选11,Gradle JDK也要选11。很多人在IDEA里能编译,到命令行就报错,或者反过来,基本都是这里没对齐。

2.2 源码版本与Gradle版本的对应关系

这一步非常关键,直接决定你后面能不能编译通过。Spring Framework每个release版本都会捆绑一个Gradle Wrapper版本,你在源码根目录的gradle/wrapper/gradle-wrapper.properties里就能看到。但Spring 5.2.x用的Gradle 5.6.4,5.3.x用的Gradle 6.x,如果你直接从GitHub拉master分支,那对应的是Gradle 7甚至Gradle 8,对JDK的要求直接跳到17。

所以我的建议是:不要用master分支,选release tag。比如spring-core要用5.2.15.RELEASE,就去GitHub的spring-projects/spring-framework仓库,切到v5.2.15.RELEASE这个tag。这个tag对应的Gradle版本是5.6.4,刚好和JDK11是兼容区间。

版本对应关系我整理过一张表,供参考:

Spring版本推荐JDKGradle Wrapper版本是否推荐
5.1.xJDK8/114.10+不推荐,插件兼容性差
5.2.xJDK115.6.x非常推荐,最稳定
5.3.xJDK11/176.x/7.x可以,但5.3编译cglib相关有坑
masterJDK177.x/8.x不推荐,需要额外的工具链

2.3 下载源码与IDEA初次导入

源码下载我就直接说命令:

git clone --branch v5.2.15.RELEASE --depth 1 https://github.com/spring-projects/spring-framework.git

加--depth 1是只拉当前版本的单条历史,速度快非常多。如果你想切到5.3.x,把branch参数换成对应的tag名就行。

下载完成后,用IDEA的Open功能打开源码目录,IDEA会自动识别Gradle项目。这一步不会立刻开始编译,它只是先读项目结构。初次导入会比较慢,因为Gradle需要下载插件和依赖。如果你之前没在IDEA里配置过Gradle,强烈建议在Settings里把Gradle user home指到一个有空间的目录,默认是在用户目录下的.gradle,这个目录会越来越大。

初次导入还有个很容易忽略的点:Spring源码里有不少模块是Kotlin写的,Gradle需要下载Kotlin插件。源码里还有spring-aspects这种需要AspectJ编译器的模块。如果你只想编译spring-core,其实用不到这些,但IDEA的Gradle同步会把整个项目的插件都尝试解析一遍,所以耐心等,这一步别中断。

3. 编译前的关键配置:仓库镜像与模块裁剪

3.1 Gradle仓库改成国内镜像

这一步不做,编译大概率会卡死在依赖下载上。Spring源码的build.gradle里配置的仓库是repo.spring.io和Maven Central,在国内网络环境下,这些地址慢得让人绝望,而且有些依赖还会因为校验问题反复失败。

我改仓库的方式是在源码根目录的settings.gradle和build.gradle里,把repositories统一改成阿里云镜像。注意settings.gradle里也有pluginManagement仓库,那个也要改,不然Gradle插件下载不顺畅。

以5.2.15.RELEASE为例,build.gradle开头找到allprojects或repository定义的地方,替换成:

allprojects { repositories { maven { url 'https://maven.aliyun.com/repository/central' } maven { url 'https://maven.aliyun.com/repository/public' } maven { url 'https://maven.aliyun.com/repository/gradle-plugin' } mavenLocal() } }

settings.gradle里如果有pluginManagement,同样加镜像:

pluginManagement { repositories { maven { url 'https://maven.aliyun.com/repository/gradle-plugin' } maven { url 'https://maven.aliyun.com/repository/public' } mavenCentral() } }

改完之后记得在IDEA里重新加载Gradle项目,让配置生效。

3.2 只编译spring-core:别傻乎乎跑全量build

Spring源码是个非常庞大的多模块Gradle项目,模块数量几十个,全量./gradlew build会编译所有模块、跑所有测试,耗时几个小时都很正常,而且失败率极高。你只是要spring-core,那就只编spring-core。

在Spring 5.2.x里,spring-core模块是基础的,但它也有依赖模块:spring-jcl(spring的commons-logging桥)。另外spring-core的测试代码依赖spring-beans,如果你只想编译main代码,其实不需要spring-beans。但如果你想用IDEA里打开源码并跳转调试,建议至少把spring-jcl也一起编出来。

命令层面,最干净的是:

./gradlew :spring-core:compileJava :spring-jcl:compileJava

如果你要连测试一起编译(一般不需要,测试编译麻烦且容易遇到环境问题),再加:spring-core:compileTestJava。但我不建议加,跳过测试能省一大半时间,而且测试编译失败并不影响你使用编译产物。

3.3 IDEA中的JDK与Gradle设置

现在很多人会在IDEA里直接双击Gradle任务面板去编译,这没问题。但IDEA里的Gradle JVM设置经常是默认的,比如你系统装了JDK17,IDEA默认用的就是17。而Spring 5.2.x的Gradle 5.6.4跑在JDK17上,基本百分百报错。所以一定要手动设置。

具体操作是:IDEA设置里搜Gradle,找到Gradle JVM,选择你安装的JDK11。同时Project Structure里Project SDK也选11,Language Level选11。两个地方对齐之后,再执行Gradle任务。

设置完后有个小技巧:在IDEA终端里执行./gradlew --version,确认一下Gradle运行时用的JVM是不是11。输出里会显示JVM版本,如果不是,多半是环境变量JAVA_HOME的问题,改成JDK11的路径即可。

4. 实操编译:命令行与IDEA双路线

4.1 命令行编译(最稳的方式)

我个人最推荐命令行编译,原因很简单:输出信息完整、报错定位准确、不会因为IDEA缓存问题抽风。在源码根目录执行:

./gradlew :spring-core:compileJava --no-daemon

--no-daemon可选,但建议加上,因为Gradle Daemon如果之前是用高版本JDK启动的,你切换JDK后Daemon没重启,编译还是会用旧JDK,很容易出现诡异问题。不加的话,如果遇到“Unknown JVM version”之类的报错,先跑一下./gradlew --stop停掉所有Daemon。

第一次编译会下载一堆依赖,等就行。编译成功后控制台会输出BUILD SUCCESSFUL,同时spring-core模块下会出现build/classes/java/main目录,里面是编译好的class文件;jar包在build/libs/spring-core-5.2.15.RELEASE.jar。

要注意,compileJava只编译main源码,不会生成带Sources的jar。如果你想看源码链接,后面要执行:

./gradlew :spring-core:jar

这个命令会同时生成spring-core-5.2.15.RELEASE.jar和spring-core-5.2.15.RELEASE-sources.jar。

4.2 在IDEA中编译

IDEA里编译其实就是在Gradle工具窗口里找到spring-core子模块,展开Tasks里的compileJava任务,双击执行。效果和命令行一样,但IDEA有个好处:编译失败时点错误信息可以直接跳到源码位置。

不过IDEA编译有个毛病,它会先执行Gradle同步,同步过程如果有些模块的插件解析失败,会导致整个任务面板灰色不可用。这时候别死磕IDEA,回到命令行把./gradlew :spring-core:compileJava跑通,然后再回IDEA刷新Gradle项目,问题通常就解决了。

另外,IDEA里编译完成后,如果你在源码里写了断点,想跑一个测试类来调试,我这里建议直接写一个最简单的main方法测试类放到spring-core\src\test\java下,然后右键运行。IDEA的Gradle项目支持直接运行测试类,断点能命中的。

4.3 编译产物与class文件验证

编译完,产物都在各个模块自己的build目录下。验证编译质量的一个粗暴手段是直接查看class文件的Java版本,用javap:

javap -v build/libs/spring-core-5.2.15.RELEASE.jar 中的某个类

更简单的方式是直接用IDEA打开jar包里的class,能看到major version 55(对应JDK11)就行。

补充一句:你编译出的jar和官方jar在功能上是等价的,但因为编译环境和JDK版本差异,jar包的MANIFEST、源码里的Automatic-Module-Name不会变,只是构建时间戳不同。所以替换的时候完全不需要担心兼容性。

5. 编译过程中最常见的坑与排查实录

5.1 版本不匹配:Unsupported class file major version 61

这个错误我见到过太多次了。提示是“Unsupported class file major version 61”,不懂的人以为是某个依赖有问题,实际上major version 61对应JDK17。Spring 5.2.x的构建链里某些插件生成的临时class是用的JDK17,Gradle 5.6.4根本不认。

解决办法只有一条路:把JDK切到11,并清掉Gradle缓存。注意如果Gradle Daemon之前用JDK17启动过,光切JDK没用,要./gradlew --stop,必要时删除用户目录下.gradle/caches里的相关模块缓存再重试。

还有个隐藏坑:你项目里可能通过JAVA_HOME指向JDK17,但Gradle JVM设置的是11,结果IDEA里编译没问题,命令行却报错。所以检查环境变量要彻底。

5.2 Kotlin与注解处理相关的报错

Spring源码里有若干Kotlin模块,IDEA的Gradle同步阶段,会尝试下载Kotlin插件和编译Kotlin标准库。常见报错是:

Could not resolve org.jetbrains.kotlin:kotlin-gradle-plugin:1.x.x

这基本就是镜像没配全,或者Gradle插件仓库地址没改。按前面第3节的配置把pluginManagement仓库改好,基本能解决。

另外还有类找不到的报错,比如Cannot access class 'kotlin.coroutines.experimental.Continuation',不用慌,这是spring-core测试代码里引用的Kotlin库版本问题。如果你只需要main代码,跳过测试编译就行,不影响。

5.3 依赖下载失败与Gradle缓存问题

典型症状是编译进度条卡在90%左右,最后报错某个依赖无法解析。原因通常是仓库地址访问超时,或者jar包损坏。我的排查顺序是:改镜像仓库、清Gradle缓存(只清caches/modules-2/files-2.1里的对应group目录)、重试。

这里有个经验之谈:不要轻易删整个.gradle目录,除非你已经确认只想重新下一遍。删整个目录后,Gradle要重新下载全部插件和依赖,半小时起步。精确删除失败的依赖目录,通常几分钟能恢复。

5.4 IDEA里源码跳转不正确

编译成功后,在IDEA里打开源码类,比如DefaultListableBeanFactory,发现跳转进去的是反编译代码而不是源码。原因是你项目依赖的spring-core是官方jar,而官方jar默认不带sources。解决办法是把自己编译的sources jar配置到依赖里,或者干脆用Gradle的sourceSets指定源码目录。

在IDEA里,最简单的方式是:右键spring-core模块的build.gradle,点“Add as Gradle Project”后,IDEA会自动关联源码。如果不行,就在Project Structure里手动把build/libs/spring-core-5.2.15.RELEASE-sources.jar加到依赖的Sources里。

6. 替换项目里的SpringCore:三种方式

编译只是手段,替换到项目里才是目的。这一步根据你项目的构建工具不同,有不同做法。我用的是Maven项目,所以重点说Maven,Gradle项目会补充说明。

6.1 方式一:通过本地Maven仓库替换

这是最规范的做法。把你编译出的jar安装到本地Maven仓库,然后项目pom.xml里引入SNAPSHOT版本。具体命令:

mvn install:install-file -Dfile=build/libs/spring-core-5.2.15.RELEASE.jar \ -DgroupId=org.springframework \ -DartifactId=spring-core \ -Dversion=5.2.15.RELEASE-custom \ -Dpackaging=jar

然后在你的项目pom.xml里,显式覆盖版本:

<dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>5.2.15.RELEASE-custom</version> </dependency>

这里有个极容易踩的坑:Spring官方依赖里,spring-core的版本通常由spring-boot-dependencies BOM统一管理。你只改spring-core的版本还不够,因为spring-beans、spring-context等还会引用官方版本,Spring的模块之间会校验版本一致性吗?答案是:不会,但类型不匹配时会出现诡异的NoSuchMethodError。所以如果你只是单纯替换spring-core,我建议把它对应版本的一整套模块都换成本地编译产物,别只换一个,除非你的改动只在spring-core内部且签名完全不变。

我用install-file方式替换后,启动项目时需要加-Dspring-core=debug之类的参数做验证(后面会讲)。

6.2 方式二:直接替换jar包

如果你用的是IDE内置的依赖管理,或者你想快速测试,直接替换jar是最快的。做法是:把你编译的spring-core jar复制到项目lib目录,然后在IDEA的Project Structure里把它放到依赖列表最前面,优先级高于Maven仓库的官方jar。

但这种方式比较脏,Maven坐标没变,只是本地lib优先。如果项目里有多个模块,每个模块都要配一遍,很容易漏。而且团队协作的时候别人拉代码没法复现你的环境,我不推荐,除非你只是临时验证某个功能。

6.3 方式三:Gradle项目中使用源码依赖

如果你是Gradle项目,替换更简单,直接在build.gradle里把依赖改成项目源码依赖:

implementation project(':spring-core')

但这里有个前提:你的项目必须和Spring源码项目在同一个Gradle构建里,或者通过includeBuild引入。如果你只是想用本地jar,更轻量的做法是像方式一那样先install到mavenLocal,然后Gradle里写:

implementation 'org.springframework:spring-core:5.2.15.RELEASE-custom'

然后配置依赖解析策略,优先使用本地仓库。

6.4 验证是否真的用了本地jar

替换完,必须验证启动时候加载的是不是你的本地产物。我常用的方法有三种:

第一,在源码里加一行独一无二的日志,比如改AbstractApplicationContext类的refresh方法开头加一句System.out.println("custom spring core loaded"),然后重新编译、install、启动项目,看控制台有没有这行输出。

第二,不用改源码,直接看启动日志里的Spring版本和jar路径。在pom中引入spring-core后用mvn dependency:tree看版本号,如果显示5.2.15.RELEASE-custom就是对的。

第三,用运行时Class对象验证:

Class<?> clazz = Class.forName("org.springframework.core.SpringVersion"); System.out.println(clazz.getProtectionDomain().getCodeSource().getLocation());

输出路径如果指向你本地仓库的custom jar,就说明替换成功。这个方法最直接,能看到jar的绝对路径。

7. 替换后的调试心得与个人经验

7.1 断点调试Spring容器初始化

替换成功后,我最常用的场景就是在IDEA里给Spring源码打断点。比如我想知道AnnotationConfigApplicationContext构造时,AnnotatedBeanDefinitionReader是怎么注册内置后置处理器的,直接在源码对应行的左侧打断点,然后启动一个最小的Spring测试类:

public class SpringCoreDebugTest { public static void main(String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MyConfig.class); MyService service = context.getBean(MyService.class); service.doSomething(); } }

因为依赖已经指向本地custom jar,IDEA会优先加载源码目录里的class,断点就能命中。你可以在refresh()方法入口打断点,跟着整个容器初始化流程走一遍,走完你会对Spring的BeanFactoryPostProcessor、BeanPostProcessor、Bean生命周期有一个远超书本的理解。

我个人的建议是:一开始别在太深层的地方打断点,先在AbstractApplicationContext.refresh()里逐行F7跟进去。这个方法是Spring容器的总纲,从这一步走下去,你会看到obtainFreshBeanFactory、invokeBeanFactoryPostProcessors、registerBeanPostProcessors、finishBeanFactoryInitialization这些关键步骤。跟完一遍,你再看任何讲Spring的博客,都会觉得清清楚楚。

7.2 魔改源码的边界与注意事项

自己编译源码的一个很大诱惑是:直接改源码来满足业务需求。这件事能做,但有边界。我踩过的坑是这样的——我给DefaultListableBeanFactory加了个自定义的Bean注册逻辑,修改后功能正常,但后来Spring升级到5.3.x,这个改动完全没法迁移,相当于锁死在了5.2.15上。

所以我的建议是:如果你的改动只是加日志、加断点、调整某个内部方法的执行顺序,那完全OK;但如果你改了Spring对外API的签名或语义,那必须考虑长期的维护成本。另一个建议是改动尽量集中在一两个类里,用注释标记清楚,以后版本升级时搜索标记就能找到所有改动点。

另外,改完源码重新编译,可能遇到增量编译没生效的问题。比如你改了AbstractApplicationContext,但重新启动发现改动没生效。这种情况检查一下Gradle是否真的重新编译了对应类:看build/classes/java/main里class文件的时间戳。如果没更新,执行一次./gradlew :spring-core:clean :spring-core:compileJava强制重编。

7.3 关于版本锁定的思考

最后分享一点带点“个人态度”的经验:自己编译源码这件事,本质上是一次“版本锁定”。你一旦把本地编译的jar替换到项目里,就意味着项目的Spring版本由你本地代码决定,官方后续的bug修复和安全补丁都不会自动生效。所以生产环境我完全不建议这么干,除非你有极强的理由。

我的典型用法是:在个人学习项目、或者本地开发调试环境里替换,验证Spring的某个行为是否符合我的理解;生产环境永远用官方发布版本。如果你确实需要定制框架能力,先考虑Spring官方提供的扩展点——BeanFactoryPostProcessor、BeanPostProcessor、ImportBeanDefinitionRegistrar、FactoryBean,这些扩展点覆盖了绝大多数需求。实在不够,再考虑魔改源码。

实际做下来,一次完整的编译替换流程耗时大约40分钟到1小时,其中大部分时间是等Gradle下载依赖。如果你第一次做,编译报错是常态,请一定把版本对应关系放第一位排查。等这条路走通之后,你再看Spring源码就会有种“这是我的框架”的感觉——因为它确实,某种意义上,是你的框架了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询