wiki:waue/2011/spring

Version 26 (modified by waue, 13 years ago) (diff)

--

Spring 學習
Spring 3.1.0 + eclipse 3.6

引用 : http://caterpillar.onlyfun.net/Gossip/SpringGossip/SpringGossip.html

理由

閱讀「iT人甘苦談─穩定的工作並非人生的全部」 最後一段描述:「對於掌握程式外包的品質,具有相當程度的信心。….比如開發一個以Java為主的專案.....會要求,團隊必須遵照Spring Framework的應用程式框架開發。..只要依照標準,基本上品質不會差到哪裡去。」

簡介

Spring 的核心是個輕量級(Lightweight)容器(Container),實現了IoC(Inversion of Control)模式的容器,基於此核心容器所建立的應用程式,可以達到程式元件的鬆散耦合(Loose coupling),讓程式元件可以進行測試(Testability),這些特性都使得整個應用程式可以在架構上與維護上都能得到相當程度的簡化。

  • 輕量級(Lightweight)
    • 核心在檔案容量上只有不到 1MB 的大小
    • 使用核心所需要的資源也是很小的
    • 非侵入性(Nonintrusive)框架,它的目的之一,是讓應用程式不感受到框架的存在,減低應用程式從框架移植時的負擔。
  • 容器(Container)
    • 管理物件的生命週期、物件的組態、相依注入等
    • 控制物件在創建時是以 原型(Prototype) 或 單例(Singleton) 的方式來建立。
  • IoC = Inversion of Control,控制反轉。
    • 在Java開發過程中,IoC意謂著將你設計好的類別交給系統去控制,而不是在你的類別內部自己控制。
    • Spring 的核心概念是IoC,更具體而易懂的名詞是依賴注入(Dependency Injection)
    • 不必自己在程式碼中維護物件的依賴關係,只需在組態檔中加以設定

除了這些特性之外,Spring 的目標是實現一個全方位的整合框架,在 Spring 框架下實現多個子框架的組合,這些子框架之間彼此可以獨立,也可以使用其它的框架方案加以替代,Spring 希望提供 one-stop shop 的框架整合方案。

  • AOP 框架
    • 支援AOP(Aspect-oriented programming)
  • 持久層
    • JDBC、O/R Mapping工具(Hibernate、iBATIS)、事務處理等。
  • Web 框架
    • 提供 Web 框架的解決方案
    • 也可以將自己所熟悉的 Web 框架與 Spring 整合
    • 如 Struts、Webwork 等

對於一些服務,例如 JNDI、Mail、排程、遠程等,Spring 不直接提供實作,而是採取抽象層方式進行包裝,讓這些服務在使用時可以有一致的使用模式且更為方便。

IoC 模式

Spring 的核心概念是 IoC,IoC 的抽象概念是「依賴關係的轉移」,像是「高層模組不應該依賴低層模組,而是模組都必須依賴於抽象」是 IoC 的一種表現,「實現必須依賴抽象,而不是抽象依賴實現」也是 IoC 的一種表現,「應用程式不應依賴於容器,而是容器服務於應用程式」也是 IoC 的一種表現。

IoC 全名 Inversion of Control,如果中文硬要翻譯過來的話,就是「控制反轉」。

初看 IoC,從字面上不容易瞭解其意義,我覺得要瞭解 IoC,要先從 Dependency Inversion 開始瞭解,也就是依賴關係的反轉。

Dependency Inversion 簡單的說,在模組設計時,高層的抽象模組通常是與業務相關的模組,它應該具有重用性,而不依賴於低層的實作模組,例如如果低層模組原先是軟碟存取模式,而高層模組是個存檔備份的需求,如果高層模組直接叫用低層模組的函式,則就對低層模組產生了依賴關係。

舉個例子,例如下面這個程式:

#include <floppy.h>
....
void save() {
        ....
        saveToFloppy()
    }
}

由於save()程式依賴於依賴於saveToFloppy(),如果今天要更換低層的存儲模組為Usb碟,則這個程式沒有辦法重用,必須加以修改才行,低層模組的更動造成了高層模組也必須跟著更動,這不是一個好的設計方式,在設計上希望模組都依賴於模組的抽象,這樣才可以重用高層的業務設計。

