我是風箏,公衆號「古時的風箏」,一個不僅有技術的技術公衆號,一個在程序圈混跡多年,主業 Java,另外 Python、React 也玩兒的 6 的斜槓開發者。 Spring Cloud 系列文章已經完成,能夠到 個人github 上查看系列完整內容。也能夠在公衆號內回覆「pdf」獲取我精心製做的 pdf 版完整教程。git
以前發了一個 Spring IoC 的預熱篇 「想要理解 Spring IoC,先要知道如何擴展 Spring 自定義 Bean」,有興趣的能夠看一看,如何在 Spring 中擴展自定義的 Bean,好比 標籤中有屬性 id 和 name,是如何實現的,咱們怎麼樣可以擴展出一個和 功能相似的標籤,可是屬性卻不同的功能呢?github
瞭解了自定義擴展 Bean 以後,再理解 Spring IoC 的過程相信會更加清楚。web
好了,正文開始。spring
Spring IoC,全稱 Inversion of Control - 控制反轉,還有一種叫法叫作 DI( Dependency Injection)-依賴注入。也能夠說控制反轉是最終目的,依賴注入是實現這個目的的具體方法。緩存
爲何叫作控制反轉呢。安全
在傳統的模式下,我想要使用另一個非靜態對象的時候會怎麼作呢,答案就是 new 一個實例出來。微信
舉個例子,假設有一個 Logger 類,用來輸出日誌的。定義以下:併發
public class Logger {
public void log(String text){ System.out.println("log:" + text); } } 複製代碼
那如今我要調用這個 log 方法,會怎麼作呢。app
Logger logger = new Logger();
logger.log("日誌內容");
複製代碼
對不對,以上就是一個傳統的調用模式。什麼時候 new 這個對象實例是由調用方來控制,或者說由咱們開發者本身控制,何時用就何時 new 一個出來。框架
而當咱們用了 Spring IoC 以後,事情就變得不同了。簡單來看,結果就是開發者不須要關心 new 對象的操做了。仍是那個 Logger 類,咱們在引入 Spring IoC 以後會如何使用它呢?
public class UserController {
@Autowired private Logger logger; public void log(){ logger.log("please write a log"); } } 複製代碼
開發者不建立對象,可是要保證對象被正常使用,不可能沒有 new 這個動做,這說不通。既然如此,確定是誰幫咱們作了這個操做,那就是 Spring 框架作了,準確的說是 Spring IoC Container 幫咱們作了。這樣一來,控制權由開發者轉變成了第三方框架,這就叫作控制反轉。
依賴注入的主謂賓補充完整,就是將調用者所依賴的類實例對象注入到調用者類。拿前面的那個例子來講,UserController 類就是調用者,它想要調用 Logger 實例化對象出來的 log 方法,logger 做爲一個實例化(也就是 new 出來的)對象,就是 UserController 的依賴對象,咱們在代碼中沒有主動使用 new 關鍵字,那是由於 Spring IoC Container 幫咱們作了,這個對於開發者來講透明的操做就叫作注入。
注入的方式有三種:構造方法的注入、setter 的注入和註解注入,前兩種方式基本上如今不多有人用了,開發中更多的是採用註解方式,尤爲是 Spring Boot 愈來愈廣泛的今天。咱們在使用 Spring 框架開發時,通常都用 @Autowired
,固然有時也能夠用 @Resource
@Autowired
private IUserService userService;
@Autowired private Logger logger; 複製代碼
前面說了注入的動做實際上是 Spring IoC Container 幫咱們作的,那麼 Spring IoC Container 到底是什麼呢?
本次要討論的就是上圖中的 Core Container 部分,包括 Beans、Core、Context、SpEL 四個部分。
Container 負責實例化,配置和組裝Bean,並將其注入到依賴調用者類中。Container 是管理 Spring 項目中 Bean 整個生命週期的管理者,包括 Bean 的建立、註冊、存儲、獲取、銷燬等等。
先從一個基礎款的例子提及。前面例子中的 @Bean
是用註解的方式實現的,這個稍後再說。既然是基礎款,那就逃不掉 xml 的,雖然如今都用 Spring Boot 了,但經過原始的 xml 方式能更加清晰的觀察依賴注入的過程,要知道,最先尚未 Spring Boot 的時候,xml 能夠說是 Spring 項目的紐帶,配置信息都大多數都來自 xml 配置文件。
首先添加一個 xml 格式的 bean 聲明文件,假設名稱爲 application.xml,若是你以前用過 Spring MVC ,那大多數狀況下對這種定義會很是熟悉。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bean="http://www.springframework.org/schema/c" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="logger" class="org.kite.spring.bean.Logger" /> </beans> 複製代碼
經過 <bean>
元素來聲明一個 Bean 對象,並指定 id 和 class,這是 xml 方式聲明 bean 對象的標準方式,若是你自從接觸 Java 就用 Spring Boot 了,那其實這種方式仍是有必要了解一下的。
以後經過經過一個控制檯程序來測試一下,調用 Logger 類的 log 方法。
public class IocTest {
public static void main(String[] args){ ApplicationContext ac = new ClassPathXmlApplicationContext("application.xml"); Logger logger = (Logger) ac.getBean("logger"); logger.log("hello log"); } } 複製代碼
ApplicationContext
是實現容器的接口類, 其中 ClassPathXmlApplicationContext
就是一個 Container 的具體實現,相似的還有 FileSystemXmlApplicationContext
,這兩個是都是解析 xml 格式配置的容器。咱們來看一下 ClassPathXmlApplicationContext
的繼承關係圖。
有沒有看起來很複雜的意思,光是到 ApplicationContext 這一層就通過了好幾層。
這是咱們在控制檯中主動調用 ClassPathXmlApplicationContext
,通常在咱們的項目中是不須要關心 ApplicationContext
的,好比咱們使用的 Spring Boot 的項目,只須要下面幾行就能夠了。
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
複製代碼
可是,這幾行並不表明 Spring Boot 就不作依賴注入了,一樣的,內部也會實現 ApplicationContext
,具體的實現叫作 AnnotationConfigServletWebServerApplicationContext
,下面看一下這個實現類的繼承關係圖,那更是複雜的很,先不用在意細節,瞭解一下就能夠了。
繼續把上面那段基礎款代碼拿過來,咱們的分析就從它開始。
public class IocTest {
public static void main(String[] args){
ApplicationContext ac = new ClassPathXmlApplicationContext("application.xml");
Logger logger = (Logger) ac.getBean("logger");
logger.log("hello log");
}
}
複製代碼
注入過程有好多文章都進行過源碼分析,這裏就不重點介紹源碼了。
簡單介紹一下,咱們若是隻分析 ClassPathXmlApplicationContext
這種簡單的容器的話,其實整個注入過程的源碼很容易讀,不得不說,Spring 的源碼寫的很是整潔。咱們從 ClassPathXmlApplicationContext
的構造函數進去,一步步找到 refresh()
方法,而後順着讀下去就能理解 Spring IoC 最基礎的過程。如下代碼是 refresh 方法的核心方法:
@Override
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// Prepare this context for refreshing.
prepareRefresh();
// Tell the subclass to refresh the internal bean factory.
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
// Prepare the bean factory for use in this context.
prepareBeanFactory(beanFactory);
try {
// Allows post-processing of the bean factory in context subclasses.
postProcessBeanFactory(beanFactory);
// Invoke factory processors registered as beans in the context.
invokeBeanFactoryPostProcessors(beanFactory);
// Register bean processors that intercept bean creation.
registerBeanPostProcessors(beanFactory);
// Initialize message source for this context.
initMessageSource();
// Initialize event multicaster for this context.
initApplicationEventMulticaster();
// Initialize other special beans in specific context subclasses.
onRefresh();
// Check for listener beans and register them.
registerListeners();
// Instantiate all remaining (non-lazy-init) singletons.
finishBeanFactoryInitialization(beanFactory);
// Last step: publish corresponding event.
finishRefresh();
}
catch (BeansException ex) { } destroyBeans(); cancelRefresh(ex); throw ex; } finally { resetCommonCaches(); } } } 複製代碼
註釋都寫的很是清楚,其中核心注入過程其實就在這一行:
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
複製代碼
我把這個核心部分的邏輯調用畫了一個泳道圖,這個圖只列了核心方法,可是已經可以清楚的表示這個過程了。(獲取矢量格式的能夠在公衆號回覆「矢量圖」獲取)
題外話:關於源碼閱讀 大部分人都不太能讀進去源碼,包括我本身,別說這種特別龐大的開源框架,就算是本身新接手的項目也看不進去多少。讀源碼最關鍵的就是細節,這兒說的細節不是讓你摳細節,偏偏相反,千萬不能太摳細節了,誰也不能把一個框架的全部源碼一行不落的全摸透,找關鍵的邏輯關係就能夠了,否則的話,頗有可能你就被一個細節搞到頭疼、懊惱,而後就放棄閱讀了。
有的同窗一看圖或者源碼會發現,怎麼涉及到這麼多的類啊,這調用鏈可真夠長的。不要緊,你就把它們當作一個總體就能夠了(理解成發生在一個類中的調用),經過前面的類關係圖就看出來了,繼承關係很複雜,各類繼承、實現,因此到最後調用鏈變得很繁雜。
那麼簡單來歸納一下注入的核心其實就是解析 xml 文件的內容,找到 元素,而後通過一系列加工,最後把這些加工後的對象存到一個公共空間,供調用者獲取使用。
而至於使用註解方式的 bean,好比使用 @Bean
、@Service
、@Component
等註解的,只是解析這一步不同而已,剩下的操做基本都一致。
因此說,咱們只要把這裏面的幾個核心問題搞清楚就能夠了。
上面的那行核心代碼,最後返回的是一個 ConfigurableListableBeanFactory
對象,並且後面多個方法都用這個返回的 beanFactory 作爲參數。
BeanFactory
是一個接口,ApplicationContext
也是一個接口,並且,BeanFactory
是 ApplicationContext
的父接口,有說 BeanFactory
纔是 Spring IoC 的容器。其實早期的時候只有 BeanFactory
,那時候它確實是 Spring IoC 容器,後來因爲版本升級擴展更多功能,因此加入了 ApplicationContext
。它們倆最大的區別在於,ApplicationContext
初始化時就實例化全部 Bean,而BeanFactory
用到時再實例化所用 Bean,因此早期版本的 Spring 默認是採用懶加載的方式,而新版本默認是在初始化時就實例化全部 Bean,因此 Spring 的啓動過程不是那麼快,這是其中的一個緣由。
上面歸納裏提到保存到一個公共空間,那這個公共空間在哪兒呢?實際上是一個 Map,並且是一個 ConcurrentHashMap ,爲了保證併發安全。它的聲明以下,在 DefaultListableBeanFactory
中。
private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>(256)
複製代碼
其中 beanName 做爲 key,也就是例子中的 logger,value 是 BeanDefinition 類型,BeanDefinition 用來描述一個 Bean 的定義,咱們在 xml 文件中定義的 元素的屬性都在其中,還包括其餘的一些必要屬性。
向 beanDefinitionMap 中添加元素,叫作 Bean 的註冊,只有被註冊過的 Bean 才能被使用。
另外,還有一個 Map 叫作 singletonObjects,其聲明以下:
/** Cache of singleton objects: bean name to bean instance. */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
複製代碼
在 refresh() 過程當中,還會將 Bean 存到這裏一份,這個存儲過程發生在 finishBeanFactoryInitialization(beanFactory) 方法內,它的做用是將非 lazy-init 的 Bean 放到singletonObjects 中。
除了存咱們定義的 Bean,還包括幾個系統 Bean。
例如咱們在代碼中這樣調用:
ApplicationContext ac = new ClassPathXmlApplicationContext("application.xml");
StandardEnvironment env = (StandardEnvironment) ac.getBean("environment");
複製代碼
在這個例子中,咱們是經過 ApplicationContext
的 getBean() 方法顯示的獲取已註冊的 Bean。前面說了咱們定義的 Bean 除了放到 beanDefinitionMap,還在 singletonObjects 中存了一份,singletonObjects 中的就是一個緩存,當咱們調用 getBean 方法的時候,會先到其中去獲取。若是沒找到(對於那些主動設置 lazy-init 的 Bean 來講),再去 beanDefinitionMap 獲取,而且加入到 singletonObjects 中。
獲取 Bean 的調用流程圖以下(公衆號回覆「矢量圖」獲取高清矢量圖)
如下是 lazy-init 方式設置的 Bean 的例子。
<bean id="lazyBean" lazy-init="true" class="org.kite.spring.bean.lazyBean" />
複製代碼
若是不設置的話,默認都是在初始化的時候註冊。
如今已經不多項目用 xml 這種配置方式了,基本上都是 Spring Boot,就算不用,也是在 Spring MVC 中用註解的方式註冊、使用 Bean 了。其實整個過程都是相似的,只不過註冊和獲取的時候多了註解的參與。Srping 中 BeanFactory
和ApplicationContext
都是接口,除此以外,還有不少的抽象類,使得咱們能夠靈活的定製屬於本身的註冊和調用流程,能夠認爲註解方式就是其中的一種定製。只要找到時機解析好對應的註解標示就能夠了。
可是看 Spring Boot 的註冊和調用過程沒有 xml 方式的順暢,這都是由於註解的特性決定的。註解用起來簡單、方便,好處多多。但同時,註解會割裂傳統的流程,傳統流程都是一步一步主動調用,只要順着代碼往下看就能夠了,而註解的方式會形成這個過程連不起來,因此讀起來須要額外的一些方法。
Spring Boot 中的 IoC 過程,咱們下次有機會再說。
獲取本文高清泳道圖請在公衆號內回覆「矢量圖」
感受還好給個贊吧,老是被白嫖,身體吃不消!
參考文檔:
https://docs.spring.io/spring/docs
我是風箏,公衆號「古時的風箏」,一個在程序圈混跡多年,主業 Java,另外 Python、React 也玩兒的很 6 的斜槓開發者。能夠在公衆號中加我好友,進羣裏小夥伴交流學習,好多大廠的同窗也在羣內呦。
技術交流還能夠加羣或者直接加我微信。