Spring IOC 容器原始碼分析(三)
附錄
id 和 name
每個 Bean 在 Spring 容器中都有一個唯一的名字(beanName)和 0 個或多個別名(aliases)。
我們從 Spring 容器中獲取 Bean 的時候,可以根據 beanName,也可以通過別名。
beanFactory.getBean("beanName or alias");
在配置<bean />
的過程中,我們可以配置 id 和 name,看幾個例子就知道是怎麼回事了。
<bean id="messageService" name="m1, m2, m3" class="com.javadoop.example.MessageServiceImpl">
以上配置的結果就是:beanName 為 messageService,別名有 3 個,分別為 m1、m2、m3。
<bean name="m1, m2, m3" class="com.javadoop.example.MessageServiceImpl" />
以上配置的結果就是:beanName 為 m1,別名有 2 個,分別為 m2、m3。
<bean class="com.javadoop.example.MessageServiceImpl">
beanName 為:com.javadoop.example.MessageServiceImpl#0,
別名 1 個,為: com.javadoop.example.MessageServiceImpl
<bean id="messageService" class="com.javadoop.example.MessageServiceImpl">
以上配置的結果就是:beanName 為 messageService,沒有別名。
配置是否允許 Bean 覆蓋、是否允許迴圈依賴
我們說過,預設情況下,allowBeanDefinitionOverriding 屬性為 null。如果在同一配置檔案中 Bean id 或 name 重複了,會拋錯,但是如果不是同一配置檔案中,會發生覆蓋。
可是有些時候我們希望在系統啟動的過程中就嚴格杜絕發生 Bean 覆蓋,因為萬一出現這種情況,會增加我們排查問題的成本。
迴圈依賴說的是 A 依賴 B,而 B 又依賴 A。或者是 A 依賴 B,B 依賴 C,而 C 卻依賴 A。預設 allowCircularReferences 也是 null。
它們兩個屬性是一起出現的,必然可以在同一個地方一起進行配置。
新增這兩個屬性的作者 Juergen Hoeller 在這個jira 的討論中說明了怎麼配置這兩個屬性。
public class NoBeanOverridingContextLoader extends ContextLoader { @Override protected void customizeContext(ServletContext servletContext, ConfigurableWebApplicationContext applicationContext) { super.customizeContext(servletContext, applicationContext); AbstractRefreshableApplicationContext arac = (AbstractRefreshableApplicationContext) applicationContext; arac.setAllowBeanDefinitionOverriding(false); } }
public class MyContextLoaderListener extends org.springframework.web.context.ContextLoaderListener { @Override protected ContextLoader createContextLoader() { return new NoBeanOverridingContextLoader(); } }
<listener> <listener-class>com.javadoop.MyContextLoaderListener</listener-class> </listener>
如果以上方式不能滿足你的需求,請參考這個連結:解決spring中不同配置檔案中存在name或者id相同的bean可能引起的問題
profile
我們可以把不同環境的配置分別配置到單獨的檔案中,舉個例子:
<beans profile="development" xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jdbc="http://www.springframework.org/schema/jdbc" xsi:schemaLocation="..."> <jdbc:embedded-database id="dataSource"> <jdbc:script location="classpath:com/bank/config/sql/schema.sql"/> <jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/> </jdbc:embedded-database> </beans>
<beans profile="production" xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jee="http://www.springframework.org/schema/jee" xsi:schemaLocation="..."> <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/> </beans>
應該不必做過多解釋了吧,看每個檔案第一行的 profile=""。
當然,我們也可以在一個配置檔案中使用:
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jdbc="http://www.springframework.org/schema/jdbc" xmlns:jee="http://www.springframework.org/schema/jee" xsi:schemaLocation="..."> <beans profile="development"> <jdbc:embedded-database id="dataSource"> <jdbc:script location="classpath:com/bank/config/sql/schema.sql"/> <jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/> </jdbc:embedded-database> </beans> <beans profile="production"> <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/> </beans> </beans>
理解起來也很簡單吧。
接下來的問題是,怎麼使用特定的 profile 呢?Spring 在啟動的過程中,會去尋找 “spring.profiles.active” 的屬性值,根據這個屬性值來的。那怎麼配置這個值呢?
Spring 會在這幾個地方尋找 spring.profiles.active 的屬性值:作業系統環境變數、JVM 系統變數、web.xml 中定義的引數、JNDI。
最簡單的方式莫過於在程式啟動的時候指定:
-Dspring.profiles.active="profile1,profile2"
profile 可以啟用多個
當然,我們也可以通過程式碼的形式從 Environment 中設定 profile:
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.getEnvironment().setActiveProfiles("development"); ctx.register(SomeConfig.class, StandaloneDataConfig.class, JndiDataConfig.class); ctx.refresh(); // 重啟
如果是 Spring Boot 的話更簡單,我們一般會建立 application.properties、application-dev.properties、application-prod.properties 等檔案,其中 application.properties 配置各個環境通用的配置,application-{profile}.properties 中配置特定環境的配置,然後在啟動的時候指定 profile:
java -Dspring.profiles.active=prod -jar JavaDoop.jar
如果是單元測試中使用的話,在測試類中使用 @ActiveProfiles 指定,這裡就不展開了。
工廠模式生成 Bean
請讀者注意 factory-bean 和 FactoryBean 的區別。這節說的是前者,是說靜態工廠或例項工廠,而後者是 Spring 中的特殊介面,代表一類特殊的 Bean,附錄的下面一節會介紹 FactoryBean。
設計模式裡,工廠方法模式分靜態工廠和例項工廠,我們分別看看 Spring 中怎麼配置這兩個,來個程式碼示例就什麼都清楚了。
靜態工廠:
<bean id="clientService" class="examples.ClientService" factory-method="createInstance"/>
public class ClientService { private static ClientService clientService = new ClientService(); private ClientService() {} // 靜態方法 public static ClientService createInstance() { return clientService; } }
例項工廠:
<bean id="serviceLocator" class="examples.DefaultServiceLocator"> <!-- inject any dependencies required by this locator bean --> </bean> <bean id="clientService" factory-bean="serviceLocator" factory-method="createClientServiceInstance"/> <bean id="accountService" factory-bean="serviceLocator" factory-method="createAccountServiceInstance"/>
public class DefaultServiceLocator { private static ClientService clientService = new ClientServiceImpl(); private static AccountService accountService = new AccountServiceImpl(); public ClientService createClientServiceInstance() { return clientService; } public AccountService createAccountServiceInstance() { return accountService; } }
FactoryBean
FactoryBean 適用於 Bean 的建立過程比較複雜的場景,比如資料庫連線池的建立。
public interface FactoryBean<T> { T getObject() throws Exception; Class<T> getObjectType(); boolean isSingleton(); }
public class Person { private Car car ; private void setCar(Car car){ this.car = car;} }
我們假設現在需要建立一個 Person 的 Bean,首先我們需要一個 Car 的例項,我們這裡假設 Car 的例項建立很麻煩,那麼我們可以把建立 Car 的複雜過程包裝起來:
public class MyCarFactoryBean implements FactoryBean<Car>{ private String make; private int year ; public void setMake(String m){ this.make =m ; } public void setYear(int y){ this.year = y; } public Car getObject(){ // 這裡我們假設 Car 的例項化過程非常複雜,反正就不是幾行程式碼可以寫完的那種 CarBuilder cb = CarBuilder.car(); if(year!=0) cb.setYear(this.year); if(StringUtils.hasText(this.make)) cb.setMake( this.make ); return cb.factory(); } public Class<Car> getObjectType() { return Car.class ; } public boolean isSingleton() { return false; } }
我們看看裝配的時候是怎麼配置的:
<bean class = "com.javadoop.MyCarFactoryBean" id = "car"> <property name = "make" value ="Honda"/> <property name = "year" value ="1984"/> </bean> <bean class = "com.javadoop.Person" id = "josh"> <property name = "car" ref = "car"/> </bean>
看到不一樣了嗎?id 為 “car” 的 bean 其實指定的是一個 FactoryBean,不過配置的時候,我們直接讓配置 Person 的 Bean 直接依賴於這個 FactoryBean 就可以了。中間的過程 Spring 已經封裝好了。
說到這裡,我們再來點乾貨。我們知道,現在還用 xml 配置 Bean 依賴的越來越少了,更多時候,我們可能會採用 javaconfig 的方式來配置,這裡有什麼不一樣呢?
@Configuration public class CarConfiguration { @Bean public MyCarFactoryBean carFactoryBean(){ MyCarFactoryBean cfb = new MyCarFactoryBean(); cfb.setMake("Honda"); cfb.setYear(1984); return cfb; } @Bean public Person aPerson(){ Person person = new Person(); // 注意這裡的不同 person.setCar(carFactoryBean().getObject()); return person; } }
這個時候,其實我們的思路也很簡單,把 MyCarFactoryBean 看成是一個簡單的 Bean 就可以了,不必理會什麼 FactoryBean,它是不是 FactoryBean 和我們沒關係。
初始化 Bean 的回撥
有以下四種方案:
<bean id="exampleInitBean" class="examples.ExampleBean" init-method="init"/>
public class AnotherExampleBean implements InitializingBean { public void afterPropertiesSet() { // do some initialization work } }
@Bean(initMethod = "init") public Foo foo() { return new Foo(); }
@PostConstruct public void init() { }
銷燬 Bean 的回撥
<bean id="exampleInitBean" class="examples.ExampleBean" destroy-method="cleanup"/>
public class AnotherExampleBean implements DisposableBean { public void destroy() { // do some destruction work (like releasing pooled connections) } }
@Bean(destroyMethod = "cleanup") public Bar bar() { return new Bar(); }
@PreDestroy public void cleanup() { }
ConversionService
既然文中說到了這個,順便提一下好了。
最有用的場景就是,它用來將前端傳過來的引數和後端的 controller 方法上的引數進行繫結的時候用。
像前端傳過來的字串、整數要轉換為後端的 String、Integer 很容易,但是如果 controller 方法需要的是一個列舉值,或者是 Date 這些非基礎型別(含基礎型別包裝類)值的時候,我們就可以考慮採用 ConversionService 來進行轉換。
<bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean"> <property name="converters"> <list> <bean class="com.javadoop.learning.utils.StringToEnumConverterFactory"/> </list> </property> </bean>
ConversionService 介面很簡單,所以要自定義一個 convert 的話也很簡單。
下面再說一個實現這種轉換很簡單的方式,那就是實現 Converter 介面。
來看一個很簡單的例子,這樣比什麼都管用。
public class StringToDateConverter implements Converter<String, Date> { @Override public Date convert(String source) { try { return DateUtils.parseDate(source, "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "HH:mm:ss", "HH:mm"); } catch (ParseException e) { return null; } } }
只要註冊這個 Bean 就可以了。這樣,前端往後端傳的時間描述字串就很容易繫結成 Date 型別了,不需要其他任何操作。
Bean 繼承
在初始化 Bean 的地方,我們說過了這個:
RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);
這裡涉及到的就是<bean parent="" />
中的 parent 屬性,我們來看看 Spring 中是用這個來幹什麼的。
首先,我們要明白,這裡的繼承和 java 語法中的繼承沒有任何關係,不過思路是相通的。child bean 會繼承 parent bean 的所有配置,也可以覆蓋一些配置,當然也可以新增額外的配置。
Spring 中提供了繼承自 AbstractBeanDefinition 的ChildBeanDefinition
來表示 child bean。
看如下一個例子:
<bean id="inheritedTestBean" abstract="true" class="org.springframework.beans.TestBean"> <property name="name" value="parent"/> <property name="age" value="1"/> </bean> <bean id="inheritsWithDifferentClass" class="org.springframework.beans.DerivedTestBean" parent="inheritedTestBean" init-method="initialize"> <property name="name" value="override"/> </bean>
parent bean 設定了abstract="true"
所以它不會被例項化,child bean 繼承了 parent bean 的兩個屬性,但是對 name 屬性進行了覆寫。
child bean 會繼承 scope、構造器引數值、屬性值、init-method、destroy-method 等等。
當然,我不是說 parent bean 中的 abstract = true 在這裡是必須的,只是說如果加上了以後 Spring 在例項化 singleton beans 的時候會忽略這個 bean。
比如下面這個極端 parent bean,它沒有指定 class,所以毫無疑問,這個 bean 的作用就是用來充當模板用的 parent bean,此處就必須加上 abstract = true。
<bean id="inheritedTestBeanWithoutClass" abstract="true"> <property name="name" value="parent"/> <property name="age" value="1"/> </bean>
方法注入
一般來說,我們的應用中大多數的 Bean 都是 singleton 的。singleton 依賴 singleton,或者 prototype 依賴 prototype 都很好解決,直接設定屬性依賴就可以了。
但是,如果是 singleton 依賴 prototype 呢?這個時候不能用屬性依賴,因為如果用屬性依賴的話,我們每次其實拿到的還是第一次初始化時候的 bean。
一種解決方案就是不要用屬性依賴,每次獲取依賴的 bean 的時候從 BeanFactory 中取。這個也是大家最常用的方式了吧。怎麼取,我就不介紹了,大部分 Spring 專案大家都會定義那麼個工具類的。
另一種解決方案就是這裡要介紹的通過使用 Lookup method。
lookup-method
我們來看一下 Spring Reference 中提供的一個例子:
package fiona.apple; // no more Spring imports! public abstract class CommandManager { public Object process(Object commandState) { // grab a new instance of the appropriate Command interface Command command = createCommand(); // set the state on the (hopefully brand new) Command instance command.setState(commandState); return command.execute(); } // okay... but where is the implementation of this method? protected abstract Command createCommand(); }
xml 配置<lookup-method />
:
<!-- a stateful bean deployed as a prototype (non-singleton) --> <bean id="myCommand" class="fiona.apple.AsyncCommand" scope="prototype"> <!-- inject dependencies here as required --> </bean> <!-- commandProcessor uses statefulCommandHelper --> <bean id="commandManager" class="fiona.apple.CommandManager"> <lookup-method name="createCommand" bean="myCommand"/> </bean>
Spring 採用CGLIB 生成位元組碼 的方式來生成一個子類。我們定義的類不能定義為 final class,抽象方法上也不能加 final。
lookup-method 上的配置也可以採用註解來完成,這樣就可以不用配置<lookup-method />
了,其他不變:
public abstract class CommandManager { public Object process(Object commandState) { MyCommand command = createCommand(); command.setState(commandState); return command.execute(); } @Lookup("myCommand") protected abstract Command createCommand(); }
注意,既然用了註解,要配置註解掃描:<context:component-scan base-package="com.javadoop" />
甚至,我們可以像下面這樣:
public abstract class CommandManager { public Object process(Object commandState) { MyCommand command = createCommand(); command.setState(commandState); return command.execute(); } @Lookup protected abstract MyCommand createCommand(); }
上面的返回值用了 MyCommand,當然,如果 Command 只有一個實現類,那返回值也可以寫 Command。
replaced-method
記住它的功能,就是替換掉 bean 中的一些方法。
public class MyValueCalculator { public String computeValue(String input) { // some real code... } // some other methods... }
方法覆寫,注意要實現 MethodReplacer 介面:
public class ReplacementComputeValue implements org.springframework.beans.factory.support.MethodReplacer { public Object reimplement(Object o, Method m, Object[] args) throws Throwable { // get the input value, work with it, and return a computed result String input = (String) args[0]; ... return ...; } }
配置也很簡單:
<bean id="myValueCalculator" class="x.y.z.MyValueCalculator"> <!-- 定義 computeValue 這個方法要被替換掉 --> <replaced-method name="computeValue" replacer="replacementComputeValue"> <arg-type>String</arg-type> </replaced-method> </bean> <bean id="replacementComputeValue" class="a.b.c.ReplacementComputeValue"/>
arg-type 明顯不是必須的,除非存在方法過載,這樣必須通過引數型別列表來判斷這裡要覆蓋哪個方法。
BeanPostProcessor
應該說 BeanPostProcessor 概念在 Spring 中也是比較重要的。我們看下介面定義:
public interface BeanPostProcessor { Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException; Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException; }
看這個介面中的兩個方法名字我們大體上可以猜測 bean 在初始化之前會執行 postProcessBeforeInitialization 這個方法,初始化完成之後會執行 postProcessAfterInitialization 這個方法。但是,這麼理解是非常片面的。
首先,我們要明白,除了我們自己定義的 BeanPostProcessor 實現外,Spring 容器在啟動時自動給我們也加了幾個。如在獲取 BeanFactory 的 obtainFactory() 方法結束後的 prepareBeanFactory(factory),大家仔細看會發現,Spring 往容器中添加了這兩個 BeanPostProcessor:ApplicationContextAwareProcessor、ApplicationListenerDetector。
我們回到這個介面本身,讀者請看第一個方法,這個方法接受的第一個引數是 bean 例項,第二個引數是 bean 的名字,重點在返回值將會作為新的 bean 例項,所以,沒事的話這裡不能隨便返回個 null。
那意味著什麼呢?我們很容易想到的就是,我們這裡可以對一些我們想要修飾的 bean 例項做一些事情。但是對於 Spring 框架來說,它會決定是不是要在這個方法中返回 bean 例項的代理,這樣就有更大的想象空間了。
最後,我們說說如果我們自己定義一個 bean 實現 BeanPostProcessor 的話,它的執行時機是什麼時候?
如果仔細看了程式碼分析的話,其實很容易知道了,在 bean 例項化完成、屬性注入完成之後,會執行回撥方法,具體請參見類 AbstractAutowireCapableBeanFactory#initBean 方法。
首先會回撥幾個實現了 Aware 介面的 bean,然後就開始回撥 BeanPostProcessor 的 postProcessBeforeInitialization 方法,之後是回撥 init-method,然後再回調 BeanPostProcessor 的 postProcessAfterInitialization 方法。
總結
按理說,總結應該寫在附錄前面,我就不講究了。
在花了那麼多時間後,這篇文章終於算是基本寫完了,大家在驚歎 Spring 給我們做了那麼多的事的時候,應該透過現象看本質,去理解 Spring 寫得好的地方,去理解它的設計思想。
本文的缺陷在於對 Spring 預初始化 singleton beans 的過程分析不夠,主要是程式碼量真的比較大,分支旁路眾多。同時,雖然附錄條目不少,但是龐大的 Spring 真的引出了很多的概念,希望日後有精力可以慢慢補充一些。