那些你应该掌握的 Spring 原理

参考文章

《Spring In Action, 2nd.Ed.》
Difference between beanfactory and-applicationcont
Spring Framework Annotations
The IoC container
BeanFactoryPostProcessor and BeanPostProcessor in lifecycle events

前言

spring 目前所代表的已经不是一个框架, 或者多个框架, 而是 java 企业级应用中一整套体系化的解决方案。
这一点从 spring 官方的宣传图上就可以看出来。
在这里插入图片描述
对于普通 java 开发者而言, 应用 spring 已经是再熟悉不过的事情了, 本文旨在梳理一些 spring 应用级别之外, 应该被进阶开发者所掌握的原理性知识。

由于 spring 的内容已经涵盖了方方面面, 本文仅会涉及 spring 中最核心, 最值得普通开发者掌握的一些原理性知识。

Spring 的基本思想:控制反转/依赖注入(IoC/DI)

首先明确一下, 控制反转(IoC, Inversion of Control) 和 依赖注入(DI, Dependency Injection) 是一个概念的两种称呼, 新手同学不必纠结, 后文可能会经常交叉使用这两个不同称呼。

在展开学习 SpringFramework 的实现原理之前,值得思考的是, 所谓的依赖注入(DI)到底有什么优点, 以致于被如此广泛的使用。

以下这段话引自 《Spring In Action, 2nd.Ed.》作者 Craig Walls , 他同样是 Pivotal 公司的开发人员( 不了解 Pivotal 公司的童鞋需要知道: Spring 以及衍生框架、Redis 存储、消息队列框架 RabbitMQ 等都出自 Pivotal 公司)

“Any nontrivial application is made up of two or more classes that collaborate with each other to perform some business logic. Traditionally, each object is responsible for obtaining its own references to the objects it collaborates with (its dependencies). When applying DI, the objects are given their dependencies at creation time by some external entity that coordinates each object in the system. In other words, dependencies are injected into objects.”

任何正式的应用都是由多个类相互协作, 各自实现不同的业务逻辑完成的。 传统上,每一个对象都是自行负责获取它所依赖对象引用的。 但是应用了 DI(Dependency Injection)后, 每个对象所依赖的对象不再由其自身负责获取, 而是统一由一个外部实体在创建这些需要相互协作的对象时提供。

上面这段话, 有些抽象, 通过代码和图片更加形象地表述一下

应用 控制翻转 / 依赖注入 (IoC/DI)之前

public class A {
   
    public void doSomethingByA(){
   
        // 实现一些 A 逻辑

        // 需要 B 类执行一些 B 逻辑
        // new B 也可以换做是 ActionFacotry.createB() 之类的工厂方法
        // 这里的关键是, 由 A 关心怎么获得 b 的引用
        B b = new B();

        // 执行 B 逻辑
        b.doSomethingB();
    }
}

由于在类 A 中直接编写了构造 B(或者是获取 B 的应用的代码), A 与 B 类变成了一种紧耦合的协作方式, 下图可以形象的表达这种耦合关系。
在这里插入图片描述
这种耦合意味着, 如果我们希望更换 A 类获取的类 B 引用, 不得不更改 A 类的代码 ( 图中可以形象理解为, 要把卡住红色部分的蓝色部分砸开 )。

有同学可能会在这里指出, 如果使用工厂模式 BFacotry 用于创建类型为 B 的实例, 那么不需要修改类 A 也可以更换类 B 的引用。

但实际上, 这并没有太大区别, 只是把 A与 B 的耦合替换成了 A 与 BFactory 的耦合而已, 会进一步引入 BFactory 的引用如何获取的问题

应用 控制翻转 / 依赖注入 (IoC/DI)之前, 代码是这项协作的 。

应用 控制翻转 / 依赖注入 (IoC/DI)之后

public class A {
   
	@Autowired
	private B b;
    public void doSomethingByA(){
   
        // 实现一些 A 逻辑
        
        // 执行 B 逻辑
        b.doSomethingB();
    }
}

由于 A 类的代码中没有编写任何获取 B 类引用的代码, 使得 A 与 B 的耦合关系变成了如下的松耦合状态
在这里插入图片描述

这样当我们想要替换 B 的引用时, 我们就完全无需改动 A 的代码。

松耦合的好处

这个地方可能还会有同学进一步产生疑问, 为什么总是要假设改变 B 的引用, 实际开发中,相互依赖的类在很多情况下, 并不需要动态修改啊。

这里的答案是 测试需求

想象一下, 你要编写一段针对某个类某个方法进行单元测试, 由于这个方法的正常执行会依赖很多其他的类, 于是在构造测试环境时, 需要 mock 很多用于测试的假的对象。

