SpringIOC二—— 容器 和 Bean的深入理解

上文:Spring IOC 一——容器裝配Bean的簡單使用

上篇文章介紹了 Spring IOC 中最重要的兩個概念——容器和Bean,以及如何使用 Spring 容器裝配Bean。本文接着記錄 Spring 中 IOC 的相關知識。

部分參考資料:
《Spring實戰(第4版)》
《輕量級 JavaEE 企業應用實戰(第四版)》
Spring 官方文檔
W3CSchool Spring教程
易百教程 Spring教程

一、Spring 容器中的 Bean 的常用屬性

Bean的作用域

目前,scope的取值有5種取值:
在Spring 2.0之前,有singleton和prototype兩種;
在Spring 2.0之後,爲支持web應用的ApplicationContext,增強另外三種:request,session和global session類型,它們只適用於web程序,通常是和XmlWebApplicationContext共同使用。

  • singleton: 單例模式,在整個 Spring IOC 容器中只會創建一個實例。默認即爲單例模式。
  • prototype:原型模式,每次通過 getBean 方法獲取實例時,都會創建一個新的實例。
  • request:在同一次Http請求內,只會生成一個實例,只在 Web 應用中使用 Spring 纔有效。
  • session:在同義詞 Http 會話內,只會生成一個實例,只在 Web 應用中使用 Spring 纔有效。
  • global session:只有應用在基於porlet的web應用程序中才有意義,它映射到porlet的global範圍的session,如果普通的servlet的web 應用中使用了這個scope,容器會把它作爲普通的session的scope對待。

配置方式:

(1) XML 文件配置:

<bean id="helloSpring" class="com.sharpcj.hello.HelloSpring" scope="ConfigurableBeanFactory.SCOPE_SINGLETON"> <!-- singleton -->
    <property name="name" value="Spring"/>
</bean>

(2) 註解配置:

@Component
@Scope("singleton")
public class HelloSpring {

}

Bean 的延遲加載

默認情況下,當容器啓動之後,會將所有作用域爲單例的bean創建好,如配置 lazy-init值爲true,表示延遲加載,即容器啓動之後,不會立即創建該實例。

(1) XML文件配置:

<bean id="mb1" class="com.sharpcj.hello.HelloSpring" lazy-init="true"></bean>

(2) 註解配置:

@Component
@Lazy
@Scope("singleton")
public class HelloSpring {

}

Bean 初始化和銷燬前後回調方法

Bean 初始化回調和銷燬回調
HelloSpring.java

package com.sharpcj.cycle;

import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

@Component
public class HelloSpring implements InitializingBean, DisposableBean{
    public HelloSpring(){
        System.out.println("構造方法");
    }

    public void xmlInit(){
        System.out.println("xml Init");
    }

    public void xmlDestory(){
        System.out.println("xml Destory");
    }

    @PostConstruct
    public void init(){
        System.out.println("annotation Init");
    }

    @PreDestroy
    public void destory(){
        System.out.println("annotation Destory");
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("interface afterPropertiesSet");
    }

    @Override
    public void destroy() throws Exception {
        System.out.println("interface destroy");
    }
}

(1) XML文件配置:

<bean id="hello" class="com.sharpcj.cycle.HelloSpring" init-method="xmlInit" destroy-method="xmlDestory"/>

(2) 註解配置:

@PostConstruct
public void init(){
    System.out.println("annotation Init");
}

@PreDestroy
public void destory(){
    System.out.println("annotation Destory");
}

另外 Bean 可以實現 org.springframework.beans.factory.InitializingBeanorg.springframework.beans.factory.DisposableBean 兩個接口。
執行結果:

構造方法
interface afterPropertiesSet
xml Init
interface destroy
xml Destory

或者

構造方法
annotation Init
interface afterPropertiesSet
annotation Destory
interface destroy

二、工廠模式創建 Bean

創建 Bean 有三種方式:通過調用構造方法創建 Bean, 調用實例工廠方法創建Bean,調用靜態工廠方法創建 Bean。

調用構造器創建 Bean

