Spring 部分配置特性
id 和 name
每個 Bean 在 Spring 容器中都有一個唯一的名字(beanName)和 0 個或多個別名(aliases)。
我們從 Spring 容器中獲取 Bean 的時候,可以根據 beanName,也可以通過別名。
beanFactory.getBean("beanName or alias");
在配置 的過程中,我們可以配置 id 和 name,看幾個例子就知道是怎麼回事了。
<bean id="admin" name="m1, m2, m3" class="com.test.Admin" />
以上配置的結果是:beanName 爲 admin,別名有 3 個,分別爲 m1、m2、m3。
<bean name="m1, m2, m3" class="com.test.Admin" />
以上配置的結果是:beanName 爲 m1,別名有 2 個,分別爲 m2、m3。
<bean class="com.test.Admin" />
以上配置的結果是:beanName 爲 com.test.Admin#0,別名爲: com.test.Admin
<bean id="admin" class="com.test.Admin" />
以上配置的結果是:beanName 爲 admin,沒有別名。
配置是否允許 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 結尾,表示它是一類 Bean,不同於普通 Bean 的是:它是實現了 FactoryBean 接口的 Bean,當在 IOC 容器中的 Bean 實現了 FactoryBean 接口後,通過 getBean(String BeanName) 方法從 BeanFactory 中獲取到的 Bean 對象並不是 FactoryBean 的實現類對象,實際上是通過 FactoryBean 的 getObject() 返回的對象。如果要獲取 FactoryBean 對象,需要在 beanName 前面加一個 & 符號來獲取。
<bean id="school" class="com.test.School">
<property name="schoolName" value="hit"/>
<property name="address" value="harbin"/>
</bean>
<bean id="factoryBean" class="com.test.FactoryBeanTest">
<property name="type" value="school"/>
</bean>
下面通過一個類實現 FactoryBean 接口,在配置文件中將該類的 type 屬性設置爲 student,會在 getObject() 方法中返回 Student 對象。
public class FactoryBeanTest implements FactoryBean {
private String type;
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public Object getObject() throws Exception {
if ("student".equals(type)) {
return new Student();
} else {
return new School();
}
}
public Class<?> getObjectType() {
return School.class;
}
public boolean isSingleton() {
return true;
}
}
通過測試可以驗證之前的想法。
School school = (School) applicationContext.getBean("factoryBean");
FactoryBeanTest factoryBean = (FactoryBeanTest) applicationContext.getBean("&factoryBean");
System.out.println(school.getClass().getName());
System.out.println(factoryBean.getClass().getName());
測試結果:
com.test.School
com.test.FactoryBeanTest
所以從 IOC 容器獲取實現了 FactoryBean 的實現類時,返回的是實現類中的 getObject 方法返回的對象,要想獲取 FactoryBean 的實現類,得在 getBean 中的 BeanName 前加上 & ,即 getBean(String &BeanName)。
BeanWrapper
BeanWrapper 接口,作爲 spring 內部的一個核心接口,正如其名,它是 bean 的包裹類,即在內部中將會保存該 bean 的實例,提供其它一些擴展功能。同時 BeanWrapper 接口還繼承了 PropertyAccessor、propertyEditorRegistry、TypeConverter、ConfigurablePropertyAccessor 接口,所以它還提供了訪問 bean 的屬性值、屬性編輯器註冊和類型轉換等功能。
School school = new School();
BeanWrapper beanWrapper = PropertyAccessorFactory.forBeanPropertyAccess(school);
PropertyValue schoolName = new PropertyValue("schoolName", "HIT");
PropertyValue address = new PropertyValue("address", "Harbin");
beanWrapper.setPropertyValue(schoolName);
beanWrapper.setPropertyValue(address);
System.out.println(beanWrapper.getWrappedInstance());
上面的代碼已經很清楚地演示瞭如何使用 BeanWrapper 設置和獲取 bean 的屬性。
初始化 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() {
}
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>
lookup-method
如果一個單例模式的 bean A 需要引用另一個非單例模式的 bean B,有兩種方法:
-
讓 bean A 通過實現 ApplicationContextAware 接口來感知 applicationContext,從而能在運行時通過 applicationContext.getBean(String beanName) 的方法來獲取最新的 bean B。但這時就與 Spring 代碼耦合,違背了控制反轉原則,即 bean 完全由 Spring 容器管理,我們只用使用 bean 就可以了
-
通過
<lookup-method />
標籤實現
看以下一個場景,NewsProvider 是一個單例類,News 是非單例類,NewsProvider 每次提供最新的 news,現在分別通過以上兩種方法來實現該需求。
通過實現 ApplicationContextAware 接口:
public class NewsProvider implements ApplicationContextAware {
private News news;
private ApplicationContext applicationContext;
public News getNews() {
return applicationContext.getBean("news", News.class);
}
public void setNews(News news) {
this.news = news;
}
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
讓NewsProvider類實現ApplicationContextAware接口(實現 BeanFactoryAware 接口也可以),每次調用 NewsProvider 的 getNews 方法時,都會從 ApplicationContext 中獲取一個新的 News 實例。
對應的配置文件爲:
<bean id="news" class="com.test.News" scope="prototype"/>
<bean id="newsProvider" class="com.test.NewsProvider">
<property name="news" ref="news"/>
</bean>
通過 <lookup-method />
標籤實現:
class LookupProvider {
private News news;
public News getNews() {
return news;
}
public void setNews(News news) {
this.news = news;
}
}
此時無需實現任何接口,只用在配置文件中進行如下設置即可:
<bean id="lookupProvider" class="com.test.LookupProvider">
<lookup-method name="getNews" bean="news"/>
</bean>
顯然我們沒有用到 Spring 的任何類和接口 ,實現了與 Spring 代碼的耦合。
其中最爲核心的部分就是 lookup-method 的配置,Spring 應用了 CGLIB 動態代理類庫,在初始化容器時對 中的 bean 做了特殊處理,Spring 會對 bean 指定的 class 做動態代理, 通過 name 中指定的方法,返回 bean 的實例對象。
replaced-method
主要作用是替換方法體及其返回值。需要改變的方法,實現 MethodReplacer 接口並重寫 reimplement 方法來動態地改變方法。內部實現爲 CGLIB 方法,重新生成子類,重寫配置方法和返回對象,達到動態改變的效果。
直接看例子,bean 配置文件:
<bean id="admin" class="com.test.Admin">
<property name="name" value="hzc"/>
<property name="age" value="23"/>
<replaced-method name="introduce" replacer="replacedAdmin"/>
</bean>
<bean id="replacedAdmin" class="com.test.ReplacedAdmin"/>
Admin 代碼:
class Admin {
private String name;
private int age;
public Admin() {
}
public Admin(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public void introduce() {
System.out.println("hello, my name is " + name +
", and I'am " + age + " years old");
}
}
ReplacedAdmin 代碼:
class ReplacedAdmin implements MethodReplacer {
public Object reimplement(Object o, Method method, Object[] objects) throws Throwable {
System.out.println("已經被替換!");
return null;
}
}
測試代碼:
public void test() throws Exception {
String location = "bean.xml";
ApplicationContext applicationContext = new ClassPathXmlApplicationContext(location);
Admin admin = (Admin) applicationContext.getBean("admin");
admin.introduce(); // 結果爲 “已經被替換”,成功地替換了原來 introduce() 的內容
}
init-method
Spring 實現 bean 的初始化方法有兩種方式:
-
實現 InitializingBean 接口,並實現其中的 afterPropertiesSet() 方法,這種方式需要實現接口,從而使得 bean 的代碼與 Spring 耦合在一起
-
通過 Spring 提供的 init-method 功能來執行一個 bean 的自定義初始化方法
對於實現 InitializingBean 接口的這一種方法來說,在 xml 配置文件中不需要對 bean 進行特殊的設置,Spring 在配置文件中完成該 bean 的全部賦值後,會檢查該 bean 是否實現了 InitializingBean 接口,如果實現了就直接調用 bean 的 afterPropertiesSet 方法
對於第二種方式,bean 的類不需要實現任何接口,只需在 <bean />
中加入 init-method=“xxx” 即可。其中 xxx 必須是一個無參方法,否則會拋出異常,該方法將會在 bean 初始化完成後被調用。
InitializingBean 和 init-method 可以一起使用,Spring 會先處理 InitializingBean 再處理 init-method。init-method 是通過反射執行的,而 afterPropertiesSet 是直接執行的,所以 afterPropertiesSet 的執行效率比 init-method 高,不過 init-method 消除了 bean 對 Spring 依賴,推薦使用 init-method。
如果一個 bean 被定義爲非單例的,那麼 afterPropertiesSet 和 init-method 在 bean 的每一個實例被創建時都會執行。單例 bean 的 afterPropertiesSet 和 init-method 只在 bean 第一次實例時執行。一般情況下 afterPropertiesSet 和 init-method 都應用在單例 bean 上。
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 方法。