如果你编写的代码使用了 IoC 容器, 想要为待测试的组件提供一个虚假的 mock 引用就变成了很容易的事。 反之, 如果没有使用 IoC 容器, 那么在不改动代码的情况下, 替换待测试类的依赖组件就变成了一个很难实现的目标。

除去测试需求, 即便你没有灵活替换某个类引用的需求, 将对象之间的引用关系维护放在一个统一的组件中管理, 也可以使得系统组件中错综复杂的关系变得有迹可循

IoC 就没有副作用吗

有, 就像前面提到的, 必要的耦合关系是不能被消除的, 只是被从一种耦合替换为另外一种耦合。使用 IoC 容器后, 由于将系统中组件依赖关系都交给了容器去维护, 整个系统就与容器耦合了起来。
在这里插入图片描述
也就是说, 如果你的系统有一天想要替换框架, 这就变得很困难, 当然这种需求很少会有, 所以 Spring IoC 框架已经近乎成为了 java 世界中企业应用的标准框架。

Spring 的核心: Bean 容器

根据 spring-framework 官方文档给出的定义, 由 Spring IoC 容器实例化, 组装, 管理的对象被统称为 Bean。 所以后文我们都会用 Bean 来描述被 Spring 所创建以及管理的对象。
在这里插入图片描述
Spring IoC 实现的最基本的功能是对象的的创建与注入, 所以从最原始的抽象层面考虑, 至少存在如下 3 个步骤。

  1. 获取 Bean 的定义信息

  2. 根据 Bean 的定义信息创建 Bean

  3. 在需要时刻, 将创建好的 Bean_A 注入 Bean_B

所以, 如果要理解 Spring, 应该从源码角度其理解 Spring 是如何实现这三个基本步骤的。

获取 Bean 的描述信息:BeanDefinition

Xml 形式的 Bean 描述

Spring 框架刚创建时, 是通过 xml 文件描述一个 Bean 的。例如下图
在这里插入图片描述

即描述了一个 class 为 MyTestBean 的对象, 其 id 被定义为 myTestBean

public class MyTestBean {
   
	private String testStr = "testStr";
	
	public String getTestStr(){
   
			return testStr;
	}
	public void setTestStr(String testStr){
   
			this.testStr = testStr;
	}
	
}

然后可以通过声明 XmlBeanFacotry 的方式来解析 xml, 加载对应的 bean

public class TestXmlBeanFactory{
   
	@Test
	public void testLoadBeanFromXml(String testStr){
   
		BeanFactory beanFactory = new XmlBeanFactory(new ClassPathResource("beanFactoryTest.xml"))
		MyTestBean bean = (MyTestBean) bf.getBean("myTestBean");
		assertEquals("testStr",bean.getTestStr());
	}
	
}

Annotation 形式的 Bean 描述

自从 Java 5.0 开始提供 Annotation 的语法支持后, Spring Framework 从 Release 2.5 版本开始, 也支持了通过 Annotation 来定义 Bean。由于 Annotation 的便捷性, Xml 形式的 bean 配置文件越来越少, 以至当下最为流行的 Spring Boot 通常使用纯注解的方式完成 Bean 的描述

@Component
public class Customer {
   
    private Person person;
    @Autowired
    public Customer (Person person) {
             
      this.person=person;
    }
}

@Component 注释的类, 只要在 @ComponentScan 所配置的扫描路径下, 就会被 Spring 自动加入 bean 的管理范围中。 被 @Autowired 修饰的域或构造器, 可以自动根据需要构造器的参数类型, 找到合适的 bean 予以注入。

两类 IoC 容器: BeanFactory vs ApplicationContext

对于 Bean 的管理, Spring 事实上提供了两类容器接口:

  1. BeanFactory
  2. ApplicationContext

这里引用一个 StackOverFlow 上发现的清晰的图, 可以较为清晰的体现两类容器接口的差别

Feature BeanFactory ApplicationContext
Annotation support No Yes
BeanPostProcessor Registration Manual Automatic
implementation XmlBeanFactory(Deprecated Since 3.1)
–> XmlBeanDeifinitionReader + DefaultListBeanFactory
ClassPathXmlApplicationContext
FileSystemXmlApplicationContext
WebXmlApplicationContext
internationalization No Yes
Enterprise services No Yes
ApplicationEvent publication No Yes

从上面的对比表格中, 我们可以发现, ApplicationContext 的功能比 BeanFactory 更为全面。 从接口关系上来说, 也确实是这样, ApplicationContext 继承了 BeanFactory( 注意这里是接口的继承)。

Tips: BeanFactory 和 ApplicationContext 的区分关键点并不在于是否支持 Annotation。
因为 BeanFactory 接口本身并没有定义任何与 Xml 或者 Annotation 有关的方法。
只是 Spring恰好没有提供一个只实现了 BeanFactory 接口的类去支持
Anotation 形式的 Bean 定义读取。

