️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 | <bean id="textEditorConstructorBasedDI" |
第一步将XML文本转换为Dom元素,即org.w3c.dom.Document
。这个是一个非常大众化的解析XML组件,从XML文本到Dom元素的具体实现也是一个累活,这里就不详细讲了。
1 | springrest.xml(XML文件) |
接着进行词法分析(通过BeanDefinitionParser实现)与反序列化对象。首先将XML中的字符串转换为Token,此部分同样干的得都是累活,主要是遍历DOM,然后for-switch循环匹配XML中的属性(比如class
,Node
等),并最终合并并构造为BeanDefination对象。
纯Java实现
代码很短,我可以把它全贴出来
1 |
|
没错,就是这么简答,Spring不需要容器也是可以运行的。源码也比较容易阅读,解析注解并转为下面的对象
1 | class BeanDefinition{ |
将这个大Class放到了beanDefinitionMap中
1 | beanDefinitionMap = new ConcurrentHashMap<String, BeanDefinition>(256); |
通过模式匹配解析遍历AST(Refresh)
此步骤发生在Refresh阶段,拥有了上下文等信息,现在可以启动了,此时我们分析就脱离了XML等文本细节,而是全部基于BeanDefinition这个AST进行分析。我们主要分析refresh中invokeBeanFactoryPostProcessors
与finishBeanFactoryInitialization
如何通过DefaultListableBeanFactory
首先找到Oil,再找到Car。
我们接着分析基于JavaConfig的Bean注入
在invokeBeanFactoryPostProcessors
阶段,通过如下方法解析Config注解中的Method,返回了所有的@Bean
,注意这里是基于代码写作顺序的,Car是在前面的
1 | ConfigurationClassBeanDefinitionReader#loadBeanDefinitions |
上述操作可以看作为一个对Tree的FlatMap的操作,通过一堆for循环实现,保证到下一步前所有的Bean都会被扫描进入
在finishBeanFactoryInitialization
阶段,将按照代码写作顺序依次实例化Bean,通过如下方法实现依赖获取它的依赖Oil
1 | //resolveDependency(DependencyDescriptor, s, s[], TypeConverter); |
这里是一个深度遍历操作,找到最终无依赖的Bean才进行初始化
然后Oil通过常规操作反射调用获得Bean
SUM
总的来说,在IOC的内部并没有其它非常高技巧类的代码,更多的反而是对元数据扫描、解析的纯业务工作,本文也是特例中的特例情况分析,总体上Spring能够做到非常完善的CornerCase处理。
另外网上教你如何在Spring拧螺丝的面试宝典实际上并没有用,因为面试时还是会问你怎么造火箭的。
附录
SpringSchema
SpringSchema是Spring提供的XML扩展。通过编写自己定制的XSD与BeanDefinationParser,可以实现更定制化的XML语法,比如阿里的Dubbo就使用了此扩展实现了ZK的事务注册与发布。
当然上述的 FreeMarker/SpringSchema 对应用开发来说,很难见到。毕竟自己能力与代码量很难达到设计软件架构的水平。
如何写一个高扩展的开源框架
比较保险的就是解释器 + 模式匹配路由到生成器(比如动态代理) + 拦截器,大部分扩展特性均可以支持