概述
上篇文章《设计模式与范式 - 创建型:4.工厂模式(上):工厂模式的介绍和使用场景分析》讲到,当创建对象是一个 "大工程" 时,一般会选择使用工厂模式,来封装对象复杂的创建过程,将对象的创建和使用分离,让代码更加清晰。那何为 "大工程" 呢?
上篇文章讲了两种情况。
- 一种是创建过程涉及复杂的 if-else 分支判断。
- 另一种是对象创建需要组装多个其他类对象或者需要复杂的初始化过程。
今天再讲一个创建对象的 "大工程",依赖注入框架,简称 DI 容器。
工厂模式和 DI 容器有何区别?
DI 容器底层最基本的设计思路就是基于工程模式的。DI 容器相当于一个大的工厂类,负责在程序启动的时候,根据配置事先创建好对象。当应用程序需要使用某个类对象时,直接从容器中获取即可。也是因为它持有一堆对象,所以这个框架才被称为 "容器"。
DI 容器相对于工厂模式的例子来说,它处理的是更大的对象创建工程。上一篇文章讲解的工厂模式中,一个工厂类只负责某个类对象或者某一组类对象的创建,而 DI 容器负责的是整个应用中所有类对象的创建。
此外,DI 容器负责的事情要比单纯的工厂模式要多。比如,它好包括配置的解析、对象生命周期的管理。
DI 容器的核心功能有哪些?
一个简单的 DI 容器的核心功能一般有三个:配置解析、对象创建和对象生命周期管理。
先看下配置解析
上篇文章讲的工程模式中,工厂类要要创建哪个类对象是事先确定好的,并且写死在工厂类代码中的。作为一个通用的框架来说,框架代码跟应用代码应该是高度解耦的,DI 容器事先并不知道应用会去创建哪些对象,不可能把某个应用要创建的对象写死在框架代码中。所以,我们需要通过一种形式,让应用告知 DI 容器要创建哪些对象。这种形式就是配置。
需要由 DI 容器来创建类对象和创建类对象的必要信息(使用哪个构造器以及对应地构造函数参数都是什么等等),放到配置文件中。容器读取配置文件,根据配置文件提供的信息来创建对象。
下面是一个典型的 Spring 容器的配置文件。Spring 容器读取这个配置文件,解析出要创建的两个对象: rateLimiter
和 redisCounter
,并且得到两者的依赖关系: rateLimiter
一来 redisCounter
。
java
public class RateLimiter {
private RedisCounter redisCounter;
public RateLimiter(RedisCounter redisCounter) {
this.redisCounter = redisCounter;
}
public void test() {
System.out.println("hello world");
}
// ...
}
public class RedisCounter {
private String ipAddress;
private int port;
public RedisCounter(String ipAddress, int port) {
this.ipAddress = ipAddress;
this.port = port;
}
// ...
}
// 配置文件 beans.xml
<beans>
<bean id="rateLimiter" class="com.example.test.di.RateLimiter">
<constractor-arg ref="redisCounter" />
</bean>
<bean id="redisCounter" class="com.example.test.di.RedisCounter">
<constractor-arg type="String" value="127.0.0.1" />
<constractor-arg type="int" value=1234 />
</bean>
</beans>
再看下对象创建
在 DI 容器中,如果给每个类都对应一个创建工厂类,那项目中类的个数会成倍增加,这会增加代码的维护成本。要解决这个问题并不难。只需要将所有类对象的创建都放到一个工厂类中完成就可以了,比如 BeansFactory
。
可能你会说,如果要创建的类对象非常多, BeansFactory
中的代码会不会线性膨胀(代码量跟创建对象的个数成正比)呢?实际上并不会。DI 容器会使用 "反射" 这种机制,在程序运行的过程中,动态地加载类、创建对象,不需要事先在代码中写死要创建哪些对象。所以,不管是创建一个对象还是十个对象, BeansFactory
工厂类的代码都是一样的。
最后,看下对象的生命周期管理
上篇文件讲到,简单工厂模式有两种实现方式,一种是每次都返回新创建的对象,另一种是每次都返回同一个事先创建好的对象,也就是单例对象。
在 Spring 框架中,可以通过配置 scope
属性,来区分这两种不同类型的对象。
scope=prototype
表示返回新创建的对象scope=singleton
表示返回单例对象
不仅如此,还可以配置对象的 init-method
和 destroy-method
方法,比如 init-method=loadProperties()
, destroy-method=updateConfigFile()
。DI 容器在创建好对象之后,会主动调用 init-method
属性指定的方法来初始化对象。在对象被销毁之前,DI 容器会主动调用 destroy-method
属性指定的方法来做一些清理工作,比如释放数据库连接池、关闭文件。
如何实现一个简单的 ID 容器?
用 Java 语言来实现一个简单的 DI 容器,核心逻辑只需要包括这样两个部分:配置文件解析、根据配置文件通过 "反射" 语法来创建对象。
1.最小原型设计
因为主要讲解设计模式,所以我们只实现一个 DI 容器的最小原型。像 Spring 框架这样的 DI 容器,它支持的配置格式非常灵活和复杂。为了简化代码的实现,重点讲解原理,在最小原型中,我们支持下面配置文件中涉及的配置语法。
xml
// 配置文件 beans.xml
<beans>
<bean id="rateLimiter" class="com.example.test.di.RateLimiter">
<constractor-arg ref="redisCounter" />
</bean>
<bean id="redisCounter" class="com.example.test.di.RedisCounter">
<constractor-arg type="String" value="127.0.0.1" />
<constractor-arg type="int" value=1234 />
</bean>
</beans>
最小原型的使用方式跟 Spring 框架非常类似,示例代码如下所示:
java
public class Demo {
public static void main(String[] args) {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml");
RateLimiter rateLimiter = (RateLimiter) applicationContext.getBean("rateLimiter");
rateLimiter.test();
// ...
}
}
2.提供执行入口
面向对象设计的最后一步是:组装类并提供执行入口。在这里,执行入口就是一组暴露给外部使用的接口和类。
通过刚刚的最小原型使用实例代码,可以看出,执行入口主要包含两部分: ApplicationContext
和 ClassPathXmlApplicationContext
。其中, ApplicationContext
是接口, ClassPathXmlApplicationContext
是接口的实现类。两个类具体实现如下所示:
java
public interface ApplicationContext {
Object getBean(String beanId);
}
public class ClassPathXmlApplicationContext implements ApplicationContext {
private BeanFactory beanFactory;
private BeanConfigParser beanConfigParser;
public ClassPathXmlApplicationContext(String configLocation) {
this.beanFactory = new BeanFactory();
this.beanConfigParser = new XmlBeanConfigParser();
loadBeanDefinitions(configLocation);
}
private void loadBeanDefinitions(String configLocation) {
InputStream in = null;
try {
in = this.getClass().getResourceAsStream("/" + configLocation);
if (in == null) {
throw new RuntimeException("Could not config file: " + configLocation);
}
List<BeanDefinition> beanDefinitions = beanConfigParser.parse(in);
beanFactory.addBeanDefinition(beanDefinitions);
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
// TODO:log error
}
}
}
}
@Override
public Object getBean(String beanId) {
return beanFactory.getBean(beanId);
}
}
从上面的代码可以看出, ClassPathXmlApplicationContext
复杂组装 BeanFactory
和 BeanConfigParser
两个类,串联执行流程:从 classpath 中加载 XML 格式的配置文件,通过 BeanConfigParser
解析为统一的 BeanDefinition
格式,然后, BeanFactory
根据 BeanDefinition
来创建对象。
3.配置文件解析
配置文件解析主要包含 BeanConfigParser
接口和 XmlBeanConfigParser
实现类,负责将配置文件解析为 BeanDefinition
结构,以便 BeanFactory
根据这个结构来创建对象。
配置文件的解析比较繁琐,不是讲解的重点,所以这里只给出两个类的大致设计思路,并未给出具体的实现代码,你可以自行补充完整。
java
public interface BeanConfigParser {
List<BeanDefinition> parse(InputStream in);
List<BeanDefinition> parse(String configContent);
}
public class XmlBeanConfigParser implements BeanConfigParser {
@Override
public List<BeanDefinition> parse(InputStream in) {
String content = null;
// ...
return parse(content);
}
@Override
public List<BeanDefinition> parse(String configContent) {
List<BeanDefinition> beanDefinitions = new ArrayList<>();
// ...
return beanDefinitions;
}
}
public class BeanDefinition {
private String id;
private String className;
private List<ConstructorArg> constructorArgs = new ArrayList<>();
private Scope scope = Scope.SINGLETON;
private boolean lazyInit = false;
public boolean isLazyInit() {
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.核心工厂类设计
最后看下, BeanFactory
是如何设计与实现的。这也是我们这个 DI 容器最核心的一个类。它负责根据从配置文件解析得到的 BeanDefinition
来创建对象。
如果对象的 scope
属性是 singleton
,那对象创建之后会缓存在 singletonObjects
这样一个 map 中,下次再请求此对象时,直接从 map 中取出返回,不需要重新创建。如果对象的 scop
属性是 prototype
,那每次请求对象,BeanFactory
都会创建一个新的对象。
实际上,BeanFactory
创建对象用到的主要技术点就是 Java 中的反射语法:一种动态加载类和创建对象的机制。JVM 在启动时会根据代码自动加载类、创建对象。至于要加载哪些类、创建哪些对象,这些都是在代码中写死,或者说是提前写好的。但是如果某个对象的创建并不是写死在代码中,而是放到配置文件中,我们需要在程序运行期间,动态地根据配置文件来加载类、创建对象,这部分工作就没法让 JVM 帮我们自动完成了,我们需要利用 Java 提供的反射语法自己去编写代码。
搞清楚了反射的原理,BeanFactory
的代码就不难看懂了。具体代码实现如下所示:
java
public class BeanFactory {
private ConcurrentHashMap<String, Object> singletonObjects = new ConcurrentHashMap<>();
private ConcurrentHashMap<String, BeanDefinition> beanDefinitions = new ConcurrentHashMap<>();
public void addBeanDefinition(List<BeanDefinition> beanDefinitions) {
for (BeanDefinition beanDefinition : beanDefinitions) {
this.beanDefinitions.putIfAbsent(beanDefinition.getId(), beanDefinition);
}
for (BeanDefinition beanDefinition : beanDefinitions) {
if (!beanDefinition.isLazyInit() && beanDefinition.isSingleton()) {
createBean(beanDefinition);
}
}
}
public Object getBean(String beanId) {
BeanDefinition beanDefinition = this.beanDefinitions.get(beanId);
if (beanDefinition == null) {
throw new NoSuchBeanDefinitionException("Bean is not defined: " + beanId);
}
return createBean(beanDefinition);
}
@VisibleForTesting
protected Object createBean(BeanDefinition beanDefinition) {
if (beanDefinition.isSingleton() && beanDefinitions.contains(beanDefinition.getId())) {
return singletonObjects.get(beanDefinition.getId());
}
Object bean = null;
try {
Class beanClass = Class.forName(beanDefinition.getClassName());
List<BeanDefinition.ConstructorArg> 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 < args.size(); i++) {
BeanDefinition.ConstructorArg arg = args.get(i);
if (!arg.isRef()) {
argClasses[i] = arg.getType();
argObjects[i] = arg.getArg();
} else {
BeanDefinition refBeanDefinition = beanDefinitions.get(arg.getArg());
if (refBeanDefinition == null) {
throw new NoSuchBeanDefinitionException("bean is not defined: " + 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("", e);
}
if (bean != null && beanDefinition.isSingleton()) {
singletonObjects.putIfAbsent(beanDefinition.getId(), bean);
return singletonObjects.get(beanDefinition.getId());
}
return bean;
}
}
总结
DI 容器在一些软件开发中已经成为了标配,比如 Spring IOC。但是,大部分人只是把它当做一个黑盒子来使用,并未真正去了解它的底层是如何实现的。当然,如果只是做一些简单的小项目,简单会用就足够了,但是如果我们面对的是非常复杂的系统,当系统出现问题时,对底层原理的掌握,决定了我们排查问题的能力,直接影响到我们排查问题的效率。
今天讲解了一个简单 DI 容器的实现原理,其核心逻辑包括: 配置文件解析,以及根据配置文件通过 "反射" 语法来创建对象。其中,创建对象的过程就应用到了我们在学的工厂模式。对象创建、组装、管理完全由 DI 容器来负责,跟具体业务代码解耦,让程序员聚焦在业务代码的开发商。