設計模式之美 - 45 | 工廠模式(下):如何設計實現一個Dependency Injection框架?

這系列相關博客,參考 設計模式之美

在上一節課我們講到,當創建對象是一個“大工程”的時候,我們一般會選擇使用工廠模式,來封裝對象複雜的創建過程,將對象的創建和使用分離,讓代碼更加清晰。那何爲“大工程”呢?上一節課中我們講了兩種情況,一種是創建過程涉及複雜的 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("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.xzg.RateLimiter">
		<constructor-arg ref="redisCounter"/>
	</bean>
	
	<bean id="redisCounter" class="com.xzg.redisCounter">
		<constructor-arg type="String" value="127.0.0.1">
		<constructor-arg type="int" value=1234>
	</bean>
</beans>

對象創建

其次,我們再來看對象創建。

在 DI 容器中,如果我們給每個類都對應創建一個工廠類,那項目中類的個數會成倍增加,這會增加代碼的維護成本。要解決這個問題並不難。我們只需要將所有類對象的創建都放到一個工廠類中完成就可以了,比如 BeansFactory。

你可能會說,如果要創建的類對象非常多,BeansFactory 中的代碼會不會線性膨脹(代碼量跟創建對象的個數成正比)呢?實際上並不會。待會講到 DI 容器的具體實現的時候,我們會講“反射”這種機制,它能在程序運行的過程中,動態地加載類、創建對象,不需要事先在代碼中寫死要創建哪些對象。所以,不管是創建一個對象還是十個對象,BeansFactory 工廠類代碼都是一樣的。

生命週期管理

最後,我們來看對象的生命週期管理。

上一節課我們講到,簡單工廠模式有兩種實現方式,一種是每次都返回新創建的對象,另一種是每次都返回同一個事先創建好的對象,也就是所謂的單例對象。在 Spring 框架中,我們可以通過配置 scope 屬性,來區分這兩種不同類型的對象。scope=prototype 表示返回新創建的對象,scope=singleton 表示返回單例對象。

除此之外,我們還可以配置對象是否支持懶加載。如果 lazy-init=true,對象在真正被使用到的時候(比如:BeansFactory.getBean(“userService”))才被被創建;如果 lazy-init=false,對象在應用啓動的時候就事先創建好。

不僅如此,我們還可以配置對象的 init-method 和 destroy-method 方法,比如 initmethod=loadProperties(),destroy-method=updateConfigFile()。DI 容器在創建好對象之後,會主動調用 init-method 屬性指定的方法來初始化對象。在對象被最終銷燬之前,DI 容器會主動調用 destroy-method 屬性指定的方法來做一些清理工作,比如釋放數據庫連接池、關閉文件。

如何實現一個簡單的 DI 容器?

實際上,用 Java 語言來實現一個簡單的 DI 容器,核心邏輯只需要包括這樣兩個部分:配置文件解析、根據配置文件通過“反射”語法來創建對象。

1. 最小原型設計

因爲我們主要是講解設計模式,所以,在今天的講解中,我們只實現一個 DI 容器的最小原型。像 Spring 框架這樣的 DI 容器,它支持的配置格式非常靈活和複雜。爲了簡化代碼實現,重點講解原理,在最小原型中,我們只支持下面配置文件中涉及的配置語法。

配置文件beans.xml
<beans>
	<bean id="rateLimiter" class="com.xzg.RateLimiter">
		<constructor-arg ref="redisCounter"/>
	</bean>
	
	<bean id="redisCounter" class="com.xzg.redisCounter" scope="singleton" la
		<constructor-arg type="String" value="127.0.0.1">
		<constructor-arg type="int" value=1234>
	</bean>
</beans>

最小原型的使用方式跟 Spring 框架非常類似,示例代碼如下所示:

public class Demo {
	public static void main(String[] args) {
		ApplicationContext applicationContext = new ClassPathXmlApplicationConte
				"beans.xml");
		RateLimiter rateLimiter = (RateLimiter) applicationContext.getBean("rate
		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("/" + configLocation);
			if (in == null) {
				throw new RuntimeException("Can not find config file: " + configLoca
			}
			List<BeanDefinition> 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<BeanDefinition> parse(InputStream inputStream);
	List<BeanDefinition> parse(String configContent);
}

public class XmlBeanConfigParser implements BeanConfigParser {

	@Override
	public List<BeanDefinition> parse(InputStream inputStream) {
		String content = null;
		// TODO:...
		return parse(content);
	}
	
	@Override
	public List<BeanDefinition> parse(String configContent) {
		List<BeanDefinition> beanDefinitions = new ArrayList<>();
		// TODO:...
		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;
	// 省略必要的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<String, Object> singletonObjects = new Concurren
	private ConcurrentHashMap<String, BeanDefinition> beanDefinitions = new Co
	
	public void addBeanDefinitions(List<BeanDefinition> beanDefinitionList) {
		for (BeanDefinition beanDefinition : beanDefinitionList) {
			this.beanDefinitions.putIfAbsent(beanDefinition.getId(), beanDefinitio
		}
		for (BeanDefinition beanDefinition : beanDefinitionList) {
			if (beanDefinition.isLazyInit() == false && beanDefinition.isSingleton
				createBean(beanDefinition);
			}
		}
	}
	
	public Object getBean(String beanId) {
		BeanDefinition beanDefinition = beanDefinitions.get(beanId);
		if (beanDefinition == null) {
			throw new NoSuchBeanDefinitionException("Bean is not defined: " + bean
		}
		return createBean(beanDefinition);
	}
	
	@VisibleForTesting
	protected Object createBean(BeanDefinition beanDefinition) {
		if (beanDefinition.isSingleton() && singletonObjects.contains(beanDefini
			return singletonObjects.get(beanDefinition.getId());
		}
		
		Object bean = null;
		try {
			Class beanClass = Class.forName(beanDefinition.getClassName());
			List<BeanDefinition.ConstructorArg> args = beanDefinition.getConstruct
			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.getIsRef()) {
						argClasses[i] = arg.getType();
						argObjects[i] = arg.getArg();
					} else {
						BeanDefinition refBeanDefinition = beanDefinitions.get(arg.getAr
						if (refBeanDefinition == null) {
							throw new NoSuchBeanDefinitionException("Bean is not defined:
						}
						argClasses[i] = Class.forName(refBeanDefinition.getClassName());
						argObjects[i] = createBean(refBeanDefinition);
					}
				}
				bean = beanClass.getConstructor(argClasses).newInstance(argObjects);
			}
		} catch (ClassNotFoundException | IllegalAccessException
				| InstantiationException | NoSuchMethodException | InvocationTar
			throw new BeanCreationFailureException("", e);
		}
		
		if (bean != null && 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() 函數是否會出現堆棧溢出?又該如何解決這個問題呢?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章