如果以物件導向的方式來設計,Dependency Injection 的解釋變為程式不應依賴實作,而是依賴於抽象,實作必須依賴於抽象。來看看下面這個 Java 程式:

public class BusinessObject {
    private FloppyWriter writer = new FloppyWriter();
    ....
   
    public void save() {
        ...
        writer.saveToFloppy();

    }
}

在這個程式中,BusinessObject? 的存檔依賴於實際的 FloppyWriter?,如果今天想要將存檔改為存至 Usb 碟,則必須修改或繼承 BusinessObject? 進行擴展,而無法直接使用BusinessObject。

如果透過介面的宣告,可以改進此一情況,例如:

public interface IDeviceWriter {
    public void saveToDevice();
}

public class BusinessObject {
    private IDeviceWriter writer;

    public void setDeviceWriter(IDeviceWriter writer) {
        this.writer = writer;
    }

    public void save() {
        ....
        writer.saveToDevice();
    }
}

這樣一來,BusinessObject? 就是可重用的,如果今天有存儲至 Floppy 或 Usb 碟的需求,只要實作 IDeviceWriter 即可,而不用修改 BusinessObject?

public class FloppyWriter implement IDeviceWriter {
    public void saveToDevice() {
        ....
        // 實際儲存至Floppy的程式碼
    }
}

public class UsbDiskWriter implement IDeviceWriter {
    public void saveToDevice() {
        ....
        // 實際儲存至UsbDisk的程式碼
    }
}

從這個角度來看,Dependency Injection (Detail) 的意思即是程式不依賴於實作,而是程式與實作都要依賴於抽象。

IoC 的 Control 是控制的意思,其實其背後的意義也是一種依賴關係的轉移,如果A依賴於B,其意義即是B擁有控制權,您想要轉移這種關係,所以依賴關係的反轉即是控制關係的反轉,藉由控制關係的轉移,可以獲得元件的可重用性,在上面的 Java 程式中,整個控制權從實際的 FloppyWriter? 轉移至抽象的 IDeviceWriter 介面上,使得BusinessObject、FloppyWriter?UsbDiskWriter? 這幾個實現依賴於抽象的 IDeviceWriter 介面。

程式的業務邏輯部份應是可以重用的,不應受到所使用框架或容器的影響,因為可能轉移整個業務邏輯至其它的框架或容器,如果業務邏輯過於依賴容器,則轉移至其它的框架或容器時,就會發生困難。

IoC 在容器的角度,可以用這麼一句好萊塢名言來代表:"Don't call me, I'll call you." 以程式的術語來說的話,就是「不要向容器要求您所需要的(物件)資源,容器會自動將這些物件給您!」。IoC 要求的是容器不侵入應用程式本身,應用程式本身提供好介面,容器可以透過這些介面將所需的資源注至至程式中,應用程式不向容器主動要求資源,故而不會依賴於容器的元件,應用程式本身不會意識到正被容器使用,可以隨時從容器中脫離轉移而不用作任何的修改,而這個特性正是一些業務邏輯中間件最需要的。

Dependency Injection

詳見 Dependency Injection

程式範例

package onlyfun.caterpillar; 

public class HelloBean { 
    private String helloWord; 
    
    public void setHelloWord(String helloWord) { 
        this.helloWord = helloWord; 
    } 
    public String getHelloWord() { 
        return helloWord; 
    } 
}

  • beans-config.xml
<?xml version="1.0" encoding="UTF-8"?> 
<!DOCTYPE beans PUBLIC "-//SPRING/DTD BEAN/EN" 
  "http://www.springframework.org/dtd/spring-beans.dtd"> 

<beans> 
    <bean id="helloBean" 
          class="onlyfun.caterpillar.HelloBean"> 
        <property name="helloWord">
            <value>Hello!Justin!</value>
        </property> 
    </bean> 
</beans>

package onlyfun.caterpillar; 

import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.beans.factory.BeanFactory; 
import org.springframework.beans.factory.xml.XmlBeanFactory; 

public class SpringDemo { 
    public static void main(String[] args) { 
        Resource rs = 
            new FileSystemResource("beans-config.xml"); 
        BeanFactory factory = 
            new XmlBeanFactory(rs); 
        
        HelloBean hello = 
            (HelloBean) factory.getBean("helloBean"); 
        System.out.println(hello.getHelloWord()); 
    } 
}

