前言
如果想要探究 Spring 源码,那么在本地编译一份 Spring 的源码是必不可少的,这利于我们深入理解框架、方便调试和定位问题,也给予我们定制和扩展的能力。但是编译 Spring 源码并非像编译我们自己的项目那么简单,其中可能会遇到不少的问题,笔者如是,于是写下这一篇教程记录下编译安装的过程和问题。
环境准备
JDK:9 以上版本(本文示例版本为 JDK11)
网络:能正常访问 Github
安装 JDK11
因为 Spring 5.3.10 使用到了 jdk9 以后才有的 jdk.jfr 模块,所以我们需要在电脑上安装 9 以上版本的 jdk,这里以 JDK11 为例。
JDK11 下载地址:Java Archive Downloads - Java SE 11
可以下载压缩包的版本:
解压后,在系统环境变量配置下 JAVA_HOME、CLASSPATH 和 PATH,这些大家应该都比较熟悉,就不在这里赘述。
下载源码
官方 github 仓库:https://github.com/spring-projects/spring-framework
git 地址:https://github.com/spring-projects/spring-framework.git
# 克隆官方仓库
git clone https://github.com/spring-projects/spring-framework.git
# 切换目录
cd spring-framework
# 切换版本对应的 tag
git checkout v5.3.10
下载后会得到以下的目录,此时先不要急着导入到 IDEA,需要先修改一些配置和预编译两个模块,以确保能够正常完成编译。
源码文件预处理
下载 Gradle
用文本编辑器打开源码根目录下的 /gradle/wrapper/gradle-wrapper.properties
文件,可以看到如下内容:
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
其中 distributionUrl
是 gradle 的路径,这里默认配置的是一个下载链接,默认会从这个地址下载,我们可以提前下载好,然后修改为自己本地的文件路径,例如:
其中
D:/develop/gradle-7.2/gradle-7.2-bin.zip
就是我本地 gradle 压缩包的路径
distributionUrl=file\:///D:/develop/gradle-7.2/gradle-7.2-bin.zip
更换 Gradle 镜像源
与 maven 类似,gradle 也需要我们配置下国内镜像源,否则下载起来会很慢。
打开根目录下的 build.gradle
,搜索 url
关键词,在如图所示位置添加几行镜像配置:
maven { url "https://maven.aliyun.com/repository/public" }
maven { url "https://maven.aliyun.com/repository/central" }
同理配置下根目录下的 settings.gradle
:
maven { url "https://maven.aliyun.com/repository/gradle-plugin" }
maven { url "https://maven.aliyun.com/repository/public" }
maven { url "https://maven.aliyun.com/repository/central" }
预编译处理
源码根目录下有一份 import-into-idea.md
文件,告诉了我们导入 idea 之前需要先预编译 spring-oxm
这个模块:
那么我们在根目录下打开命令行窗口,输入以下命令执行预编译:
-
Mac / Linux 执行以下命令:
./gradlew :spring-oxm:compileTestJava
-
Windows 用户执行以下命令:
gradlew.bat :spring-oxm:compileTestJava
等待一段时间,当出现 SUCCESSFUL 字样的时候,表示预编译成功。
接着,我们最好也预编译下 spring-core
这个模块,虽然官方没有提及,但这个模块是核心模块,所以最好也提前预编译下:
-
Mac / Linux:
./gradlew :spring-core:compileTestJava
-
Windows:
gradlew.bat :spring-core:compileTestJava
导入 IDEA 进行编译
IDEA 打开 build.gradle 文件
点击 File - New - Project from Existing Sources...
选择源码根目录下的 build.gradle
文件
接着就进行漫长的下载依赖的过程
创建一个测试项目
等依赖下载完毕且模块正常加载后,我们的目录应该会有很多小蓝点,表示 idea 已将其加载为模块:
我们可以创建一个自己的模块,用来测试运行项目,在这之前,我们需要先修改一下 idea 的编译方式:
打开 Settings - Build, Execution, Deployment - Build Tools - Gradle
,将编译方式都改为 IntelliJ IDEA
:
此时我们就可以开始创建自己的模块了,在根目录下右键 New - Module
:
接着填写项目的名称等信息,然后点击 Create
:
创建完毕后,会得到以下目录,其中 build.gradle
就类似于 maven 中的 pom.xml
文件,用来管理我们的依赖。
打开 build.gradle
,添加一些依赖,然后 reload 一下:
implementation(project(":spring-context"))
implementation(project(":spring-instrument"))
implementation("javax.annotation:javax.annotation-api:1.3.2")
然后我们就可以开始愉快地编写代码了,比方说可以写几行简单的代码来测试 Spring 容器:
运行 Main 方法,输出以下信息,表示 Spring 容器正常加载,测试成功~
常见问题
java: 程序包 jdk.jfr 不存在
解决办法:检查 IDEA 配置的 SDK 版本 和 编译版本 是否正确
① 检查 Project Structure
② 检查编译字节码版本
java: 找不到类 InstrumentationSavingAgent
添加 spring-instrument
依赖即可:
implementation(project(":spring-instrument"))
IDEA: 每次编译项目总会自动重置 JDK 版本为 1.8
这个问题是由于 gradle 目录下有两个文件配置了默认的 JDK 版本为 1.8,我们需要将这两个配置文件中涉及到 1.8 的内容修改为 11。
① gradle/ide.gradle
只需要修改这个地方,将 1.8 或 8 改为 11 或自己的 JDK 版本即可
eclipse.jdt {
sourceCompatibility = 11
targetCompatibility = 11
}
② gradle/toolchains.gradle
因为这个文件涉及到要修改的地方较多,所以这里我直接贴了完整内容,如果有需要直接复制全部内容然后替换即可,如果你是其他的 JDK 版本,可以搜索并替换文件中的 11 为自己的版本
/**
* Apply the JVM Toolchain conventions
* See https://docs.gradle.org/current/userguide/toolchains.html
*
* One can choose the toolchain to use for compiling the MAIN sources and/or compiling
* and running the TEST sources. These options apply to Java, Kotlin and Groovy sources
* when available.
* {@code "./gradlew check -PmainToolchain=8 -PtestToolchain=11"} will use:
* <ul>
* <li>a JDK8 toolchain for compiling the main SourceSet
* <li>a JDK11 toolchain for compiling and running the test SourceSet
* </ul>
*
* By default, the build will fall back to using the current JDK and 11 language level for all sourceSets.
*
* Gradle will automatically detect JDK distributions in well-known locations.
* The following command will list the detected JDKs on the host.
* {@code
* $ ./gradlew -q javaToolchains
* }
*
* We can also configure ENV variables and let Gradle know about them:
* {@code
* $ echo JDK11
* /opt/openjdk/java11
* $ echo JDK15
* /opt/openjdk/java15
* $ ./gradlew -Porg.gradle.java.installations.fromEnv=JDK11,JDK15 check
* }
*
* @author Brian Clozel
* @author Sam Brannen
*/
def mainToolchainConfigured() {
return project.hasProperty('mainToolchain') && project.mainToolchain
}
def testToolchainConfigured() {
return project.hasProperty('testToolchain') && project.testToolchain
}
def mainToolchainLanguageVersion() {
if (mainToolchainConfigured()) {
return JavaLanguageVersion.of(project.mainToolchain.toString())
}
return JavaLanguageVersion.of(11)
}
def testToolchainLanguageVersion() {
if (testToolchainConfigured()) {
return JavaLanguageVersion.of(project.testToolchain.toString())
}
return mainToolchainLanguageVersion()
}
plugins.withType(JavaPlugin) {
// Configure the Java Toolchain if the 'mainToolchain' is configured
if (mainToolchainConfigured()) {
java {
toolchain {
languageVersion = mainToolchainLanguageVersion()
}
}
}
else {
// Fallback to JDK11
java {
sourceCompatibility = JavaVersion.VERSION_11
}
}
// Configure a specific Java Toolchain for compiling and running tests if the 'testToolchain' property is defined
if (testToolchainConfigured()) {
def testLanguageVersion = testToolchainLanguageVersion()
tasks.withType(JavaCompile).matching { it.name.contains("Test") }.configureEach {
javaCompiler = javaToolchains.compilerFor {
languageVersion = testLanguageVersion
}
}
tasks.withType(Test).configureEach{
javaLauncher = javaToolchains.launcherFor {
languageVersion = testLanguageVersion
}
}
}
}
plugins.withType(GroovyPlugin) {
// Fallback to JDK11
if (!mainToolchainConfigured()) {
compileGroovy {
sourceCompatibility = JavaVersion.VERSION_11
}
}
}
pluginManager.withPlugin("kotlin") {
// Configure the Kotlin compiler if the 'mainToolchain' property is defined
if (mainToolchainConfigured()) {
def mainLanguageVersion = mainToolchainLanguageVersion()
def compiler = javaToolchains.compilerFor {
languageVersion = mainLanguageVersion
}
// See https://kotlinlang.org/docs/gradle.html#attributes-specific-for-jvm
def javaVersion = mainLanguageVersion.toString() == '11' ? '11' : mainLanguageVersion.toString()
compileKotlin {
kotlinOptions {
jvmTarget = javaVersion
jdkHome = compiler.get().metadata.installationPath.asFile.absolutePath
}
}
// Compile the test classes with the same version, 'testToolchain' will override if defined
compileTestKotlin {
kotlinOptions {
jvmTarget = javaVersion
jdkHome = compiler.get().metadata.installationPath.asFile.absolutePath
}
}
}
else {
// Fallback to JDK11
compileKotlin {
kotlinOptions {
jvmTarget = '11'
}
}
compileTestKotlin {
kotlinOptions {
jvmTarget = '11'
}
}
}
if (testToolchainConfigured()) {
def testLanguageVersion = testToolchainLanguageVersion()
def compiler = javaToolchains.compilerFor {
languageVersion = testLanguageVersion
}
// See https://kotlinlang.org/docs/gradle.html#attributes-specific-for-jvm
def javaVersion = testLanguageVersion.toString() == '11' ? '11' : testLanguageVersion.toString()
compileTestKotlin {
kotlinOptions {
jvmTarget = javaVersion
jdkHome = compiler.get().metadata.installationPath.asFile.absolutePath
}
}
}
}
// Configure the JMH plugin to use the toolchain for generating and running JMH bytecode
pluginManager.withPlugin("me.champeau.jmh") {
if (mainToolchainConfigured() || testToolchainConfigured()) {
tasks.matching { it.name.contains('jmh') && it.hasProperty('javaLauncher') }.configureEach {
javaLauncher.set(javaToolchains.launcherFor {
languageVersion.set(testToolchainLanguageVersion())
})
}
tasks.withType(JavaCompile).matching { it.name.contains("Jmh") }.configureEach {
javaCompiler = javaToolchains.compilerFor {
languageVersion = testToolchainLanguageVersion()
}
}
}
}
// Store resolved Toolchain JVM information as custom values in the build scan.
rootProject.ext {
resolvedMainToolchain = false
resolvedTestToolchain = false
}
gradle.taskGraph.afterTask { Task task, TaskState state ->
if (mainToolchainConfigured() && !resolvedMainToolchain && task instanceof JavaCompile && task.javaCompiler.isPresent()) {
def metadata = task.javaCompiler.get().metadata
task.project.buildScan.value('Main toolchain', "$metadata.vendor $metadata.languageVersion ($metadata.installationPath)")
resolvedMainToolchain = true
}
if (testToolchainConfigured() && !resolvedTestToolchain && task instanceof Test && task.javaLauncher.isPresent()) {
def metadata = task.javaLauncher.get().metadata
task.project.buildScan.value('Test toolchain', "$metadata.vendor $metadata.languageVersion ($metadata.installationPath)")
resolvedTestToolchain = true
}
}