這是最常見的情況, 當我們通過配置文件,或者註解的方式配置 Bean, Spring 會通過調用 Bean 類的構造方法,來創建 Bean 的實例。通過 xml 文件配置,明確指定 Bean 的 class 屬性,或者通過註解配置,Spring 容器知道 Bean 的完整類名,然後通過反射調用該類的構造方法即可。

調用實例工廠方法創建 Bean

直接上代碼:
Ipet.java

package com.sharpcj.factorytest;

public interface IPet {
    void move();
}

Dog.java

package com.sharpcj.factorytest;

public class Dog implements IPet {
    @Override
    public void move() {
        System.out.println("Dog can run!");
    }
}

Parrot.java

package com.sharpcj.factorytest;

public class Parrot implements IPet {
    @Override
    public void move() {
        System.out.println("Parrot can fly!");
    }
}

工廠類, PetFactory.java

package com.sharpcj.factorytest;

public class PetFactory {
    public IPet getPet(String type){
        if ("dog".equals(type)) {
            return new Dog();
        } else if ("parrot".equals(type)){
            return new Parrot();
        } else {
            throw new IllegalArgumentException("pet type is illegal!");
        }
    }
}

resources 文件夾下配置文件, factorybeantest.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

        <bean id="petFactory" class="com.sharpcj.factorytest.PetFactory"></bean>

        <bean id="dog" factory-bean="petFactory" factory-method="getPet">
            <constructor-arg value="dog"></constructor-arg>
        </bean>

        <bean id="parrot" factory-bean="petFactory" factory-method="getPet">
            <constructor-arg value="parrot"></constructor-arg>
        </bean>
</beans>

測試類,AppTest.java

package com.sharpcj.factorytest;

import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionReader;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;

public class AppTest {
    public static void main(String[] args) {
        Resource resource = new ClassPathResource("factorybeantest.xml");
        BeanFactory factory = new DefaultListableBeanFactory();
        BeanDefinitionReader bdr = new XmlBeanDefinitionReader((BeanDefinitionRegistry) factory);
        bdr.loadBeanDefinitions(resource);

        Dog dog = (Dog) factory.getBean("dog");
        Parrot parrot = (Parrot) factory.getBean("parrot");
        dog.move();
        parrot.move();
    }
}

程序結果:

可以看到,程序正確執行了。注意看配置文件中,我們並沒有配置 dog 和 parrot 兩個 Bean 類的 class 屬性,而是配置了他們的 factory-beanfactory-method兩個屬性,這樣,Spring 容器在創建 dog 和 parrot 實例時會先創建 petFactory 的實例,然後再調用其工廠方法,創建對應的 dog 和 parrot 實例。

另外,假設我們在測試類中通過 factory 獲取 Bean 實例時,傳入一個非法的參數,會如何? PetFactory 類工廠方法的代碼,看起來會拋出我們自定義的異常?
比如調用如下代碼:

factory.getBean("cat");

結果是:

結果說明,Spring 本身就處理了參數異常,因爲我們並沒有在配置文件中配置中配置 name 爲 “cat” 的 Bean, 所以,Spring 容器拋出了此異常,程序執行不到工廠方法裏去了。

調用靜態工廠方法創建 Bean

拋開 Spring 不談,相比實例工廠方法,其實我們平時用的更多的可能是靜態工廠方法。 Spring 當然也有靜態工廠方法創建 Bean 的實現。下面我們修改工廠方法爲靜態方法:

package com.sharpcj.staticfactorytest;

import com.sharpcj.factorytest.Dog;
import com.sharpcj.factorytest.IPet;
import com.sharpcj.factorytest.Parrot;

public class PetFactory {
    public static IPet getPet(String type){
        if ("dog".equals(type)) {
            return new Dog();
        } else if ("parrot".equals(type)){
            return new Parrot();
        } else {
            throw new IllegalArgumentException("pet type is illegal!");
        }
    }
}

此時我們也應該修改配置文件,這裏我們重新創建了一個配置文件, staticfactorbeantest.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="dog" class="com.sharpcj.staticfactorytest.PetFactory" factory-method="getPet">
        <constructor-arg value="dog"></constructor-arg>
    </bean>
    <bean id="parrot" class="com.sharpcj.staticfactorytest.PetFactory" factory-method="getPet">
        <constructor-arg value="parrot"></constructor-arg>
    </bean>