用 ApplicationContext

用 ApplicationContext? 取代 BeanFactory?

BeanFactory負責讀取Bean定義檔,管理物件的載入、生成,物件之間的關係維護,負責Bean的生命週期,對於簡單的應用程式來說,使用 BeanFactory就已經足夠,但是若要利用到Spring在框架上的一些功能以及進階的容器功能,則可以使用 ApplicationContext?,BeanFactory則通常用於一些資源有限的裝置,像是行動設備。

ApplicationContext的基本功能與BeanFactory很相似,它也負責讀取Bean定義檔,維護Bean之間的關係等,然而ApplicationContext提供的一個應用程式所需的更完整的框架功能:

  • 提供取得資源檔案更方便的方法。
  • 提供文字訊息解析的方法,並支援國際化(Internationalization, I18N)訊息。
  • 可以發佈事件,對事件感興趣的Bean可以接收到這些事件。

Rod Johnson建議使用ApplicationContext來取代BeanFactory,在許多實作ApplicationContext的類別中,最常使用的大概是以下三個:

  • FileSystemXmlApplicationContext
    • 可指定XML定義檔的相對路徑或絕對路徑來讀取定義檔。
  • ClassPathXmlApplicationContext
    • 從Classpath中來讀取XML定義檔。
  • XmlWebApplicationContext
    • 在Web應用程式中的檔案架構中讀取定義檔。
package onlyfun.caterpillar; 

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

public class SpringDemo { 
    public static void main(String[] args) { 
        ApplicationContext context = 
            new FileSystemXmlApplicationContext("beans-config.xml");
         
        HelloBean hello = 
            (HelloBean) context.getBean("helloBean"); 
        System.out.println(hello.getHelloWord()); 
    } 
}

程式簡化版

簡化以上的寫法, bean , xml , main java 間的關係簡化如下程式碼。

並注意以下 兩個 bean 間引用的關係 (BBean , dateBean )

  • BBean.java
package waue.org; 

import java.util.Date; 

public class BBean { 
    private String baa; 
    private Date date; 
    
    public void setBaa(String ba) { 
        this.baa = ba; 
    } 
    public String getBaa() { 
        return this.baa; 
    } 
    public void setDate(Date date) { 
        this.date = date; 
    }    
    public Date getDate() { 
        return this.date; 
    } 
}

在以下的Bean定義檔中,先定義了一個dateBean,之後bBean可以直接參考至dateBean,Spring會幫我們完成這個依賴關係:

  • A.xml
<?xml version="1.0" encoding="UTF-8"?> 
<!DOCTYPE beans PUBLIC "-//SPRING/DTD BEAN/EN" 
  "http://www.springframework.org/dtd/spring-beans.dtd"> 

<beans> 
    <bean id="dateBean" class="java.util.Date"/> 
    
    <bean id="bBean" class="waue.org.BBean"> 
        <property name="baa"> 
            <value>Hello!</value> 
        </property> 
        <property name="date"> 
            <ref bean="dateBean"/> 
        </property> 
    </bean> 
</beans>

直接指定值或是使用<ref>直接指定參考至其它的Bean,撰寫以下的程式來測試Bean的依賴關係是否完成:

  • C.java
package waue.org; 

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

public class C { 
    public static void main(String[] args) { 
        ApplicationContext context = 
            new FileSystemXmlApplicationContext("A.xml");
         
        BBean hello = 
            (BBean) context.getBean("bBean");
        System.out.print(hello.getBaa());
        System.out.print(" It's ");
        System.out.print(hello.getDate());
        System.out.println(".");
    } 
}
  • 執行結果如下:
Hello! It's Sat Oct 22 15:36:48 GMT+08:00 2005.

資料集合

對於像 陣列、java.util.List、java.util.Set、java.util.Map集合物件,在注入前必須填充入一些物件至集合中,然後再將集合物件注入至所需的Bean中,

public class SomeBean {
    private String[] someStrArray;
    private SomeObj[] someObjArray;
    private List someList;
    private Map someMap;
    ....

}
  • B.xml :陣列,List => <list> ; Map => <map>
