
说实话spring源码编译这件事劝退了不知道多少想读源码的人。很多朋友打开GitHub仓库的时候热血沸腾结果照着网上的教程一编译就是一整天报错报得怀疑人生。我第一次编译的时候也踩了一堆坑从Gradle版本不匹配到依赖下载失败从AspectJ模块编不过到IDEA导入后疯狂标红折腾了两天。这篇内容我就把自己折腾spring源码编译的完整经历和排坑方法整理出来包括环境怎么配、脚本怎么改、命令怎么敲、报错怎么解决以及编译成功后怎么在本地引用自己编译出来的jar。适合下面三类人看一是准备认真啃Spring源码、想在代码里加注释断点的人二是工作中需要做Spring二次开发或自定义扩展的人三是面试前想通过源码加深对IoC、AOP、事务这些核心机制理解的人。看完这篇文章你至少能少走两天的弯路。1. Spring源码编译为什么劝退了一大波人1.1 Spring不是单个项目而是一个几十个模块组成的Gradle工程很多人对Spring源码的第一印象可能是“一个普通的Maven项目”毕竟平时自己写的Spring Boot工程都是Maven管理的。但Spring Framework官方用的是Gradle而且它不是一个单模块工程是由spring-core、spring-beans、spring-context、spring-aop、spring-web、spring-tx、spring-aspects等几十个模块组成的多模块构建。这意味着你编一个模块它可能依赖另外好几个模块而这些模块又可能依赖别的第三方库。编译链路比想象中长得多。比如你要编spring-context它依赖spring-core、spring-beans、spring-aop这几个兄弟模块而spring-core里还内嵌了重新打包的cglib和objenesis这些类不是从中央仓库直接拉一个独立jar就能搞定的是构建过程中由spring-core自己生成和打包的。所以如果直接跳过spring-core去编译其他模块基本必挂。理解这个结构后你就明白为什么网上各种教程喜欢强调“先编译spring-core”。不是玄学是模块依赖决定的。1.2 版本匹配是最大的隐形炸弹Spring源码对JDK版本、Gradle版本的要求非常严格。Spring 5.x老版本用JDK 8就能编但Spring 6.x强制要求JDK 17以上。Gradle版本也不是随便装的每个Spring版本的gradle/wrapper/gradle-wrapper.properties文件里写死了期望的Gradle版本如果你本机Gradle版本和它不匹配构建时会有一堆奇奇怪怪的报错。我用表格整理一下常见Spring版本和环境的对应关系方便你对照Spring源码版本最低JDK要求推荐JDK对应Gradle Wrapper大致版本5.2.xJDK 8JDK 8/11Gradle 5.6.x5.3.xJDK 8JDK 11/17Gradle 7.x6.0.xJDK 17JDK 17Gradle 7.56.1.xJDK 17JDK 17/21Gradle 7.6/8.x6.2.xJDK 17JDK 17/21Gradle 8.x注意这里的Gradle版本不是绝对的以仓库里gradle/wrapper/gradle-wrapper.properties中配置的版本为准。但JDK版本一定要满足特别是Spring 6.x用JDK 8编译会直接报class file版本错误。1.3 国外的仓库地址是慢工出细活的“同义词”spring源码编译过程会从Maven Central以及repo.spring.io官方仓库拉取大量依赖。国内网络环境下直接下载经常是几十KB每秒有时候直接超时失败。这是很多人卡在第一关的原因——不是你的配置有问题是网络确实不给力。解决办法是在构建脚本里加入国内镜像仓库。这块我在下一部分详细说因为改仓库配置也有讲究改错了反而会引入新的问题。2. 编译前配置先把环境里的雷排掉2.1 获取Spring源码的两种方式获取Spring源码有两种常用方式一种是用Git克隆官方仓库另一种是直接到GitHub的release页面下载对应版本的zip压缩包。我建议如果你主要是为了读代码和学习直接下载带版本号的zip更稳定比如spring-framework-6.1.x.zip。因为官方仓库的main分支永远是最新版本可能还在变动对应的构建配置也可能不完整。如果你要做二次开发并长期维护再用Git克隆固定分支git clone -b v6.1.14 https://github.com/spring-projects/spring-framework.git注意Windows用户下载zip解压后源码目录路径不要嵌套太深尽量放到D:\spring-framework这种短路径否则后面Gradle构建时容易因为路径过长报错。2.2 修改仓库配置给Gradle换个“下载源”拿到源码后不要急着构建先打开根目录下的build.gradle文件找到repositories配置。默认情况下它是这样的repositories { mavenCentral() maven { url https://repo.spring.io/release } if (version.contains(-)) { maven { url https://repo.spring.io/milestone } } if (version.endsWith(BUILD-SNAPSHOT)) { maven { url https://repo.spring.io/snapshot } } }这段配置本身没毛病但在国内网络环境下repo.spring.io经常连接缓慢甚至超时。我的做法是在mavenCentral()前面加入阿里云镜像仓库repositories { maven { url https://maven.aliyun.com/repository/public } maven { url https://maven.aliyun.com/repository/spring } mavenCentral() maven { url https://repo.spring.io/release } }除了build.gradle里的依赖仓库还有一个地方很容易漏掉就是settings.gradle文件里的插件仓库pluginManagement。Spring构建会用到io.spring.javaformat、io.spring.nohttp、org.jetbrains.kotlin.jvm等Gradle插件这些插件默认从Gradle Plugin Portal下载。国内访问这个门户站点也不稳定。可以在pluginManagement里加上阿里云的Gradle插件镜像pluginManagement { repositories { maven { url https://maven.aliyun.com/repository/gradle-plugin } maven { url https://maven.aliyun.com/repository/spring-plugin } gradlePluginPortal() } }2.3 gradle.properties配置内存和并行编译Spring源码体量不小全量编译时Gradle默认的JVM内存经常不够用会出现OutOfMemoryError。建议在项目根目录下的gradle.properties里显式加大内存org.gradle.jvmargs-Xmx4g -XX:MaxMetaspaceSize1g -Dfile.encodingUTF-8 org.gradle.paralleltrue org.gradle.cachingtrue org.gradle.daemontrue-Xmx4g是根据大多数8G/16G内存机器的经验设置的如果你内存充足设置成-Xmx8g编译速度会更快。org.gradle.paralleltrue可以让多个模块并行编译节省时间。org.gradle.cachingtrue开启构建缓存之后增量编译会快很多。2.4 JDK版本环境切换编译Spring 6.x建议安装JDK 17并且要把JAVA_HOME环境变量、IDEA的Project SDK、Gradle JVM三处都统一指向同一个JDK。命令行下临时切换JDKexport JAVA_HOME/path/to/jdk-17 export PATH$JAVA_HOME/bin:$PATH java -versionWindows用户可以在当前终端窗口里临时设置set JAVA_HOMEC:\path\to\jdk-17 set PATH%JAVA_HOME%\bin;%PATH% java -version确定java -version输出版本正确后再继续不要偷懒跳过这步。很多编译失败的根因就是JDK版本不对。3. 一步一步实操命令行编译与IDEA导入3.1 先跑通命令行编译这是最稳的路径我第一次编译Spring源码上来就用IDEA直接打开工程结果IDEA疯狂下载Gradle、下载依赖界面卡到怀疑人生最后还报了一堆错。后来研究明白正确顺序是先命令行编译再用IDEA导入。打开终端切到spring源码根目录执行./gradlew clean compileJavaWindows环境下命令是gradlew.bat clean compileJava这个命令会做两件事先下载gradle-wrapper.properties里指定的Gradle发行版然后编译所有模块的主代码。整个过程会比较久第一次可能要半小时以上主要耗时在下载Gradle发行版和各种依赖。-x test要不要加如果你执行的是build任务建议加上-x test -x compileTestJava因为Spring的测试代码依赖大量第三方测试框架编译和运行测试都极慢而且很多测试需要外部的数据库、Redis等中间件环境本地跑必挂。但我这里直接用的compileJava任务它本身不依赖测试代码所以不需要额外加-x参数。如果你只想编译自己关心的模块比如只需spring-core和spring-context可以用这种精确指定子模块的方式./gradlew :spring-core:compileJava :spring-beans:compileJava :spring-context:compileJava这种方式的好处是编译速度快、失败时定位问题更精准。坏处是模块之间有依赖关系你指定的模块依赖缺失时还是会报错。我的经验是第一次编译直接用全量compileJava以后改动代码后增量编译再指定模块。3.2 spring-aspects这个“另类”模块所有模块里spring-aspects是最容易让人崩溃的一个。它依赖AspectJ编译器而AspectJ本身又依赖Spring的某些类先有鸡还是先有蛋的循环依赖问题很容易在这里爆雷。从Spring 5.x开始spring-aspects的构建脚本里通过Gradle的AspectJ插件来编译如果你单独只编spring-aspects经常会遇到找不到AspectJ编译器或者aspectjweaver依赖的问题。实际经验是全量compileJava能过spring-aspects基本也能过。如果全量编译时报spring-aspects相关错误大概率不是它本身的问题而是前序核心模块没有编译成功导致它拿不到依赖的jar。你可以先单独把核心模块编一遍./gradlew :spring-core:compileJava :spring-beans:compileJava :spring-aop:compileJava然后再回到全量编译。3.3 IDEA导入源码项目的正确姿势命令行编译成功后再用IDEA导入Spring源码就顺利多了。在IDEA中选择Open选中spring源码根目录IDEA识别到settings.gradle后会把它作为Gradle工程导入。这里有几个关键配置点第一Settings - Build Tools - Gradle里把Gradle JVM选成和命令行一致的JDK 17不要用默认的JDK 21或其他版本否则可能触发Gradle与JDK不兼容的问题。第二Settings - Build Tools - Gradle - Runner里“Delegate IDE build/run actions to Gradle”这个选项建议勾选让IDEA的编译动作交给Gradle执行避免IDEA内置编译器出现和Gradle不一致的结果。第三导入后IDEA会开始同步索引右下角会有进度条这个过程不要中断也不要急着点“Reload All Gradle Projects”。源码文件很多首次索引可能需要5到10分钟。第四如果IDEA提示缺少Kotlin插件一定要安装。Spring源码中有不少Kotlin代码主要集中在spring-webflux等模块不装Kotlin插件的话这些文件会标红而且Gradle同步时也可能报错。3.4 修改源码后怎么增量编译如果你是奔着二次开发来的改完源码后重新编译的频率会非常高。这时候再用全量compileJava就不划算了。推荐的做法是在IDEA里直接用Gradle工具窗口找到目标模块的compileJava任务双击执行。IDEA的Gradle工具窗口一般位于右侧边栏找到Tasks - build - compileJava。如果你是在命令行操作同样的命令再跑一遍就行。Gradle有增量编译机制只改了一个文件时重新编译通常几秒到十几秒就完成了。3.5 把编译产物发布到本地Maven仓库编译出来的jar默认在对应模块的build/libs目录下但如果你的业务项目想直接引用这套修改过的Spring jar更优雅的方式是发布到本地Maven仓库./gradlew publishToMavenLocal -x test执行完后所有模块的jar都会安装到~/.m2/repository/org/springframework/目录下。然后你在自己的Maven或Gradle项目里依赖对应版本号就能引到本地jar。注意如果只想发布部分模块可以指定模块名./gradlew :spring-context:publishToMavenLocal -x test这样发布后你的项目里如果依赖spring-contextMaven会优先从本地仓库找到它。4. 高频报错与排查方案实录4.1 “Could not find”“Could not resolve”依赖下载失败这是出现频率最高的报错比如Could not resolve org.springframework:spring-core:6.1.14或Could not find org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.x这类报错的原因无非两种一是网络问题依赖下载不到二是模块间依赖顺序问题某个模块还没发布到本地仓库另一个模块就已经去找它了。网络问题的解法就是前面说的把build.gradle和settings.gradle里的仓库配置都改成包含阿里云镜像。改完后如果之前已经有部分依赖缓存损坏建议把Gradle缓存里对应目录删掉rm -rf ~/.gradle/caches/modules-2/files-2.1/org.springframework删缓存这个操作要坚持“只删出问题的模块”不要一上来就清空整个缓存目录否则下次构建又是一场漫长的下载。模块间依赖顺序的解法是先编译并发布被依赖模块。比如提示找不到spring-core就先单独把spring-core编译并发布到本地仓库./gradlew :spring-core:publishToMavenLocal -x test再回过来编其他模块。4.2 报错Unsupported class file major version这类报错长这样Unsupported class file major version 61major version 61对应JDK 17major version 65对应JDK 21。这个报错通常意味着你当前使用的Gradle版本不支持这个JDK版本编译出的class文件或者是你用低版本JDK想编只支持高版本JDK的源码。排查方法很简单先确认Spring源码版本要求的JDK再看java -version把JDK统一到正确版本。如果Spring 5.3.x用JDK 17编译时出现Gradle版本过旧的问题可以检查gradle-wrapper.properties里的distributionUrl对应升级到高版本Gradle。但如果这个分支的Gradle wrapper本来就配置得比较老建议直接用Spring官方测试通过的JDK版本比如Spring 5.3.x用JDK 8或11更稳。4.3 Kotlin/Gradle插件解析失败报错一般是Plugin [id: org.jetbrains.kotlin.jvm] was not found in any of the following sources或者Failed to apply plugin [id org.jetbrains.kotlin.jvm]问题出在Gradle插件仓库没配置好Gradle无法下载Kotlin插件。解决方法就是在settings.gradle的pluginManagement.repositories里添加阿里云gradle-plugin镜像然后重新同步。如果镜像配置之后仍然失败还有一个笨办法手动到阿里云镜像仓库页面搜索对应的插件坐标确认这个版本确实存在再根据报错里的版本号去gradle.properties或build.gradle里调整版本。4.4 编译过程中内存溢出常见的报错是java.lang.OutOfMemoryError: Metaspace或者GC overhead limit exceeded这个最好解决在gradle.properties里加大JVM参数就行org.gradle.jvmargs-Xmx8g -XX:MaxMetaspaceSize2g另外IDEA本身的内存也建议调大在Help - Change Memory Settings里设置到4G以上。Spring源码在IDEA里索引非常吃内存设置小了导入后也会频繁卡死。4.5 Windows下的路径过长和脚本问题Windows环境编译Spring源码有两个经典问题。第一个是路径过长。Gradle在Windows下会生成很深的路径超过260字符就会报错。解决方法是把源码放在短的根路径下比如D:\springfw并开启Windows的长路径支持。具体操作是打开注册表编辑器定位到HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\FileSystem把LongPathsEnabled值从0改成1重启电脑。第二个是命令用错。Windows下不能直接敲./gradlew要用gradlew.bat。很多Windows用户第一次操作时卡在这里。4.6 测试代码编译失败如果你执行的是./gradlew build或者./gradlew compileTestJava大概率会碰到测试模块编译失败的问题报错里可能涉及testcontainers、H2等依赖或者某些测试类找不到外部中间件。这个问题不要硬解。Spring官方测试覆盖很广有些测试确实需要外部环境。本地学习场景编译主代码就够了。如果你确实需要跑某个核心模块的测试比如spring-beans的测试可以单独指定模块再过滤测试./gradlew :spring-beans:test --tests org.springframework.beans*如果依赖还是有问题那就老老实实先publishToMavenLocal把模块装到本地仓库再跑测试。4.7 IDEA导入后大量标红但命令行编译正常这种情况遇到过好几次命令行走得通IDEA里全是红波浪线。绝大多数原因是IDEA里模块依赖没解析出来。先等Gradle同步和索引完成有时候是索引还没建完导致的临时标红。如果索引完成后还标红检查IDEA的Gradle配置里Gradle JVM是否和命令行用的JDK一致然后在Gradle工具窗口点一次Reload All Gradle Projects。还有一个常见原因是IDEA里禁用了某些模块比如spring-aspects编译不通过时有人会手动排除这个模块结果其他模块对spring-aspects的依赖全部标红。正确的处理方式不是排除模块而是把spring-aspects编译通过。4.8 常见问题速查表我把上面这些报错整理成一张速查表方便你对照报错信息根因快速解法Could not find/could not resolve依赖下载失败或模块顺序问题配置阿里云镜像先发布会依赖的模块到本地仓库Unsupported class file major versionJDK/Gradle版本不匹配统一JDK和Gradle版本Plugin was not foundGradle插件仓库缺失settings.gradle中配置pluginManagement镜像OutOfMemoryError / GC overheadGradle内存不足gradle.properties加大JVM内存路径过长Windows路径限制源码放短路径开启LongPathsEnabledcompileTestJava失败测试依赖外部环境跳过测试编译只执行compileJava5. 编译之后的玩法验证产物与二次开发5.1 确认编译产物编译完成后每个模块生成的jar在对应目录的build/libs下例如spring-core/build/libs/spring-core-6.1.14.jar。你可以用jar命令看一眼里面是否有熟悉的classjar tf spring-core/build/libs/spring-core-6.1.14.jar | grep BeanFactory能看到org/springframework/beans/factory/BeanFactory.class就说明编译没问题。5.2 在业务项目中引用本地Spring jar编译完成且发布到本地Maven仓库后如何确认这套自编译的Spring是有效的最直观的验证方式是新建一个最简单的Spring工程在pom.xml或build.gradle里强制指定本地仓库里的Spring版本。比如你编译的是6.1.14就在依赖里写死dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version6.1.14/version /dependency如果本地仓库有这套jarMaven会优先用本地的不会再远程下载。然后写一个带Configuration和Bean的最小化启动类跑通一个Bean的获取基本就验证成功了。为了进一步确认用的是自编译jar而不是中央仓库的可以在自编译jar里加个打印或者改一个类的日志输出运行应用时如果能观察到变化说明确实引用上了。5.3 调试Spring核心流程的断点建议自己编译源码最大的好处是能直接在源码里打断点调试。我强烈建议你在IDEA里打开AbstractApplicationContext的refresh()方法在finishBeanFactoryInitialization这一行加个断点然后运行一个最简单的Spring启动类你会看到整个IoC容器初始化流程一步一步走完比死记八股文强十倍。想看循环依赖处理逻辑在DefaultSingletonBeanRegistry的getSingleton方法里打断点配合三级缓存源码把“三级缓存怎么解决循环依赖”这个问题直接看透。想看AOP代理创建过程去AbstractAutoProxyCreator的wrapIfNecessary方法打断点能清晰看到代理对象什么时候创建、Advisor怎么匹配。5.4 在源码上做实验的推荐方向如果你编译源码不只是为了看还想动手改一改我推荐几个比较安全的练手方向第一个是在DefaultListableBeanFactory里加日志打印每个Bean的创建耗时理解Bean实例化的开销分布。第二个是修改ClassPathBeanDefinitionScanner的默认过滤规则加一个自定义注解扫描测试感受Spring扫描机制的实际代码路径。第三个是给AbstractApplicationContext的refresh方法里某个钩子方法加扩展逻辑然后通过继承ApplicationContext的方式让改造成效比较接近实际二次开发的玩法。这些实验都建立在编译通过的基础上一旦编译环境畅通改代码-编译-运行-验证的循环很快学习效率会高很多。5.5 源码调试的隐藏技巧在IDEA里调试Spring源码时强烈建议打开“Debug”面板的“Drop Frame”功能。当你走入一个源码方法后想重新走一遍不用重启应用直接Drop Frame回到调用处重新进入。另外Spring源码里有很多if (logger.isDebugEnabled())之类的日志代码调试时如果想看内部日志输出在src/main/resources下放一个log4j2.xml或者logback.xml把Spring的日志级别调成DEBUG能看到容器初始化的完整过程对理解流程很有帮助。最后分享点个人体会编译一次Spring源码比我之前看十篇源码解析博客都值。这个过程不光是敲几个命令的问题你会被迫去了解Gradle多模块构建、spring-core对其他模块的基础支撑作用、AspectJ和Spring AOP的底层关系、JDK和构建工具的版本兼容规则。这些知识看起来和Spring源码本身没直接关系但实际上都是在补底层功底。第一次编译不建议追求全量一次过先跑通spring-core、spring-beans、spring-context这“三件套”就够日常阅读和调试了。等环境完全跑顺以后再逐步扩展到spring-aop、spring-web、spring-tx这些模块。说实话把这三个核心模块打通你理解Spring IoC容器和Bean生命周期就已经超过大多数背面试题的人了。