</beans>

測試代碼:

package com.sharpcj.staticfactorytest;

import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionReader;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;

public class Apptest {
    public static void main(String[] args) {
        Resource resource = new ClassPathResource("staticfactorybeantest.xml");
        BeanFactory factory = new DefaultListableBeanFactory();
        BeanDefinitionReader bdr = new XmlBeanDefinitionReader((BeanDefinitionRegistry) factory);
        bdr.loadBeanDefinitions(resource);

        Dog dog = (Dog) factory.getBean("dog");
        Parrot parrot = (Parrot) factory.getBean("parrot");
        dog.move();
        parrot.move();
    }
}

測試結果如下:

結果正常,這裏注意配置文件,使用靜態工廠方法是,配置文件中我們並沒有配置 PetFactory, 而在配置
dog 和 parrot 時,我們配置的 class 屬性的值是工廠類的完整類名com.sharpcj.staticfactorytest.PetFactory,同事配置了 factory-method屬性。

調用實例工廠方法和調用靜態工廠方法創建 Bean 的異同

調用實例工廠方法和調用靜態工廠方法創建 Bean 的用法基本相似,區別如下:

  • 配置實例工廠方法創建 Bean,必須將實例工廠配置成 Bean 實例;而配置靜態工廠方法創建 Bean,則無需配置工廠 Bean;
  • 配置實例工廠方法創建 Bean,必須使用 factory-bvean 屬性確定工廠 Bean; 而配置靜態工廠方法創建 Bean,則使用 class 屬性確定靜態工廠類。
    相同之處如下:
  • 都需要使用 factory-method 指定生產 Bean 實例的工廠方法;
  • 工廠方法如果需要參數,都使用 <constructor-arg.../> 元素指定參數值;
  • 普通的設值注入,都使用 <property.../>元素確定參數值。

三、FactoryBean 和 BeanFactory

FactoryBean 和 BeanFactory 是兩個極易混淆的概念,需要理解清楚。下面分別來明說這兩個概念。

FactoryBean

FactoryBean 翻譯過來就是 工廠Bean 。需要說明的是,這裏的 FactoryBean 和上一節提到的工廠方法創建Bean不是一個概念,切莫不要把實例工廠創建 Bean 時,配置的工廠 Bean ,和 FactoryBean 混爲一談。兩者沒有聯繫,上一節說的是標準的工廠模式,Spring 只是通過調用工廠方法來創建 Bean 的實例。
這裏的所說的 工廠 Bean 是一種特殊的 Bean 。它需要實現 FactoryBean 這個接口。

FactoryBean 接口提供了三個方法:

T getObject() throws Exception;
Class<?> getObjectType();
boolean isSingleton() {return true;}

當自定義一個類實現了FactoryBean接口後,將該類部署在 Spring 容器裏,再通過 Spring 容器調用 getBean 方法獲取到的就不是該類的實例了,而是該類實現的 getObject 方法的返回值。這三個方法意義如下:

  • getObject() 方法返回了該工廠Bean 生成的 java 實例。
  • getObjectType() 該方法返回該工廠Bean 生成的 java 實例的類型。
  • isSingleton() 該方法返回該工廠Bean 生成的 java 實例是否爲單例。

下面舉一個例子:
定義一個類 StringFactoryBean.java

package com.sharpcj.factorybeantest;

import org.springframework.beans.factory.FactoryBean;

public class StringFactoryBean implements FactoryBean<Object> {
    private String type;
    private String originStr;

    public void setType(String type) {
        this.type = type;
    }

    public void setOriginStr(String originStr) {
        this.originStr = originStr;
    }

    @Override
    public Object getObject() throws Exception {
        if("builder".equals(type) && originStr != null){
            return new StringBuilder(originStr);
        } else if ("buffer".equals(type) && originStr != null) {
            return new StringBuffer(originStr);
        } else {
            throw new IllegalArgumentException();
        }
    }

