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

SharpCJ發表於2019-05-23

上文: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();
    }
}

程式結果:
SpringIOC二—— 容器 和 Bean的深入理解

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

另外,假設我們在測試類中通過 factory 獲取 Bean 例項時,傳入一個非法的引數,會如何? PetFactory 類工廠方法的程式碼,看起來會丟擲我們自定義的異常?
比如呼叫如下程式碼:

factory.getBean("cat");

結果是:
SpringIOC二—— 容器 和 Bean的深入理解

結果說明,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();
    }
}

測試結果如下:
SpringIOC二—— 容器 和 Bean的深入理解

結果正常,這裡注意配置檔案,使用靜態工廠方法是,配置檔案中我們並沒有配置 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());

    }
}

結果如下:
SpringIOC二—— 容器 和 Bean的深入理解

那有沒有辦法把獲取 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());
    }
}

結果如下:
SpringIOC二—— 容器 和 Bean的深入理解

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();
    }
}

執行結果如下:
SpringIOC二—— 容器 和 Bean的深入理解

可以看到,我們像配置其它 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();
    }
}

結果如下:

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

容器後處理器的作用物件是容器本身,展開 BeanFactoryPostProcessor 介面的繼承關係,我們可以看到 Spring 本身提供了很多常見的容器後處理器。
SpringIOC二—— 容器 和 Bean的深入理解

其中一些在實際開發中很常用,如屬性佔位符配置器 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();
    }
}

測試結果如下:
SpringIOC二—— 容器 和 Bean的深入理解

咦,數學老師也受到了事件,為什麼還受到兩次事件?首先根據程式碼,我們能想明白,只要是容器釋出了事件,所有實現了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());
    }
}

然後再次執行,結果如下:
SpringIOC二—— 容器 和 Bean的深入理解

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

七、總結

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

相關文章