<beans>
    <bean id="someBean" class="onlyfun.caterpillar.SomeBean">
        <property name="someArray">
            <list>
                <value>Hello!Justin!</value>
                <value>Hello!Momor!</value>
                <value>Hello!Bush!</value>
            </list>
        </property>
        <property name="someObjArray">
            <list>
                <ref bean="someObj1"/>
                <ref bean="someObj2"/>
            </list>
        </property>
        <property name="someList">
            <list>
                 <value>Hello!Justin!</value>
                 <ref bean="someObj1"/>
                 <ref bean="someObj2"/>
            </list>
        </property>
        <property name="someMap">
            <map>
                 <entry key="somekey1">
                     <ref bean="someObj1"/>
                 </entry>
                 <entry key="somekey2">
                     <value>Hello!Justin!</value>
                 </entry>
            </map>
        </property>
    </bean>
</beans>

上面的Bean定義檔是個綜合示範,陣列與List物件都是用<list>標籤來設定,而Map物件使用<map>標籤設定,並需要一個key值設定。

  • Set => <set> :
<set>
    <value>a set element</value>
    <ref bean="otherBean"/>
    <ref bean="anotherBean"/>
</set>

自動綁定

autowire (詳細) 測試後not work,也許與Spring 版本有關

bean 生命週期

在Spring中,從BeanFactory或ApplicationContext取得的實例為 Singleton,預設是每一個Bean別名維持一個實例,對單執行緒的程式來說並不會有什麼問題,但對於多執行緒的程式,您必須注意到執行緒安全,您也可以設定每次取得Bean時都產生一個新的實例,例如:

<bean id="helloBean"
      class="onlyfun.caterpillar.HelloBean"
      singleton="false">

不透過 xml 給值

假設HelloBean的內容如下:

  • HelloBean?.java
    package onlyfun.caterpillar; 
    
    public class HelloBean { 
        private String helloWord;
        
        public HelloBean() {
        }
        
        public void setHelloWord(String helloWord) { 
            this.helloWord = helloWord;
        } 
        public String getHelloWord() { 
            return helloWord; 
        }
    }
    

使用 properties

XML檔案的階層格式適用於於組態設定,也因此許多的開源專案都將XML作為預設的組態定義方式,但通常也會提供非XML定義檔的方式,像屬性檔案. properties,Spring也可以讓您使用屬性檔案定義Bean,例如定義一個beans-config.properties:

  • beans-config.properties
    helloBean.class=onlyfun.caterpillar.HelloBean
    helloBean.helloWord=Welcome!
    

屬性檔中helloBean名稱即是Bean的名稱,.class用於指定類別來源,其它的屬性就如.helloWord即屬性的名稱,可以使用 org.springframework.beans.factory.support.PropertiesBeanDefinitionReader? 來讀取屬性檔,一個範例如下:

package onlyfun.caterpillar; 

import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.PropertiesBeanDefinitionReader; 
import org.springframework.core.io.ClassPathResource;

public class SpringDemo { 
    public static void main(String[] args) { 
        BeanDefinitionRegistry reg = 
            new DefaultListableBeanFactory(); 
        PropertiesBeanDefinitionReader reader = 
            new PropertiesBeanDefinitionReader(reg); 
        reader.loadBeanDefinitions(
                new ClassPathResource("beans-config.properties")); 
        
        BeanFactory factory = (BeanFactory) reg; 
        HelloBean hello = (HelloBean) factory.getBean("helloBean"); 
        System.out.println(hello.getHelloWord()); 
    } 
}

直接綁值

不用 xml , 也不用 properties 來設定值

好處是,客戶端與定義檔是隔離的,他們無法接觸定義檔的內容,直接來看個例子:

package onlyfun.caterpillar; 

import org.springframework.beans.factory.support.BeanDefinitionRegistry; 
import org.springframework.beans.factory.support.DefaultListableBeanFactory; 
import org.springframework.beans.factory.support.RootBeanDefinition; 
import org.springframework.beans.factory.BeanFactory; 
import org.springframework.beans.MutablePropertyValues; 