两类容器的接口关系及相关实现类的 UML 图如下(注意图中并未对 Interface 和 Class 进行图形上的区分标注)。
在这里插入图片描述上图的容器实现类主要区别如下:

  • XmlBeanFactory 从 Xml 配置文件中获取 Bean 的描述信息, 该类已经从 Spring 3.1 之后被标注为 @Deprecated , 相对应的, 鼓励使用 XmlBeanDefinitionReaderDefaultListableBeanfacotory 来完成原本 XmlBeanFactory 所实现的功能。
  • FileSystemXmlApplicationContext 从特定完整路径的 Xml 配置文件获取 Bean 描述信息
  • ClassPathXmlApplicationContext 从类路径中的Xml 配置文件获取 Bean 描述信息
  • AnnotationConfigApplicationContext 从注解类型的配置中获取 Bean 的描述信息
  • AnnotationConfigWebApplicationContextXmlWebApplicationContext 顾名思义, 分别从 Annotation 和 Xml 中获取 Bean 的描述信息, 另外, 这种容器专门用于为 Web 类型的应用提供 Bean 的管理

只是两种不同的描述格式

对于 Spring 容器来说, 无论是通过 xml 获取 Bean 的定义, 还是通过 annotation 获取 bean 的定义, 只是两种不同的信息获取渠道而已, 前者通过解析 xml 文件内容完成, 后者通过扫描 java class 文件, 利用 jvm 提供的反射功能, 获取 annotation 信息 。

Spring 具体解析 xml 或着 class 文件的过程不必细究, 需要了解的是,这些 Bean 的描述信息, 最终会被存储在一个 BeanDefination 的实例中, spring 容器后续会以此为基础来创建以及管理 Bean

BeanDefinition

正如前文所描述, 通过 xml 和 annotation 获取到 Bean 的描述信息后, 肯定需要将其统一存储和管理起来。 在 Spring 框架代码中, Bean 的描述信息的最终存储形式即为 BeanDefinition

BeanDefinition 是一个接口, 在 Spring 中有三种实现:

  1. RootBeanDefinition
  2. ChildBeanDefinition
  3. GenericBeanDefinition

这三种实现,均继承自 AbstractBeanDefinition。

在这里插入图片描述
BeanDefinition 接口中包含了所有 Bean 描述信息的获取方法, 例如:

  • getBeanClassName()
  • isLazyInit()
  • getScope()

其中, RootBeanDefinition 是最常用的实现类, 它对应于 Xml 文档中的 标签 或者 Annotation 中的 @Component, @Bean 等类型注解所描述的 Bean 。

ChildBeanDefinition 的出现来源于 Xml 文档中提供的 Bean 继承功能, 这项功能在 Annotation 中并没有对应实现, 原因是使用 Annotation 描述 Bean 时, java 语言本身就足以提供 xml 文档通过 Bean 继承实现的功能。

GenericBeanDefinition 则主要用于注册用户可见的 BeanDefinition, 例如, 用户会通过 post-processor 来修改 BeanDefinition 的属性。

BeanDefinition 的管理

明确了 BeanDefinition 的概念以后, 就可以思考 Spring 如何管理 BeanDefinition 了, 整体首先肯定要划分为如下步骤:

  1. 从 Xml 或 Annotation 中读取 Bean 的原始描述信息, 实例化为 BeanDefinition 接口实现类的实例
  2. 将众多 BeanDefinition 的信息统一存储到一个数据结构中, 最容易想到的是一个 Map 中
  3. 根据需求, 在特定的时刻获取 BeanDefinition , 完成 Bean 的创建与管理
BeanDefinition 的读取与注册

由于 Annotation 是现在最为广泛使用的 Bean 定义来源,以 Spring Boot 框架为例学习, BeanDefiniton 的读取与注册过程。

Spring Boot 典型的包含 main 方法的启动类代码如下:

@SpringBootApplication
public class Application {
   
    public static void main(String[] args) {
   
        SpringApplication.run(Application.class, args);
    }
}    

对于 SpringApplication 的 run 方法中的源码做删减之后, 可以得到如下的核心步骤

		// 注意下面代码在源码基础上删减了一些次要步骤, 例如 environment , applicatioNArguments 的的构建操作等
		ConfigurableApplicationContext context = null;
		SpringApplicationRunListeners listeners = getRunListeners(args);
		listeners.starting();
		try {
   
			ApplicationArguments applicationArguments = new DefaultApplicationArguments(
					args);
			prepareContext(context, environment, listeners, applicationArguments,
					printedBanner);
					
			context = createApplicationContext();
			
			refreshContext(context);
			
			afterRefresh(context, applicationArguments);
		}