    @Override
    public Class<?> getObjectType() {
        if("builder".equals(type)){
            return StringBuilder.class;
        } else if ("buffer".equals(type)) {
            return StringBuffer.class;
        } else {
            throw new IllegalArgumentException();
        }
    }

    @Override
    public boolean isSingleton() {
        return false;
    }
}

配置文件, factorybean.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="strFactoryBean" class="com.sharpcj.factorybeantest.StringFactoryBean">
        <property name="type" value="buffer"/>
        <property name="originStr" value="hello"/>
    </bean>

</beans>

測試類 AppTest.java

package com.sharpcj.factorybeantest;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;


public class AppTest {
    public static void main(String[] args) {

        ApplicationContext context = new ClassPathXmlApplicationContext("factorybean.xml");

        System.out.println(context.getBean("strFactoryBean"));
        System.out.println(context.getBean("strFactoryBean").getClass().toString());

    }
}

結果如下:

那有沒有辦法把獲取 FactoryBean 本身的實例呢?當然可以,如下方式

context.getBean("&strFactoryBean")

getBean方法是,在Bean id 前面增加&符號。

package com.sharpcj.factorybeantest;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class AppTest {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("factorybean.xml");
        System.out.println(context.getBean("strFactoryBean"));
        System.out.println(context.getBean("strFactoryBean").getClass().toString());
        System.out.println(context.getBean("&strFactoryBean").getClass().toString());
    }
}

結果如下:

BeanFactory

其實在前面的例子中,AppTest.java 類中我們已經使用過 BeanFactory 了, BeanFactory 也是一個接口。Spring 有兩個核心的接口: BeanFactory 和 ApplicationContext ,其中 ApplicationContext 是 BeanFactory 的子接口,他們都可以代表 Spring 容器。Spring 容器是生成 Bean 實例的工廠,並管理容器中的 Bean 。
BeanFactory 包含如下幾個基本方法:

boolean containsBean(String name) // 判斷Spring容器中是否包含 id 爲 name 的 Bean 實例
<T> getBean(Class<T> requeriedType) // 獲取Spring容器中屬於 requriedType 類型的、唯一的 Bean 實例。
Object getBean(String name) // 返回容器中 id 爲 name 的 Bean 實例
<T> getBean(String name, Class requiredType) // 返回容器中 id 爲name,並且類型爲 requriedType 的Bean
Class<T> getType(String name) // 返回 id 爲 name 的 Bean 實例的類型

四、Bean 後處理器 和 容器後處理器

Spring 提供了兩種常用的後處理使得 Spring 容器允許開發者對 Spring 容器進行擴展,分別是 Bean 後處理器和容器後處理器。

Bean 後處理器

Bean 後處理器是一種特殊的 Bean, 它可以對容器中的 Bean 進行後處理,對 Bean 進行額外加強。這種特殊的 Bean 不對外提供服務,它主要爲容器中的目標 Bean 進行擴展,例如爲目標 Bean 生成代理等。

Bean 後處理器需要實現 BeanPostProcessor 接口,該接口包含如下兩個方法:

Object postProcessBeforeInitialization(Object bean, String beanName)
Object postProcessAfterInitialization(Object bean, String beanName)

這兩個方法的第一個參數都表示即將進行後處理的 Bean 實例,第二個參數是該 Bean 的配置 id ,這兩個方法會在目標 Bean 初始化之前和初始化之後分別回調。

看例子:
新建一個類,PetBeanPostProcessor.java 重寫上述兩個方法。

package com.sharpcj.beanpostprocessor;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;

public class PetBeanPostProcessor implements BeanPostProcessor {
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        if ("dog".equals(beanName)) {
            System.out.println("準備初始化 dog ...");
        }
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if ("parrot".equals(beanName)) {
            System.out.println("parrot 初始化完成 ... ");
        }
        return bean;
    }
}

配置文件, beanpostprocessor.xml:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="dog" class="com.sharpcj.beanpostprocessor.Dog"/>
    <bean id="parrot" class="com.sharpcj.beanpostprocessor.Parrot"/>
    <bean class="com.sharpcj.beanpostprocessor.PetBeanPostProcessor"/>
</beans>

最後看測試代碼:AppTest.java