public class SpringDemo { 
    public static void main(String[] args) { 
        // 設置屬性 
        MutablePropertyValues properties = new MutablePropertyValues(); 
        properties.addPropertyValue("helloWord", "Hello!Justin!"); 
        
        // 設置Bean定義 
        RootBeanDefinition definition = 
                    new RootBeanDefinition(HelloBean.class, properties); 
        
        // 註冊Bean定義與Bean別名 
        BeanDefinitionRegistry reg = new DefaultListableBeanFactory(); 
        reg.registerBeanDefinition("helloBean", definition); 
        
        BeanFactory factory = (BeanFactory) reg;        
        HelloBean hello = (HelloBean) factory.getBean("helloBean"); 
        System.out.println(hello.getHelloWord()); 
    } 
}

bean 給值的前後設定

在Bean的屬性被Spring容器設定之後,您還有機會自訂一些對Bean的修正,您可以實作org.springframework.beans.factory.config.BeanPostProcessor介面:

package org.springframework.beans.factory.config;
public interface BeanPostProcessor {
    public Object postProcessBeforeInitialization(Object bean, String name) throws BeansException;
    public Object postProcessAfterInitialization(Object bean, String name) throws BeansException;
}

postProcessBeforeInitialization()方法會在Bean初始化動作之前(例如InitializingBean的 afterPropertiesSet()方法或自定義的init方法)被呼叫,而postProcessAfterInitialization()方法會在Bean初始化之後立即被呼叫。

舉個例子來說,您可以實作一個大寫修正器,對於String型態的Bean屬性,無論在定義檔中是設定為大寫或小寫,在Bean屬性設定之後,您可以在大寫修正器中將所有的String改為大寫,例如:

package onlyfun.caterpillar;

import java.lang.reflect.Field;

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

public class UpperCaseModifier implements BeanPostProcessor {

    public Object postProcessBeforeInitialization(
                        Object bean, String name) throws BeansException {
        Field[] fields = bean.getClass().getDeclaredFields();
        
        for(int i = 0; i < fields.length; i++) {
            if(fields[i].getType().equals(String.class)) {
                fields[i].setAccessible(true);
                try {
                    String original = (String) fields[i].get(bean);
                    fields[i].set(bean, original.toUpperCase());
                } catch (IllegalArgumentException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
        
        return bean;
    }

    public Object postProcessAfterInitialization(
                          Object bean, String name) throws BeansException {
        return bean;
    }

}

假設您定義了這麼一個Bean類別:

package onlyfun.caterpillar; 

public class HelloBean { 
    private String helloWord; 
    
    public HelloBean() {
    }
    
    public void setHelloWord(String helloWord) { 
        this.helloWord = helloWord; 
    } 
    public String getHelloWord() { 
        return helloWord; 
    } 
}

ApplicationContext會自動偵測您是否在定義檔中定義了實作BeanPostProcessor介面的類別,例如:

  • beans-config.xml
<?xml version="1.0" encoding="UTF-8"?> 
<!DOCTYPE beans PUBLIC "-//SPRING/DTD BEAN/EN" 
  "http://www.springframework.org/dtd/spring-beans.dtd"> 

<beans>  
    <bean id="upperCaseModifier" 
          class="onlyfun.caterpillar.UpperCaseModifier"/>
    
    <bean id="helloBean" class="onlyfun.caterpillar.HelloBean"> 
        <property name="helloWord"> 
            <value>Hello!</value> 
        </property> 
    </bean>
</beans>

Spring容器會在每一個Bean被初始化之前之後分別呼叫upperCaseModifier的 postProcessBeforeInitialization()方法與postProcessAfterInitialization()方法,以對Bean進行指定的相關修正,可以實際來看看以下的測試程式:

package onlyfun.caterpillar; 

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

public class SpringDemo { 
    public static void main(String[] args) { 
        ApplicationContext context = 
            new FileSystemXmlApplicationContext("beans-config.xml");
         
        HelloBean hello = 
            (HelloBean) context.getBean("helloBean");
        System.out.println(hello.getHelloWord());
    } 
}

執行結果如下:

HELLO!

雖然您在定義檔中的helloBean之helloWord屬性是設定小寫字母,但upperCaseModifier將之改為大寫字母了。

eclipse 開發環境

詳見SpringEclipse