刚拿到Spring源码准备自己编译一份的时候我其实是有点膨胀的。觉得无非就是clone代码、装个JDK、跑个Gradle能有多难结果一整天搭进去光是在“编译不通过”和“下载依赖失败”之间来回折腾就花了大半时间。后来冷静下来把整个流程重新捋了一遍才发现大部分问题其实都出在一些非常基础的环节上JDK版本不匹配、Gradle版本不对、网络环境没有提前处理、源码分支没选对。这篇文章我把整个编译过程以及我踩过的坑完整记录下来希望能帮你绕开这些弯路。先说结论Spring源码编译本身不复杂真正复杂的是“你本机环境与Spring构建体系之间的隐性要求”。这些东西官方文档不会主动告诉你只有亲自动手编译一遍才会明白为什么Spring官方推荐直接用Gradle Wrapper而不是本机全局Gradle为什么源码分支要和JDK版本严格对应为什么明明照着文档操作还是会在依赖下载阶段卡死。这篇文章适合这些读者想深入阅读Spring源码的开发者、准备调试Spring框架本身的人、想给Spring提交PR的贡献者以及单纯想搞懂Gradle构建原理的Java后端工程师。如果你只是想在项目里引入Spring依赖那完全没必要自己编译直接用Maven或Gradle拉取官方发布版本就行。1. 编译前的准备工作环境、版本与源码分支的选择1.1 JDK版本与Spring版本之间的硬性对应关系这是最容易踩的第一个坑也是大多数人编译失败的根本原因。Spring Framework 6.x系列要求JDK 17以上Spring Framework 5.3.x还停留在JDK 8到JDK 17区间而Spring Framework 7.0直接要求JDK 17甚至JDK 21。如果你拿一个JDK 8去编译Spring 6.x连Gradle都会在初始化阶段就报错退出错误信息还不明不白甚至一度让我怀疑是Gradle本身坏了。我建议直接按下面这个对应关系去选Spring源码版本分支最低JDK推荐的Gradle JDK备注5.3.xJDK 8JDK 8/11老项目兼容性最好6.0.xJDK 17JDK 17当前主流版本6.1.xJDK 17JDK 17推荐学习使用7.0.xJDK 17JDK 21较新需注意Gradle版本注意这里说的JDK版本不是“能跑起来就行”而是“Gradle和Spring源码都要求的最低版本”。我自己就在用JDK 17编译Spring 6.1.x的时候因为Gradle Wrapper指定的版本对JDK 17的支持有细微差异导致编译时出现奇怪的字节码错误。后来统一调整到Gradle官方推荐的组合才恢复正常。1.2 源码分支选择不要一上来就mainSpring源码在GitHub上的默认分支是main但main分支往往处于持续开发状态经常会出现今天能编译、明天就因为某个新提交导致构建失败的情况。对于首次编译的开发者我不建议直接clone main分支而是选择一个发布过的稳定tag。具体操作可以先列出所有tag找一个最新的release版本git clone https://github.com/spring-projects/spring-framework.git cd spring-framework git tag | sort -V | tail -20比如你打算学习Spring 6.1.x那就直接切到对应的tag或者release分支git checkout v6.1.14踩坑提示Spring的tag命名是v6.1.14这种格式但有些历史版本的tag格式不一样可能没有前面的v。如果你git tag搜不到想要的版本试着不带v再搜一次。1.3 Gradle JDK的选择用JAVA_HOME说话Spring源码仓库里带了gradlew和gradle-wrapper.properties理论上你不需要自己安装Gradle直接用Wrapper脚本就行。但这里有个很微妙的点Gradle Wrapper会下载一个指定版本的Gradle而这个Gradle是用某个JDK版本去运行的。Gradle运行时的JDK版本必须满足Spring源码构建的最低JDK要求。实际操作时我习惯先把JAVA_HOME环境变量指到目标JDK再执行export JAVA_HOME/path/to/jdk-17 export PATH$JAVA_HOME/bin:$PATH java -version ./gradlew --version这样做的好处是如果gradlew报错至少能确认Gradle本身启动成功问题排查的范围就缩小到了构建脚本和依赖层面。2. 网络问题与依赖下载大部分人卡住的第一道坎2.1 依赖仓库配置先改镜像再编译Spring源码依赖非常多包括Spring自身模块之间的依赖、第三方库如Jackson、JUnit、ASM等这些依赖默认从repo.maven.apache.org和repo.spring.io拉取。如果你的网络环境访问这些仓库不稳定编译过程会反复出现依赖下载超时或校验失败。我踩了两次坑之后总结出一个相对稳妥的方案在项目根目录的settings.gradle中显式配置阿里云镜像仓库或者其他你实际测试可用的国内Maven镜像。具体配置方式是在settings.gradle文件的pluginManagement和dependencyResolutionManagement部分加入镜像仓库地址dependencyResolutionManagement { repositories { maven { url https://maven.aliyun.com/repository/public } maven { url https://maven.aliyun.com/repository/spring } maven { url https://repo.spring.io/release } mavenCentral() } }注意Spring框架的构建脚本里对仓库顺序是敏感的。如果你把mavenCentral()放在镜像之前Gradle会先去Maven Central拉取速度依然慢。所以我建议把镜像仓库放在最前面让Gradle优先从镜像拉依赖只有镜像缺失时才回源到中央仓库。2.2 断点续传与并发下载Gradle参数优化即使配了镜像Spring源码的依赖数量巨大首次编译时依然可能要下载几百MB甚至上GB的依赖文件。Gradle默认的下载并发数比较保守而一旦网络抖动单个依赖下载失败就会导致整个构建失败。我后来在~/.gradle/gradle.properties里面加上了这些配置org.gradle.jvmargs-Xmx4g -XX:MaxMetaspaceSize1024m -Dfile.encodingUTF-8 org.gradle.paralleltrue org.gradle.daemontrue org.gradle.cachingtrue org.gradle.workers.max8其中org.gradle.paralleltrue能显著提升多模块项目的编译速度。Spring源码本身就是超多模块的项目串行编译和并行编译差距很大并行能让CPU多核充分利用起来。另外有一个隐藏参数容易被忽略就是Gradle的网络超时时间。默认情况下Gradle下载依赖的超时时间比较短依赖一多、网络稍微波动就容易失败。可以在gradle.properties里增加systemProp.org.gradle.internal.http.socketTimeout180000 systemProp.org.gradle.internal.http.connectionTimeout180000这两个参数我没有放在官方文档里见过很醒目的说明但实测下来对网络不稳定的环境非常有帮助能减少很多“莫名其妙下载失败”的情况。2.3 不要反复删~/.gradle/caches目录刚开始编译失败的时候我一度怀疑是本地Gradle缓存损坏于是反复删除~/.gradle/caches目录重新下载。这其实是个非常糟糕的做法。Gradle的缓存是分模块、分版本的除非你确认某个依赖确实损坏否则完全不需要清空整个缓存。正确的排查思路应该是这样先看构建日志里具体是哪个依赖下载失败再去本地缓存目录里查看对应文件是否存在如果存在但校验不过删除单个依赖的缓存目录即可。Gradle远程缓存的目录结构大致是~/.gradle/caches/modules-2/files-2.1/{group}/{artifact}/{version}/{hash}你可以根据日志中的坐标信息快速定位到具体依赖。3. 正式编译从Gradle Wrapper到spring-core模块3.1 先编译spring-core还是全量编译Spring源码仓库包含几十个模块比如spring-core、spring-beans、spring-context、spring-web、spring-boot注意spring-boot是一个独立仓库不在spring-framework中、spring-aop等。全量编译全部模块耗时很长而且有些模块之间还有复杂的依赖关系如果你只是想读源码完全没必要一上来就跑全量。我第一次操作时直接执行了./gradlew build结果编译到一半就因为某个文档测试或者代码检查失败而中断。后来我改成先编译最核心的spring-core模块把构建时间缩短了一大半也方便逐步熟悉整个构建体系。具体命令是这样的./gradlew :spring-core:compileJava这条命令只编译spring-core模块的Java代码不跑测试、不生成文档、不做代码风格检查是最快速的验证环境是否正常的方式。踩坑提示Spring的Gradle构建脚本里定义了大量的自定义任务比如检查license头、检查代码风格、生成Ant依赖文件等。如果直接跑./gradlew build这些任务都会执行。首次编译时如果有代码格式或license问题比如你的JDK生成的文件编码不对整个构建会中断。所以先用compileJava验证环境是最稳妥的。3.2 全量编译的正确姿势当你确认spring-core能编译通过后就可以尝试全量编译了。但建议不要直接跑build而是跑./gradlew clean test -x test等一下这个命令会剔除测试任务因为Spring的测试套件非常庞大跑全量测试需要几十分钟对机器性能要求也高。如果是编译后拿来读源码完全不需要跑测试。我实际采用过的最优方案是两步走第一步全量编译Java源码./gradlew clean compileJava第二步编译并生成可读的class文件用于IDE导入./gradlew clean idea 或者直接用IDEA打开后让IDEA自动导入我个人的最终选择是用IntelliJ IDEA直接打开Spring源码目录IDEA会识别Gradle项目并自动执行导入。这个过程中IDEA会调用Gradle Wrapper去下载依赖并编译。只要前面网络配置和JDK版本没问题IDEA导入的成功率还是很高的。3.3 编译时常见的内存不足问题Spring源码编译过程中spring-core、spring-web这些大模块会产生大量中间类Gradle编译worker的内存如果不够就会抛出各种OutOfMemoryError。我最初没有设置任何内存参数在编译spring-web时遇到了java.lang.OutOfMemoryError: Java heap space。后来在gradle.properties中增加org.gradle.jvmargs-Xmx4g -XX:MaxMetaspaceSize1024m同时把Gradle worker的最大堆内存也做了调整org.gradle.workers.max84G堆是目前编译Spring全量模块比较稳的配置。如果你的机器内存只有8G建议把-Xmx设置成3g同时关闭浏览器等大内存应用避免系统内存不足导致编译被操作系统杀掉。4. 我踩过的具体坑与排查过程4.1 坑一Gradle Wrapper卡在下载Gradle发行版打开Spring源码目录后第一次运行./gradlew --version时Gradle Wrapper会先下载指定版本的Gradle发行包。这个发行包大小在100MB以上下载源是services.gradle.org。在网络环境不理想的情况下这个下载过程会反复失败甚至失败后没有任何明确的错误提示只是进程一直卡住。后来我找到了一个比较高效的解决办法手动从其他渠道下载对应版本的Gradle二进制包zip文件放到~/.gradle/wrapper/dists/gradle-X-bin/{hash}/目录下然后解压。这样Gradle Wrapper检测到本地已经存在对应版本时就会跳过网络下载。具体操作步骤大致是查看gradle/wrapper/gradle-wrapper.properties文件中的distributionUrl确认Gradle版本号。从可访问的Gradle发行版镜像或官方渠道下载对应的gradle-X-bin.zip。将zip文件放到~/.gradle/wrapper/dists/gradle-X-bin/下的某个子目录中Gradle Wrapper的目录结构是{版本号}/{hash}。重新运行./gradlew --versionGradle会自动检测并解压。这个做法本质上是绕过下载源问题把你已经下载好的发行包“投喂”给Gradle Wrapper。4.2 坑二Task :spring-core:compileJava FAILED且提示Unsupported class file major version看到这个提示时大多数人第一反应是源码有问题实际上是对应的class文件版本和当前JDK版本不一致。比如Spring 6.1.x源码编译时需要JDK 17而你用了JDK 11Gradle启动正常但编译时javac收到源码里的某些语法特性后直接报invalid target release或者Unsupported class file major version。这种问题没有捷径只能是检查JAVA_HOMEecho $JAVA_HOME java -version javac -version确保三个输出都指向同一个JDK并且版本满足Spring源码版本的要求。还有一点容易忽略如果你用IDEA打开源码IDEA会为项目配置一个SDK这个SDK可能与你命令行里JAVA_HOME指向的JDK不一致。在这种情况下IDEA里显示编译通过但命令行构建却失败或者反过来。所以不要只看一个地方的JDK版本IDEA的Project Structure里的SDK也要检查一遍。4.3 坑三Cannot find symbol类错误尤其与NullAway或Checkstyle相关Spring源码里集成了静态代码检查工具如Checkstyle、NullAway这些检查器在编译时会对代码进行额外的类型标注和风格检查。有时候明明代码没有语法错误却因为某个检查规则不通过导致编译失败。我第一次编译时遇到了一个非常隐蔽的错误某个Spring类中有一个方法参数标注了Nullable但编译报告说这个标注在实际使用时没有被正确传播。排查了很久最后发现是Gradle的注解处理器没有正常启用。解决方案是强制清理Gradle缓存并重新编译./gradlew clean如果还是不行检查一下ide.gradle中关于NullAway的配置确认你用的JDK版本在NullAway支持的范围内。这个坑在JDK 17下不常见在JDK 21下概率会高一些。4.4 坑四Could not resolve org.springframework:spring-core:6.x.x-SNAPSHOT这个错误发生在你尝试从源码仓库构建一个项目而这个项目依赖的是Spring的SNAPSHOT版本时。明明你在本机已经编译了Spring源码但本地项目就是解析不到这个SNAPSHOT依赖。原因在于编译Spring源码时Gradle会在本地Maven仓库生成对应的POM和JAR文件但这个生成动作是由publishToMavenLocal任务控制的。如果只跑了build或compileJava并不会自动把构建产物发布到本地Maven仓库。解决办法是执行./gradlew publishToMavenLocal这个任务会把所有模块的构建产物发布到~/.m2/repository中之后你的本地项目就能通过Maven坐标引用到源码编译出的Spring版本了。实战经验我编译Spring源码的目的就是为了在本地项目中调试框架内部逻辑。这种情况下用publishToMavenLocal配合Maven或Gradle依赖坐标如org.springframework:spring-core:6.1.14-SNAPSHOT来引用比直接把源码导入IDEA后依赖module更方便调试因为运行项目时能精确走你编译出的class文件。5. 常见问题与排查速查表问题现象主要原因排查顺序解决方案Gradle Wrapper下载慢或卡死网络访问services.gradle.org不稳定查看distributionUrl、检查本地wrapper cache手动下载发行包放入本地cache配置镜像Unsupported class file major versionJDK版本与Spring/Gradle不匹配检查JAVA_HOME、IDEA SDK更换JDK版本依赖下载失败或校验错误仓库访问不稳定查看具体依赖坐标、检查镜像配置配置阿里云镜像、调整socket超时参数Cannot find symbol类错误注解处理器未正确启用清理缓存、检查JDK版本兼容性./gradlew clean后重试本地项目无法引用编译产物未发布到本地Maven仓库检查本地.m2目录执行./gradlew publishToMavenLocal编译过程中OOMGradle worker内存不足查看内存占用修改gradle.properties堆内存参数全量build时被测试任务中断测试任务耗时且环境不满足查看具体失败测试改用compileJava跳过测试6. 编译完成之后的验证与IDE导入6.1 验证编译产物编译完成之后不要急着关终端先做几个快速验证。第一看构建日志末尾是否输出BUILD SUCCESSFUL。第二检查关键模块的class文件是否正常生成比如spring-core/build/classes/java/main目录下应该有大量class文件。第三执行以下命令确认产物可被正常解析./gradlew :spring-core:test --tests org.springframework.core.env.StandardEnvironmentTests如果你不想跑测试也可以直接进入spring-core/build/libs目录查看生成的JAR包是否存在。6.2 IDEA导入源码时的Gradle设置在IDEA中导入Spring源码注意以下几点设置Gradle JVM为项目对应的JDK版本具体路径在Settings - Build Tools - Gradle - Gradle JVM。在Settings - Build Tools - Gradle - Runner中勾选Delegate IDE build/run actions to Gradle这样IDEA在运行时也会统一走Gradle构建逻辑。导入之后首次同步会比较慢因为Gradle会重新解析所有模块依赖。此时耐心等待即可不要频繁点击刷新按钮。6.3 修改源码并验证热生效如果你是想调试Spring内部逻辑有一个很实用的小技巧修改Spring源码中的某个类后只需要重新编译对应的模块然后运行你的测试项目即可。由于本地Maven坐标指向的是你编译出的版本你的测试项目会自动加载最新的class文件。我自己在调试Spring事件机制时就在spring-context模块的SimpleApplicationEventMulticaster里加了几行日志然后执行./gradlew :spring-context:compileJava :spring-context:publishToMavenLocal再回到自己的测试项目中运行日志立刻生效。这种调试方式比在IDE中断点观察更加直观也不用重新打包整个项目。7. 写在最后的个人经验Spring源码编译这件事难度不在“编译”本身而在于对整个构建体系的理解和环境的精确控制。我身边有不少同事第一次编译都能跑通靠的不是运气而是把版本对应关系、网络环境、Gradle参数这三件事提前处理好了。就我个人的实操体会来说最值得投入时间的环节是“第一次完整地把各个版本的对应关系搞清楚”。JDK版本、Gradle发行版版本、Spring源码版本这三者环环相扣任何一环不匹配都会给你制造各种奇怪的问题。建议你在编译之前先列出一个清晰的版本矩阵再去动代码。另外一个比较实用的心得是不要执着于一条命令build全量跑完。先把小的核心模块跑通再逐步扩大范围。这能让你在遇到问题时把问题快速限定在一个小范围内而不是面对几十个模块同时报错毫无头绪。如果只是为了阅读源码编译到spring-core和spring-context基本就够了。如果你要参与到Spring的源码贡献中那么再考虑全量编译和全量测试。最后编译过程中如果遇到了文档里没写过的奇怪错误不要上来就删缓存、重装JDK。先看错误日志定位到具体任务和依赖再做针对性处理。Spring的Gradle构建体系很成熟绝大多数问题都是环境问题不是代码问题。把环境这关过了后面就会非常顺畅。希望这篇编译踩坑记录能帮你在Spring源码编译这条路上少走一些弯路。如果你在实际操作中遇到了其他问题欢迎在评论区一起交流。