package com.sharpcj.beanpostprocessor;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class AppTest {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("beanpostprocessor.xml");
        Dog dog = (Dog) context.getBean("dog");
        Parrot parrot = (Parrot) context.getBean("parrot");
        dog.move();
        parrot.move();
    }
}

執行結果如下:

可以看到,我們像配置其它 Bean 一樣配置該 Bean 後處理器,但是我們沒有配置 id ,這是因爲我們使用的 ApplicationContext 作爲 Spring 容器,Spring 容器會自動檢測容器中所有的 Bean ,如果發現某個 Bean 實現了 BeanPostProcessor 接口,ApplicationContext 就會自動將其註冊爲 Bean 後處理器。 如果使用 BeanFactory 作爲 Spring 的容器,則需手動註冊 Bean 後處理器。這時,需要在配置文件中爲 Bean 後處理器指定 id 屬性,這樣容器可以先獲取到 Bean 後處理器的對象,然後註冊它。如下:

配置文件:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="dog" class="com.sharpcj.beanpostprocessor.Dog"/>
    <bean id="parrot" class="com.sharpcj.beanpostprocessor.Parrot"/>
    <bean id="petBeanPostProcessor" class="com.sharpcj.beanpostprocessor.PetBeanPostProcessor"/>
</beans>

測試代碼:

Resource resource = new ClassPathResource("beanpostprocessor.xml");
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
new XmlBeanDefinitionReader(beanFactory).loadBeanDefinitions(resource);

PetBeanPostProcessor petBeanPostProcessor = (PetBeanPostProcessor) beanFactory.getBean("petBeanPostProcessor");
beanFactory.addBeanPostProcessor(petBeanPostProcessor);

Dog dog = (Dog) beanFactory.getBean("dog");
Parrot parrot = (Parrot) beanFactory.getBean("parrot");
dog.move();
parrot.move();

上面例子中我們只是在實例化 Bean 前後打印了兩行 Log , 那麼實際開發中 Bean 後處理有什麼用處呢?其實 Bean 後處理器的作用很明顯,相當於一個攔截器,對目標 Bean 進行增強,在目標 Bean 的基礎上生成新的 Bean。 若我們需要對容器中某一批 Bean 進行增強處理,則可以考慮使用 Bean 後處理器,結合前面一篇文章講到到代理模式,可以想到,我們完全可以通過 Bean 後處理器結合代理模式做更多實際工作,比如初始化,深圳完全改變容器中一個或者一批 Bean 的行爲。
你可以配置多個 BeanPostProcessor 接口,通過設置 BeanPostProcessor 實現的 Ordered 接口提供的 order 屬性來控制這些 BeanPostProcessor 接口的執行順序。

容器後處理器

容器後處理器則是對容器本身進行處理。容器後處理器需要實現 BeanFactoryPostProcessor 接口。該接口必須實現如下一個方法:

postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)

類似於 BeanPostProcessor , ApplicationContext 可以自動檢測到容器中的容器後處理器,並自動註冊,若使用 BeanFactory 作爲 Spring 容器,則需要手動獲取到該容器後處理器的對象來處理該 BeanFactory 容器。
例子:容器後處理器, PetBeanFactoryPostProcessor.java

package com.sharpcj.beanfactorypostprocessor;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;

public class PetBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        System.out.println("容器後處理器沒有對容器做改變...");
    }
}

配置文件:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="dog" class="com.sharpcj.beanfactorypostprocessor.Dog"/>
    <bean id="parrot" class="com.sharpcj.beanfactorypostprocessor.Parrot"/>
    <bean id="petBeanFactoryPostProcessor" class="com.sharpcj.beanfactorypostprocessor.PetBeanFactoryPostProcessor"/>
</beans>

測試代碼:

package com.sharpcj.beanfactorypostprocessor;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class AppTest {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("beanfactorypostprocessor.xml");
        Dog dog = (Dog) context.getBean("dog");
        Parrot parrot = (Parrot) context.getBean("parrot");
        dog.move();
        parrot.move();
    }
}

結果如下:

