如何阅读 Spring 源码?
昨晚原计划给几个朋友简单介绍一下阅读 Spring 源码的方法。结果,大家因为各种原因没能及时参加。后来,就取消分享了。干脆写一篇文章出来,感兴趣欢迎自取。
代码准备
Spring Framework 是开源的,代码托管在 GitHub 上: Spring Framework。任何人都可以方便地获得它的源代码。所以,如果想阅读 Spring 的源代码,当然是直接把代码克隆到本地,然后直接在 IDE(推荐 IDEA)中进行调试了。另外,还需要存放自己写一些测试和文档。所以,最好把代码 fork 到自己的账户下,从 master
上切出一个新分支并 push 到自己的 Repo 中,这样自己就可以随意更新了。具体步骤如下:
克隆代码
# 直接克隆原始仓库为 origin git clone git@github.com:spring-projects/spring-framework.git
fork 代码,D瓜哥直接 fork 到自己账户下了: diguage/spring-framework。
添加原创仓库地址:
# 添加自己仓库为 diguage # 这样就能在所有项目中保持命名的一致性,方便标识 git remote add diguage git@github.com:diguage/spring-framework.git
创建新分支
# 创建新分支 git switch -c analysis # 将新分支 push 到自己的 Repo 中 git push diguage analysis
这样,在这个新分支上,就可以随意折腾了。
下载依赖
# Mac or Linux ./gradlew clean && ./gradlew :spring-oxm:compileTestJava && ./gradlew test # Windows gradlew.bat clean && gradlew.bat :spring-oxm:compileTestJava && gradlew.bat test
上述操作会很慢,如果想加快速度,可以给 Gradle 配置一下阿里云的 Maven 镜像。
// 在用户根目录下,创建 .gradle 目录,然后在其中创建 init.gradle 文件, // 目录如:~/.gradle/init.gradle。最后,将下面内容加入到文件中: buildscript { repositories { maven { url 'https://maven.aliyun.com/repository/public'} maven { url 'https://maven.aliyun.com/repositories/jcenter' } maven { url 'https://maven.aliyun.com/repositories/google' } maven { url 'https://maven.aliyun.com/repository/central' } maven { url 'https://maven.aliyun.com/repository/spring/' } maven { url 'https://maven.aliyun.com/repository/gradle-plugin' } mavenLocal() mavenCentral() } } allprojects { repositories { maven { url 'https://maven.aliyun.com/repository/public'} maven { url 'https://maven.aliyun.com/repositories/jcenter' } maven { url 'https://maven.aliyun.com/repositories/google' } maven { url 'https://maven.aliyun.com/repository/central' } maven { url 'https://maven.aliyun.com/repository/spring/' } maven { url 'https://maven.aliyun.com/repository/gradle-plugin' } mavenLocal() mavenCentral() } }
将代码导入到 IDE 中,为了方便添加自己的测试代码,可以新建一个 Gradle 模块。例如 diguage/spring-framework/truman at analysis:
tree spring-framework ├── # 省略 N 行 ├── spring-aop ├── spring-beans ├── spring-context ├── spring-core ├── src └── truman # 个人新建模块 ├── build.gradle ├── docs # 存放文档,可以把阅读代码的笔记放在这里 │ ├── AnnotationAwareAspectJAutoProxyCreator.puml │ ├── BeanDefinition.puml │ ├── BeanFactory.puml │ ├── ConfigurationClassPostProcessor.puml │ └── notes.adoc └── src # 存放自己的调试代码 ├── main │ ├── java │ │ └── com │ │ └── diguage │ │ └── truman │ │ ├── aop │ │ │ ├── AopTest.java │ │ │ ├── DeclareParentsAopTest.java │ │ │ ├── FinalTest.java │ │ │ ├── MoreAopTest.java │ │ │ └── TargetSourceTest.java │ │ ├── context │ │ │ ├── ApplicationContextAwareTest.java │ │ │ ├── ApplicationListenerTest.java │ │ │ ├── BeanDefinitionRegistryPostProcessorTest.java │ │ │ ├── BeanFactoryPostProcessorAutowireTest.java │ │ │ ├── BeanFactoryPostProcessorTest.java │ │ │ ├── BeanPostProcessorAnnoBeanTest.java │ │ │ ├── BeanPostProcessorAutowireTest.java │ │ │ ├── BeanPostProcessorTest.java │ │ │ ├── CircularDependenceConstructorTest.java │ │ │ ├── CircularDependencePrototypeTest.java │ │ │ ├── CircularDependenceSingletonTest.java │ │ │ ├── FactoryBeanTest.java │ │ │ ├── InitializingBeanTest.java │ │ │ ├── InstantiationAwareBeanPostProcessorTest.java │ │ │ ├── LifecycleTest.java │ │ │ ├── ObjectFactoryTest.java │ │ │ ├── PropertyValuesTest.java │ │ │ └── mybatis │ │ ├── ext │ │ │ ├── DggNamespaceHandler.java │ │ │ ├── ExtensionTest.java │ │ │ ├── User.java │ │ │ └── UserBeanDefinitionParser.java │ │ ├── jdbc │ │ │ └── JdbcTest.java │ │ └── mybatis │ │ ├── Employees.java │ │ ├── EmployeesMapper.java │ │ └── MybatisTest.java │ └── resources │ ├── META-INF │ │ ├── dgg.xsd │ │ ├── spring.handlers │ │ └── spring.schemas │ ├── com │ │ └── diguage │ │ └── truman │ │ └── ext │ │ └── dgg.xml │ └── log4j2.xml ├── test │ ├── java │ └── resources └── testFixtures ├── java └── resources
更新代码和提交修改
# 在 master 分支上更新代码 git pull # 然后切换到 analysis 分支,同步更新 git rebase master
示例代码
原来使用 Spring,需要 XML 文件。甚至,现在的文档中也有大量的 XML 配置。为了方便起见,D瓜哥介绍一个不需要使用 XML 配置文件可以跑起来的写法:
package com.diguage.truman.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.junit.jupiter.api.Test;
import org.springframework.aop.framework.AopContext;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;
import javax.annotation.Resource;
/**
* @author D瓜哥, https://www.diguage.com/
* @since 2020-06-02 11:12
*/
public class AopTest {
@Test
public void test() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(Config.class);
context.refresh();
UserService bean = context.getBean(UserService.class);
bean.test();
bean.getDesc();
bean.setDesc("This is a test.");
String user = bean.getById(119);
System.out.println(user);
BeanDefinition definition = context.getBeanDefinition(UserService.class.getName());
System.out.println(definition);
}
@Configuration
@Import(AopImportSelector.class)
@EnableAspectJAutoProxy(exposeProxy = true)
public static class Config {
}
// 使用 @Import 和 ImportSelector 搭配,就可以省去 XML 配置
public static class AopImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[]{
UserDao.class.getName(),
UserService.class.getName(),
TestAspect.class.getName()
};
}
}
@Aspect
public static class TestAspect {
@Pointcut("execution(* com.diguage.truman.aop.AopTest$UserService.test(..))")
public void test() {
}
@Before("test()")
public void beforeTest() {
System.out.println("beforeTest");
}
@After("test()")
public void afterTest() {
System.out.println("afterTest");
}
@Around("test()")
public Object aroundTest(ProceedingJoinPoint pjp) {
System.out.println("aroundBefore1");
Object restul = null;
Signature signature = pjp.getSignature();
System.out.println(pjp.getKind());
Object target = pjp.getTarget();
System.out.println(target.getClass().getName() + "#" + signature.getName());
try {
restul = pjp.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
System.out.println("aroundAfter1");
return restul;
}
}
public static class UserDao {
public String getById(int id) {
return "diguage-" + id;
}
}
public static class UserService {
private String desc = "testBean";
@Resource
private UserDao userDao;
public String getDesc() {
System.out.println("getDesc");
this.test();
System.out.println("--this----------getDesc");
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
// 使用 @EnableAspectJAutoProxy(exposeProxy = true) 打开 exposeProxy = true
// 则必须这样写,才能获取到当前的代理对象,然后调用的方法才是被 AOP 处理后的方法。
// 使用 this.methodName() 调用,依然调用的是原始的、未经 AOP 处理的方法
((UserService) AopContext.currentProxy()).test();
System.out.println("--AopContext----setDesc");
}
public void test() {
System.out.println("----------------test");
}
public String getById(int id) {
return userDao.getById(id);
}
}
}
关键代码
Spring 代码庞大,除去测试代码,还有 22 多万行正式的 Java 代码。所以,如果不能抽丝剥茧,那么肯定会掉进坑里爬不出来。所以,要选择一些关键代码去重点阅读。
其实,在前面的文章中,几乎已经把关键代码都列出来了。大家可以重点关注这几篇文章:
学习 Spring 源码,一个关键点就是学习 Spring 支持的扩展点,一方面可以帮助理解 Spring 的设计;另外一方面也可以帮助我们在需要的时候,对 Spring 做一定的扩展,简化我们的代码。下面这几篇文章重点介绍了 Spring 支持的扩展点以及这些扩展点的应用示例:
除此之外,通过对 Spring 源码实现的了解,还要可以更快地定位问题原因,寻找出合适的解决方案:
add Jakarta Annotations API by diguage · Pull Request #367 · seata/seata-samples — 这个 PR 还要求对 Dubbo 的源码实现有一点的了解。
奇技淫巧
在调试代码时,D瓜哥也积累了一些小技巧,分享给大家:
直接在 Spring 源码上加注释,例如: diguage/spring-framework/ConfigurationClassPostProcessor.java at analysis。
有问题,随时记录在册,方便后续跟进和解决。例如: diguage/spring-framework/notes.adoc at analysis。
针对不同场景,写不同的测试代码来调试。例如: diguage/spring-framework/truman/src/main/java/com/diguage/truman at analysis。
充分利用栈帧信息,查看方法调用链。例如:
Figure 1. 方法调用栈