Spring下的控制反转(IOC)
2016-08-20 / modified at 2022-04-04 / 1.7k words / 6 mins
️This article has been over 2 years since the last update.

控制反转是一种简化开发的方法,可以轻易地获取对象而不用考虑如何构建,实现了高效解藕与测试。在Java后端、Android端、PHP等领域均很常见,比如Server端的Autowired、Android中的getSystemService都是控制反转的例子。

本文基于Spring4.0+进行分析

什么方向的控制被反转了?答案是依赖对象的获得。

IOC的简介

实现控制反转主要有两种方法,一个是依赖注入(DependenceInjection),另一个是依赖查找(DependenceLookUp)。前者是被动地接受所依耐的其它组件被Ioc容器注入,而后者通过主动调用一个全局的Map<Name, Component>查询实现。通过元数据可以剥离内部细节,降低样板代码。

至于两者的区别,在StackOverflow上的相关解答中,是这样介绍的

The main difference between the two approaches is “who is responsible for retrieving the dependencies”.

依赖注入的实现

依赖注入的描述方法

实现依赖注入,也就意味着注入框架能够理解组件间的构造关系,而这种构造关系一般采用标记语言进行描述,它们被称作元数据(metadata),更深入的话叫做领域专属语言(DSL),为了便于理解,下文均称作DSL。Spring支持多种DSL的描述方法

  • 以XML/YAML为主的外部DSL,它内部描述各种依赖情况
  • 通过Ruby、Groovy等动态语言为描述的内部DSL
  • 通过直接对Java进行@Config标记(目前SpringBoot比较流行AutoConfig)

上面的具体实现各有便利性的优势,均由BeanFactory接口进行规范,当然在大型项目中,为了维护可控与降低门槛,一般采用XML对关系进行描述,基于XML的是DefaultBeanFactory。

解释器与动态代理

通过对DSL进行处理并输出结果的流程叫做Interceptor(解释器),而Context就是解释器运行时的上下文,在Spring中,输入是XML的依赖描述,输出是动态代理生成的Bean对象,很多Spring分析的厚书耗费很长时间去分析各种Java细节,但是万变不离其宗: 这种设计模式在很多用于简化开发的开源框架中均存在

  • 通过XML与OGNL简化SQL操作的MyBatis,具体实现是LanguageDriver
  • 通过为HTML加入Tag实现简化绑定的AngularJS,具体实现是$Parser
  • 通过注解简化请求的Feign/Retrofit
  • 通过上下文与HTML实现静态输出的TemplateEngine: 比如JSP/Jeklly

所以在分析源码时,千万不要陷入实现细节中,比如Spring中的Bean对象就是最核心的AST,而它之前的XML是如何转换解析并不重要

总体流程如下

1
XML/YAML --(Parser)--> AST(BeanDefinition) --(Eval)--> Map

基于XML/Java依赖注入的实现

虽然刚刚讲了并不重要,但是照顾初学者,在本文中将以Spring与XML为例,对依赖注入的过程进行分析。

Spring的主要流程如下图所

文本反序列化为AST(BeanDefination)

此部分发生在refresh操作前

XML实现

比如在XML中定义了一个bean,这个对象是一个Car,它需要使用oil作为构造引用

1
2
3
4
<bean id="textEditorConstructorBasedDI"
class="com.github.miao1007.Car">
<constructor-arg ref="oil" />
</bean>

第一步将XML文本转换为Dom元素,即org.w3c.dom.Document。这个是一个非常大众化的解析XML组件,从XML文本到Dom元素的具体实现也是一个累活,这里就不详细讲了。

1
2
3
4
5
springrest.xml(XML文件)
|
w3c解析XML
|
DOM元素(java对象)

接着进行词法分析(通过BeanDefinitionParser实现)与反序列化对象。首先将XML中的字符串转换为Token,此部分同样干的得都是累活,主要是遍历DOM,然后for-switch循环匹配XML中的属性(比如classNode等),并最终合并并构造为BeanDefination对象。

纯Java实现

代码很短,我可以把它全贴出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Configuration
public class SpringBeanTest {

public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringBeanTest.class);
Car bean = context.getBean(Car.class);
System.out.println(bean.getOil().getName());
}

@Bean
public Car getCar(Oil oil) {
Car car = new Car();
car.setOil(oil);
return car;
}

@Bean
public Oil getOil() {
Oil oil = new Oil();
oil.setName("Shell");
return oil;
}
}

没错,就是这么简答,Spring不需要容器也是可以运行的。源码也比较容易阅读,解析注解并转为下面的对象

1
2
3
4
5
class BeanDefinition{
String beanclass;//com.github.miao1007.Car
ConstructorArgumentValues constructorArgumentValues;
// ConstructorArgumentValues在内部维护着`List<ValueHolder>`,它内部维护着Oil的对象引用
}

将这个大Class放到了beanDefinitionMap中

1
beanDefinitionMap = new ConcurrentHashMap<String, BeanDefinition>(256);

通过模式匹配解析遍历AST(Refresh)

此步骤发生在Refresh阶段,拥有了上下文等信息,现在可以启动了,此时我们分析就脱离了XML等文本细节,而是全部基于BeanDefinition这个AST进行分析。我们主要分析refresh中invokeBeanFactoryPostProcessorsfinishBeanFactoryInitialization如何通过DefaultListableBeanFactory首先找到Oil,再找到Car。

我们接着分析基于JavaConfig的Bean注入

invokeBeanFactoryPostProcessors阶段,通过如下方法解析Config注解中的Method,返回了所有的@Bean,注意这里是基于代码写作顺序的,Car是在前面的

1
ConfigurationClassBeanDefinitionReader#loadBeanDefinitions

上述操作可以看作为一个对Tree的FlatMap的操作,通过一堆for循环实现,保证到下一步前所有的Bean都会被扫描进入

finishBeanFactoryInitialization阶段,将按照代码写作顺序依次实例化Bean,通过如下方法实现依赖获取它的依赖Oil

1
2
3
4
5
6
//resolveDependency(DependencyDescriptor, s, s[], TypeConverter);
for (String candidate : candidateNames) {
if (!isSelfReference(beanName, candidate) && isAutowireCandidate(candidate, descriptor)) {
addCandidateEntry(result, candidate, descriptor, requiredType);
}
}

这里是一个深度遍历操作,找到最终无依赖的Bean才进行初始化

然后Oil通过常规操作反射调用获得Bean

SUM

总的来说,在IOC的内部并没有其它非常高技巧类的代码,更多的反而是对元数据扫描、解析的纯业务工作,本文也是特例中的特例情况分析,总体上Spring能够做到非常完善的CornerCase处理。

另外网上教你如何在Spring拧螺丝的面试宝典实际上并没有用,因为面试时还是会问你怎么造火箭的。

附录

SpringSchema

SpringSchema是Spring提供的XML扩展。通过编写自己定制的XSD与BeanDefinationParser,可以实现更定制化的XML语法,比如阿里的Dubbo就使用了此扩展实现了ZK的事务注册与发布。

当然上述的 FreeMarker/SpringSchema 对应用开发来说,很难见到。毕竟自己能力与代码量很难达到设计软件架构的水平。

如何写一个高扩展的开源框架

比较保险的就是解释器 + 模式匹配路由到生成器(比如动态代理) + 拦截器,大部分扩展特性均可以支持