容器後處理器的作用對象是容器本身,展開 BeanFactoryPostProcessor 接口的繼承關係,我們可以看到 Spring 本身提供了很多常見的容器後處理器。

其中一些在實際開發中很常用,如屬性佔位符配置器 PropertyPlaceholderConfigurer 、 重寫佔位符配置器 PropertyOverrideConfigurer 等。

五、BeanFactoryAware 和 BeanNameAware

讓 Bean 獲取 Spring 容器

程序啓動時,初始化 Spring 容器,我們已經知道如何通過容器,獲取 Bean 的實例方式,形如:

BeanFactory factory = xxx ;
factory.getBean(xxx...);

在某些特殊情況下,我們需要讓 Bean 獲取 Spring 容器,這個如何實現呢?
我們只需要讓 Bean 實現 BeanFactoryAware 接口,該接口只有一個方法:

void setBeanFactory(BeanFactory beanFactory);

該方法的參數即指向創建該 Bean 的 BeanFactory ,這個 setter 方法看起來有點奇怪,習慣上在 java 中 setter 方法都是由程序員調用,傳入參數,而此處的方法則由 Spring 調用。與次類似的,還有 ApplicationContextAware 接口,需要實現一個方法

void setApplicationContext(ApplicationContext applicationContext);

下面通過例子來說明:
這次我們的 Dog 類,修改了:

package com.sharpcj.beanfactoryaware;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;

public class Dog implements IPet, BeanFactoryAware {

    private BeanFactory factory;

    @Override
    public void move() {
        System.out.println("Dog can run!");
    }

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        factory = beanFactory;
    }

    public void test() {
        Parrot parrot = (Parrot) factory.getBean("parrot");
        parrot.move();
    }

}

測試代碼,AppTest.java

package com.sharpcj.beanfactoryaware;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class AppTest {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("beanfactoryaware.xml");
        Dog dog = (Dog) context.getBean("dog");
        dog.test();
    }
}

輸出結果:

Parrot can fly!

結果表明,我們確實在 Dog 類裏面獲取到了 Spring 容器,然後通過該容器創建了 Parrot 實例。

獲取 Bean 本身的 id

有時候,當我們在開發一個 Bean 類時,Bean 何時被部署到 Spring 容器中,部署到 Spring 容器中的 id 又是什麼,開發的時候我們需要提前預知,這是就可以藉助 Spring 提供的 BeanNameAware 接口,該接口提供一個方法:

void setBeanName(String name);

用法與上面一樣,這裏不再過多解釋,修改上面的例子:
Dog.java

package com.sharpcj.beanfactoryaware;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.BeanNameAware;

public class Dog implements IPet, BeanFactoryAware, BeanNameAware {

    private BeanFactory factory;

    private String id;

    @Override
    public void move() {
        System.out.println("Dog can run!");
    }

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        factory = beanFactory;
    }

    public void test() {
        System.out.println("Dog 的 id 是: " + id);
    }

    @Override
    public void setBeanName(String name) {
        id = name;
    }
}

測試類:AppTest.java

package com.sharpcj.beanfactoryaware;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class AppTest {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("beanfactoryaware.xml");
        Dog dog = (Dog) context.getBean("dog");
        dog.test();
    }
}

結果:

Dog 的 id 是: dog

六、ApplicationContext 的事件機制

ApplicationContext 的事件機制是觀察者模式的實現,由 事件源、事件和事件監聽器組成。通過 ApplicationEvent類 和 ApplicationListener 接口實現。
Spring 事件機制的兩個重要成員:

  • ApplicationEvent: 容器事件,必須由 ApplicationContext 發佈
  • ApplicationListener:事件監聽器,可由容器中任何 Bean 擔任。

事件機制原理:有 ApplicationContext 通過 publishEvent() 方法發佈一個實現了ApplicationEvent接口的事件,任何實現了 ApplicationListener接口的 Bean 充當事件監聽器,可以對事件進行處理。這個原理有點類似於 Android 裏面廣播的實現。
下面給出一個例子:
ITeacher.java

package com.sharpcj.appevent;

public interface ITeacher {
    void assignWork();
}

ChineseTeacher.java

package com.sharpcj.appevent;

import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;

