CategoryResourceRepost/极客时间专栏/设计模式之美/设计模式与范式:创建型/45 | 工厂模式(下):如何设计实现一个Dependency Injection框架?.md
louzefeng d3828a7aee mod
2024-07-11 05:50:32 +00:00

320 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<audio id="audio" title="45 | 工厂模式如何设计实现一个Dependency Injection框架" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/96/ff/9656485234582086d0fdd37ed1bc4fff.mp3"></audio>
在上一节课我们讲到当创建对象是一个“大工程”的时候我们一般会选择使用工厂模式来封装对象复杂的创建过程将对象的创建和使用分离让代码更加清晰。那何为“大工程”呢上一节课中我们讲了两种情况一种是创建过程涉及复杂的if-else分支判断另一种是对象创建需要组装多个其他类对象或者需要复杂的初始化过程。
今天我们再来讲一个创建对象的“大工程”依赖注入框架或者叫依赖注入容器Dependency Injection Container简称DI容器。在今天的讲解中我会带你一块搞清楚这样几个问题DI容器跟我们讲的工厂模式又有何区别和联系DI容器的核心功能有哪些以及如何实现一个简单的DI容器
话不多说,让我们正式开始今天的学习吧!
## 工厂模式和DI容器有何区别
实际上DI容器底层最基本的设计思路就是基于工厂模式的。DI容器相当于一个大的工厂类负责在程序启动的时候根据配置要创建哪些类对象每个类对象的创建需要依赖哪些其他类对象事先创建好对象。当应用程序需要使用某个类对象的时候直接从容器中获取即可。正是因为它持有一堆对象所以这个框架才被称为“容器”。
DI容器相对于我们上节课讲的工厂模式的例子来说它处理的是更大的对象创建工程。上节课讲的工厂模式中一个工厂类只负责某个类对象或者某一组相关类对象继承自同一抽象类或者接口的子类的创建而DI容器负责的是整个应用中所有类对象的创建。
除此之外DI容器负责的事情要比单纯的工厂模式要多。比如它还包括配置的解析、对象生命周期的管理。接下来我们就详细讲讲一个简单的DI容器应该包含哪些核心功能。
## DI容器的核心功能有哪些
总结一下一个简单的DI容器的核心功能一般有三个配置解析、对象创建和对象生命周期管理。
**首先,我们来看配置解析。**
在上节课讲的工厂模式中工厂类要创建哪个类对象是事先确定好的并且是写死在工厂类代码中的。作为一个通用的框架来说框架代码跟应用代码应该是高度解耦的DI容器事先并不知道应用会创建哪些对象不可能把某个应用要创建的对象写死在框架代码中。所以我们需要通过一种形式让应用告知DI容器要创建哪些对象。这种形式就是我们要讲的配置。
我们将需要由DI容器来创建的类对象和创建类对象的必要信息使用哪个构造函数以及对应的构造函数参数都是什么等等放到配置文件中。容器读取配置文件根据配置文件提供的信息来创建对象。
下面是一个典型的Spring容器的配置文件。Spring容器读取这个配置文件解析出要创建的两个对象rateLimiter和redisCounter并且得到两者的依赖关系rateLimiter依赖redisCounter。
```
public class RateLimiter {
private RedisCounter redisCounter;
public RateLimiter(RedisCounter redisCounter) {
this.redisCounter = redisCounter;
}
public void test() {
System.out.println(&quot;Hello World!&quot;);
}
//...
}
public class RedisCounter {
private String ipAddress;
private int port;
public RedisCounter(String ipAddress, int port) {
this.ipAddress = ipAddress;
this.port = port;
}
//...
}
配置文件beans.xml
&lt;beans&gt;
&lt;bean id=&quot;rateLimiter&quot; class=&quot;com.xzg.RateLimiter&quot;&gt;
&lt;constructor-arg ref=&quot;redisCounter&quot;/&gt;
&lt;/bean&gt;
&lt;bean id=&quot;redisCounter&quot; class=&quot;com.xzg.redisCounter&quot;&gt;
&lt;constructor-arg type=&quot;String&quot; value=&quot;127.0.0.1&quot;&gt;
&lt;constructor-arg type=&quot;int&quot; value=1234&gt;
&lt;/bean&gt;
&lt;/beans&gt;
```
**其次,我们再来看对象创建。**
在DI容器中如果我们给每个类都对应创建一个工厂类那项目中类的个数会成倍增加这会增加代码的维护成本。要解决这个问题并不难。我们只需要将所有类对象的创建都放到一个工厂类中完成就可以了比如BeansFactory。
你可能会说如果要创建的类对象非常多BeansFactory中的代码会不会线性膨胀代码量跟创建对象的个数成正比实际上并不会。待会讲到DI容器的具体实现的时候我们会讲“反射”这种机制它能在程序运行的过程中动态地加载类、创建对象不需要事先在代码中写死要创建哪些对象。所以不管是创建一个对象还是十个对象BeansFactory工厂类代码都是一样的。
**最后,我们来看对象的生命周期管理。**
上一节课我们讲到简单工厂模式有两种实现方式一种是每次都返回新创建的对象另一种是每次都返回同一个事先创建好的对象也就是所谓的单例对象。在Spring框架中我们可以通过配置scope属性来区分这两种不同类型的对象。scope=prototype表示返回新创建的对象scope=singleton表示返回单例对象。
除此之外我们还可以配置对象是否支持懒加载。如果lazy-init=true对象在真正被使用到的时候比如BeansFactory.getBean(“userService”)才被被创建如果lazy-init=false对象在应用启动的时候就事先创建好。
不仅如此我们还可以配置对象的init-method和destroy-method方法比如init-method=loadProperties()destroy-method=updateConfigFile()。DI容器在创建好对象之后会主动调用init-method属性指定的方法来初始化对象。在对象被最终销毁之前DI容器会主动调用destroy-method属性指定的方法来做一些清理工作比如释放数据库连接池、关闭文件。
## 如何实现一个简单的DI容器
实际上用Java语言来实现一个简单的DI容器核心逻辑只需要包括这样两个部分配置文件解析、根据配置文件通过“反射”语法来创建对象。
### 1.最小原型设计
因为我们主要是讲解设计模式所以在今天的讲解中我们只实现一个DI容器的最小原型。像Spring框架这样的DI容器它支持的配置格式非常灵活和复杂。为了简化代码实现重点讲解原理在最小原型中我们只支持下面配置文件中涉及的配置语法。
```
配置文件beans.xml
&lt;beans&gt;
&lt;bean id=&quot;rateLimiter&quot; class=&quot;com.xzg.RateLimiter&quot;&gt;
&lt;constructor-arg ref=&quot;redisCounter&quot;/&gt;
&lt;/bean&gt;
&lt;bean id=&quot;redisCounter&quot; class=&quot;com.xzg.redisCounter&quot; scope=&quot;singleton&quot; lazy-init=&quot;true&quot;&gt;
&lt;constructor-arg type=&quot;String&quot; value=&quot;127.0.0.1&quot;&gt;
&lt;constructor-arg type=&quot;int&quot; value=1234&gt;
&lt;/bean&gt;
&lt;/bean
```
最小原型的使用方式跟Spring框架非常类似示例代码如下所示
```
public class Demo {
public static void main(String[] args) {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext(
&quot;beans.xml&quot;);
RateLimiter rateLimiter = (RateLimiter) applicationContext.getBean(&quot;rateLimiter&quot;);
rateLimiter.test();
//...
}
}
```
### 2.提供执行入口
前面我们讲到,面向对象设计的最后一步是:组装类并提供执行入口。在这里,执行入口就是一组暴露给外部使用的接口和类。
通过刚刚的最小原型使用示例代码我们可以看出执行入口主要包含两部分ApplicationContext和ClassPathXmlApplicationContext。其中ApplicationContext是接口ClassPathXmlApplicationContext是接口的实现类。两个类具体实现如下所示
```
public interface ApplicationContext {
Object getBean(String beanId);
}
public class ClassPathXmlApplicationContext implements ApplicationContext {
private BeansFactory beansFactory;
private BeanConfigParser beanConfigParser;
public ClassPathXmlApplicationContext(String configLocation) {
this.beansFactory = new BeansFactory();
this.beanConfigParser = new XmlBeanConfigParser();
loadBeanDefinitions(configLocation);
}
private void loadBeanDefinitions(String configLocation) {
InputStream in = null;
try {
in = this.getClass().getResourceAsStream(&quot;/&quot; + configLocation);
if (in == null) {
throw new RuntimeException(&quot;Can not find config file: &quot; + configLocation);
}
List&lt;BeanDefinition&gt; beanDefinitions = beanConfigParser.parse(in);
beansFactory.addBeanDefinitions(beanDefinitions);
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
// TODO: log error
}
}
}
}
@Override
public Object getBean(String beanId) {
return beansFactory.getBean(beanId);
}
}
```
从上面的代码中我们可以看出ClassPathXmlApplicationContext负责组装BeansFactory和BeanConfigParser两个类串联执行流程从classpath中加载XML格式的配置文件通过BeanConfigParser解析为统一的BeanDefinition格式然后BeansFactory根据BeanDefinition来创建对象。
### 3.配置文件解析
配置文件解析主要包含BeanConfigParser接口和XmlBeanConfigParser实现类负责将配置文件解析为BeanDefinition结构以便BeansFactory根据这个结构来创建对象。
配置文件的解析比较繁琐,不涉及我们专栏要讲的理论知识,不是我们讲解的重点,所以这里我只给出两个类的大致设计思路,并未给出具体的实现代码。如果感兴趣的话,你可以自行补充完整。具体的代码框架如下所示:
```
public interface BeanConfigParser {
List&lt;BeanDefinition&gt; parse(InputStream inputStream);
List&lt;BeanDefinition&gt; parse(String configContent);
}
public class XmlBeanConfigParser implements BeanConfigParser {
@Override
public List&lt;BeanDefinition&gt; parse(InputStream inputStream) {
String content = null;
// TODO:...
return parse(content);
}
@Override
public List&lt;BeanDefinition&gt; parse(String configContent) {
List&lt;BeanDefinition&gt; beanDefinitions = new ArrayList&lt;&gt;();
// TODO:...
return beanDefinitions;
}
}
public class BeanDefinition {
private String id;
private String className;
private List&lt;ConstructorArg&gt; constructorArgs = new ArrayList&lt;&gt;();
private Scope scope = Scope.SINGLETON;
private boolean lazyInit = false;
// 省略必要的getter/setter/constructors
public boolean isSingleton() {
return scope.equals(Scope.SINGLETON);
}
public static enum Scope {
SINGLETON,
PROTOTYPE
}
public static class ConstructorArg {
private boolean isRef;
private Class type;
private Object arg;
// 省略必要的getter/setter/constructors
}
}
```
### 4.核心工厂类设计
最后我们来看BeansFactory是如何设计和实现的。这也是我们这个DI容器最核心的一个类了。它负责根据从配置文件解析得到的BeanDefinition来创建对象。
如果对象的scope属性是singleton那对象创建之后会缓存在singletonObjects这样一个map中下次再请求此对象的时候直接从map中取出返回不需要重新创建。如果对象的scope属性是prototype那每次请求对象BeansFactory都会创建一个新的对象返回。
实际上BeansFactory创建对象用到的主要技术点就是Java中的反射语法一种动态加载类和创建对象的机制。我们知道JVM在启动的时候会根据代码自动地加载类、创建对象。至于都要加载哪些类、创建哪些对象这些都是在代码中写死的或者说提前写好的。但是如果某个对象的创建并不是写死在代码中而是放到配置文件中我们需要在程序运行期间动态地根据配置文件来加载类、创建对象那这部分工作就没法让JVM帮我们自动完成了我们需要利用Java提供的反射语法自己去编写代码。
搞清楚了反射的原理BeansFactory的代码就不难看懂了。具体代码实现如下所示
```
public class BeansFactory {
private ConcurrentHashMap&lt;String, Object&gt; singletonObjects = new ConcurrentHashMap&lt;&gt;();
private ConcurrentHashMap&lt;String, BeanDefinition&gt; beanDefinitions = new ConcurrentHashMap&lt;&gt;();
public void addBeanDefinitions(List&lt;BeanDefinition&gt; beanDefinitionList) {
for (BeanDefinition beanDefinition : beanDefinitionList) {
this.beanDefinitions.putIfAbsent(beanDefinition.getId(), beanDefinition);
}
for (BeanDefinition beanDefinition : beanDefinitionList) {
if (beanDefinition.isLazyInit() == false &amp;&amp; beanDefinition.isSingleton()) {
createBean(beanDefinition);
}
}
}
public Object getBean(String beanId) {
BeanDefinition beanDefinition = beanDefinitions.get(beanId);
if (beanDefinition == null) {
throw new NoSuchBeanDefinitionException(&quot;Bean is not defined: &quot; + beanId);
}
return createBean(beanDefinition);
}
@VisibleForTesting
protected Object createBean(BeanDefinition beanDefinition) {
if (beanDefinition.isSingleton() &amp;&amp; singletonObjects.contains(beanDefinition.getId())) {
return singletonObjects.get(beanDefinition.getId());
}
Object bean = null;
try {
Class beanClass = Class.forName(beanDefinition.getClassName());
List&lt;BeanDefinition.ConstructorArg&gt; args = beanDefinition.getConstructorArgs();
if (args.isEmpty()) {
bean = beanClass.newInstance();
} else {
Class[] argClasses = new Class[args.size()];
Object[] argObjects = new Object[args.size()];
for (int i = 0; i &lt; args.size(); ++i) {
BeanDefinition.ConstructorArg arg = args.get(i);
if (!arg.getIsRef()) {
argClasses[i] = arg.getType();
argObjects[i] = arg.getArg();
} else {
BeanDefinition refBeanDefinition = beanDefinitions.get(arg.getArg());
if (refBeanDefinition == null) {
throw new NoSuchBeanDefinitionException(&quot;Bean is not defined: &quot; + arg.getArg());
}
argClasses[i] = Class.forName(refBeanDefinition.getClassName());
argObjects[i] = createBean(refBeanDefinition);
}
}
bean = beanClass.getConstructor(argClasses).newInstance(argObjects);
}
} catch (ClassNotFoundException | IllegalAccessException
| InstantiationException | NoSuchMethodException | InvocationTargetException e) {
throw new BeanCreationFailureException(&quot;&quot;, e);
}
if (bean != null &amp;&amp; beanDefinition.isSingleton()) {
singletonObjects.putIfAbsent(beanDefinition.getId(), bean);
return singletonObjects.get(beanDefinition.getId());
}
return bean;
}
}
```
## 重点回顾
好了,今天的内容到此就讲完了。我们来一块总结回顾一下,你需要重点掌握的内容。
DI容器在一些软件开发中已经成为了标配比如Spring IOC、Google Guice。但是大部分人可能只是把它当作一个黑盒子来使用并未真正去了解它的底层是如何实现的。当然如果只是做一些简单的小项目简单会用就足够了但是如果我们面对的是非常复杂的系统当系统出现问题的时候对底层原理的掌握程度决定了我们排查问题的能力直接影响到我们排查问题的效率。
今天我们讲解了一个简单的DI容器的实现原理其核心逻辑主要包括配置文件解析以及根据配置文件通过“反射”语法来创建对象。其中创建对象的过程就应用到了我们在学的工厂模式。对象创建、组装、管理完全有DI容器来负责跟具体业务代码解耦让程序员聚焦在业务代码的开发上。
## 课堂讨论
BeansFactory类中的createBean()函数是一个递归函数。当构造函数的参数是ref类型时会递归地创建ref属性指向的对象。如果我们在配置文件中错误地配置了对象之间的依赖关系导致存在循环依赖那BeansFactory的createBean()函数是否会出现堆栈溢出?又该如何解决这个问题呢?
你可以可以在留言区说一说,和同学一起交流和分享。如果有收获,也欢迎你把这篇文章分享给你的朋友。