上面的源码完成了

  • 应用启动时监听器 SpringApplicationRunListener 获取注册
  • ApplicationContext 的预处理( prepare)
  • ApplicationContext 的创建 ( create )
  • ApplicationContext 的刷新(refresh)

(拓展内容开始)

在执行 prepareContext 方法时, 会广播一个 ApplicationEnvironmentPrepared类型的 event, 触发监听了该事件类型的 ApplicationListener 的 onApplicationEvent(ApplicationEnvironmentPreparedEvent event) 方法执行。 如果使用了 SpringCloud, 此时会触发BootstrapApplicationListener 的 onApplicationEvent 方法, 其中包含一段如下代码

		ConfigurableApplicationContext context = null;
		... 其他代码...
		if (context == null) {
			context = bootstrapServiceContext(environment, event.getSpringApplication(),
					configName);
		}
		... 其他代码...

这里的 bootstrapServiceContext 方法内部还会再次调用 SpringApplication.run() 的方法, 创建一个 contextId 为 “bootstrap” 的 context, 该 context 会成为主应用 ApplicationContext 的 parent 。

更详细的信息可以参见: Spring Cloud Context: Application Context Services, 这里为保持文章主线清晰, 不做展开。

拓展内容结束


回到 SpringApplication.run() 的主流程中, 如果启动的是一个典型的 web 应用, 那么 createApplicationContext() 会返回一个 AnnotationConfigServletWebServerApplicationContext 的实例。

在这里观察下 ApplicationContext 中 beanFacotory 中的变量 beanDefinitionMap
在这里插入图片描述
可以看到, 在刚刚执行完 createApplicationContext() 方法后, 获得的 context 实例中的 beanDefinitionMap 变量中已经产生了 6 个键值对。

这说明在这个步骤中, 已经发生了 beanDefinition 的获取。 于是查看一下 createApplicationContext 的内容

... 其他代码 ...
public static final String DEFAULT_CONTEXT_CLASS = "org.springframework.context."
			+ "annotation.AnnotationConfigApplicationContext";

public static final String DEFAULT_SERVLET_WEB_CONTEXT_CLASS = "org.springframework.boot."
			+ "web.servlet.context.AnnotationConfigServletWebServerApplicationContext";

public static final String DEFAULT_REACTIVE_WEB_CONTEXT_CLASS = "org.springframework."
			+ "boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext";

... 其他代码 ...
protected ConfigurableApplicationContext createApplicationContext() {
   
		Class<?> contextClass = this.applicationContextClass;
		if (contextClass == null) {
   
			try {
   
				switch (this.webApplicationType) {
   
				case SERVLET:
					contextClass = Class.forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS);
					break;
				case REACTIVE:
					contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);
					break;
				default:
					contextClass = Class.forName(DEFAULT_CONTEXT_CLASS);
				}
			}
			... 其他代码 ...
		}
		// 下面是实例化操作
		return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass);
	}

可以看到, createApplicationContext() 方法就是简单的根据应用类型, 实例化了特定的 ApplicationContext 类, 在笔者测试的样例中, 由于测试的是 Servlet 类型的应用, 这里实例化的类就是 AnnotationConfigServletWebServerApplicationContext

进一步查看 BeanUtils.instantiateClass(contextClass) 的方法, 可以看到实例化操作是通过调用 context 类的无参数构造器(constructor) 完成的。

public static <T> T instantiateClass(Class<T> clazz) throws BeanInstantiationException {
   
		... 其他代码 ...
		try {
   
			Constructor<T> ctor = (KotlinDetector.isKotlinType(clazz) ?
					KotlinDelegate.getPrimaryConstructor(clazz) : clazz.getDeclaredConstructor());
			return instantiateClass(ctor);
		}
		... 其他代码 ...
	}

进一步查看 AnnotationConfigServletWebServerApplicationContext 无参构造函数内容

public AnnotationConfigServletWebServerApplicationContext() {
   
		this.reader = new AnnotatedBeanDefinitionReader(this);
		this.scanner = new ClassPathBeanDefinitionScanner(this);
	}

构造函数初始化了 AnnotatedBeanDefinitionReaderClassPathBeanDefinitionScanner 两个变量, 从字面上可以看出来, 这两个类分别负责了 BeanDeifinition 的读取和扫描。

当然, 到这里为止, 依然没有发生任何 beanDefinition 的获取, 我们需要继续追踪, 先前的 6 个 beanDefinition 是在哪里获取的。

步进到 AnnotatedBeanDefinitionReader 的构造函数中

public AnnotatedBeanDefinitionReader(BeanDefinitionRegistry registry, Environment environment) {
   
		Assert.notNull(registry, "BeanDefinitionRegistry must not be null");
		Assert.notNull(environment, "Environment must not be null");
		this.registry = registry;
		this.conditionEvaluator = new ConditionEvaluator(registry, environment, null)
  • 9
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值