public class ChineseTeacher implements ITeacher, ApplicationListener {
    @Override
    public void assignWork() {
        System.out.println("背誦三首唐詩");
    }

    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        if (event instanceof ComplainEvent) {
            System.out.println("語文老師收到了抱怨...");
            System.out.println("抱怨的內容是:" + ((ComplainEvent) event).getMsg());
            System.out.println("認真傾聽抱怨,但是作業量依然不能減少...");
        }
    }
}

MathTeacher.java

package com.sharpcj.appevent;

import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;

public class MathTeacher implements ITeacher, ApplicationListener {
    @Override
    public void assignWork() {
        System.out.println("做三道數學題");
    }

    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        System.out.println("數學老師收到了事件,但沒有判斷事件類型,不作處理。。。。");
    }
}

定義一個事件 ComplainEvent.java 繼承自 ApplicationContextEvent:

package com.sharpcj.appevent;

import org.springframework.context.ApplicationContext;
import org.springframework.context.event.ApplicationContextEvent;

public class ComplainEvent extends ApplicationContextEvent {

    private String msg;

    /**
     * Create a new ContextStartedEvent.
     *
     * @param source the {@code ApplicationContext} that the event is raised for
     *               (must not be {@code null})
     */
    public ComplainEvent(ApplicationContext source) {
        super(source);
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }
}

IStudent.java

package com.sharpcj.appevent;

public interface IStudent {
    void doWork();
}

XiaoZhang.java

package com.sharpcj.appevent;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

public class XiaoZhang implements IStudent, ApplicationContextAware {
    private ApplicationContext mContext;

    @Override
    public void doWork() {
        System.out.println("小張背了李白的唐詩,做了三道幾何體");
    }

    public void complain() {
        ComplainEvent complainEvent = new ComplainEvent(mContext);
        complainEvent.setMsg("作業太多了");
        mContext.publishEvent(complainEvent);
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.mContext = applicationContext;
    }
}

配置文件, appevent.xml:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="chineseTeacher" class="com.sharpcj.appevent.ChineseTeacher"/>
    <bean id="mathTeacher" class="com.sharpcj.appevent.MathTeacher"/>
    <bean id="xiaoZhang" class="com.sharpcj.appevent.XiaoZhang"/>
</beans>

測試類, AppTest.java:

package com.sharpcj.appevent;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class AppTest {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("appevent.xml");
        XiaoZhang xiaoZhang = (XiaoZhang) context.getBean("xiaoZhang");
        xiaoZhang.complain();
    }
}

測試結果如下:

咦,數學老師也受到了事件,爲什麼還受到兩次事件?首先根據代碼,我們能想明白,只要是容器發佈了事件,所有實現了ApplicationListener接口的監聽器都能接收到事件,那爲什麼,數學老師打印出了兩條呢?我猜,容器初始化期間,本身發佈了一次事件。下面稍微修改了一下代碼,便驗證了我的猜想是正確的。
MathTeacher.java

package com.sharpcj.appevent;

import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;

public class MathTeacher implements ITeacher, ApplicationListener {
    @Override
    public void assignWork() {
        System.out.println("做三道數學題");
    }

    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        System.out.println("數學老師收到了事件,但沒有判斷事件類型,不作處理。。。。" + event.getClass().getSimpleName());
    }
}

然後再次執行,結果如下:

事實證明,容器初始化時,確實發佈了一次 ContextRefreshedEvent 事件。

七、總結

既上一篇文章總結了一下 Spring 裝配 Bean 的三種方式之後,這篇文章繼續記錄了一寫 SpringIOC 的高級知識,本文沒有按照一般書籍的順序介紹 Spring 容器的相關知識,主要是從橫向對幾組關鍵概念進行對比解釋,主要記錄了一下 SpringIOC 中的一些關鍵知識點。當然 Spring IOC 其它的知識點還有很多,比如裝配 Bean 時屬性歧義性處理、 Bean 的組合屬性、注入集合值、國際化、基於 XML Schema 的簡化配置方式等。其它知識點可以通過查閱官方文檔或者專業書籍學習。
接下來會再整理一篇 Spring AOP 的文章。

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