Spring 部分配置特性

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,有兩種方法:

  1. 讓 bean A 通過實現 ApplicationContextAware 接口來感知 applicationContext,從而能在運行時通過 applicationContext.getBean(String beanName) 的方法來獲取最新的 bean B。但這時就與 Spring 代碼耦合,違背了控制反轉原則,即 bean 完全由 Spring 容器管理,我們只用使用 bean 就可以了

  2. 通過 <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 的初始化方法有兩種方式:

  1. 實現 InitializingBean 接口,並實現其中的 afterPropertiesSet() 方法,這種方式需要實現接口,從而使得 bean 的代碼與 Spring 耦合在一起

  2. 通過 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 方法。

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