wiki:waue/2011/spring
Spring 學習
Spring 3.1.0

引用 : 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.

xml 與 properties

不用 bean.xml

注意!此段原文可編譯但無法執行,以下編碼已經完全修正,請注意各檔案的路徑

假設 AaBean? 的內容如下:

  • src/waue/one/AaBean.java
package waue.one; 

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

只用 properties

XML檔案的階層格式適用於於組態設定,也因此許多的開源專案都將XML作為預設的組態定義方式,但通常也會提供非XML定義檔的方式,像屬性檔案. properties

Spring也可以讓您使用屬性檔案定義Bean,例如定義一個 waue_one.properties:

注意! waue_one.properties 放在src 目錄底下,而非根目錄(非beans-config.xml 的目錄)

注意! aaBean.(class) 的()符號不可省略,否則運算時出錯

  • src/waue_one.properties
aaBean.(class)=waue.one.AaBean
aaBean.helloWord=Properties Welcome!
  • src/waue/one/SpringDemo.java
package waue.one; 

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("waue_one.properties")); 
        
        BeanFactory factory = (BeanFactory) reg; 
        AaBean hello = (AaBean) factory.getBean("aaBean"); 
        System.out.println(hello.getHelloWord()); 
    } 
}

main中直接綁值

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

注意!以下 SpringDemo?.java 引用上面的 waue.one.AaBean?.java ,

  • src/waue/two/SpringDemo.java
package waue.two;

import org.springframework.beans.MutablePropertyValues;
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.RootBeanDefinition;

import waue.one.AaBean;

public class SpringDemo {
  public static void main(String[] args) {
    // 設置屬性
    MutablePropertyValues properties = new MutablePropertyValues();
    properties.addPropertyValue("helloWord", "Hello! Waue!");

    // 設置Bean定義
    RootBeanDefinition definition = new RootBeanDefinition(AaBean.class,
        properties);

    // 註冊Bean定義與Bean別名
    BeanDefinitionRegistry reg = new DefaultListableBeanFactory();
    reg.registerBeanDefinition("fooBean", definition);

    BeanFactory factory = (BeanFactory) reg;
    AaBean hello = (AaBean) factory.getBean("fooBean");
    System.out.println(hello.getHelloWord());
  }
}

properties / xml

藉由這個類別,您可以在.properties中設定一些優先屬性設定,這個設定如果與XML中的屬性定義有相衝突,則以.properties中的設定為主

注意! 此時 properties 檔必須跟 xml 檔放在一起

注意! xml 內的value 值無意義,因為都已 properties 為主,而 properties 也不可以因為 xml 已經設定了該property 屬性,而不給 aaBean.helloWord

  • /waue_one.properties
aaBean.helloWord=Setup Properties with beans-config 
  • /beans-config.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-3.0.xsd">
    <bean id="configBean" 
  class="org.springframework.beans.factory.config.PropertyOverrideConfigurer"> 
        <property name="location"> 
            <value>waue_one.properties</value> 
        </property> 
    </bean> 

    <bean id="aaBean" class="waue.one.AaBean"> 
        <property name="helloWord"> 
            <value> </value> 
        </property> 
    </bean>

</beans>
  • src/waue/one/SpringDemo.java (同前)
package waue.one; 

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

public class SpringDemo { 
    public static void main(String[] args) throws InterruptedException { 
        ApplicationContext context = 
            new FileSystemXmlApplicationContext("beans-config.xml");
         
        AaBean hello = 
            (AaBean) context.getBean("aaBean");
        System.out.println(hello.getHelloWord());
    } 
}
  • src/waue/one/AaBean.java (同前)
package waue.one; 

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

進階 bean 設定

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將之改為大寫字母了。

資料集合

對於像 陣列、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>

自動綁定

測試後not work,也許與Spring 版本有關

autowire (詳細)

bean 生命週期

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

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

spring aop

在一個服務的流程中插入與服務無關的邏輯(例如Logging、Security),這樣的邏輯稱為 Cross-cutting concerns,將 Crossing-cutting concerns 獨立出來為一個物件,這樣的特殊物件稱之為 Aspect,Aspect-oriented programming 著重在 Aspect 的設計及與應用程式的縫合(Weave)。

可以使用代理(Proxy)機制來解決這個問題,在這邊討論兩種代理方式:靜態代理(Static proxy)與動態代理(Dynamic proxy)。

以下範例中,HelloProxy或是LogHandler,這樣的物件稱之為切面(Aspect)

AOP中的Aspect所指的可以是像日誌等這類的動作或服務, 您將這些動作(Cross-cutting concerns)設計為通用、 不介入特定業務物件的一個職責清楚的Aspect物件, 這就是所謂的Aspect-oriented programming,縮寫名詞即為AOP。

Aspect可以獨立於應用程式之外,在必要的時候,可以介入應用程式之中提供服務; 而不需要相關服務的時候,又可以將這些Aspect直接從應用程式中脫離, 而您的應用程式本身不需修改任何一行程式碼。

無 aop

package onlyfun.caterpillar;

import java.util.logging.*;

public class HelloSpeaker {
    private Logger logger =
            Logger.getLogger(this.getClass().getName());

    public void hello(String name) {
        // 方法執行開始時留下日誌
        logger.log(Level.INFO, "hello method starts....");
        // 程式主要功能
        System.out.println("Hello, " + name);
        // 方法執行完畢前留下日誌
        logger.log(Level.INFO, "hello method ends....");
    }
}

在HelloSpeaker類別中,當執行hello()方法時,您希望該方法執行開始與執行完畢時都能留下日誌,最簡單的作法就是如以上的程式設計,在方法執行的前後加上日誌動作,然而記錄的這幾行程式碼橫切入(Cross-cutting)HelloSpeaker類別中,對於 HelloSpeaker來說,日誌的這幾個動作並不屬於HelloSpeaker商務邏輯(顯示"Hello"等文字),這使得 HelloSpeaker增加了額外的職責

靜態代理

  • IHello.java
package onlyfun.caterpillar;

public interface IHello {
    public void hello(String name);
}
package onlyfun.caterpillar;

public class HelloSpeaker implements IHello {
    public void hello(String name) {
        System.out.println("Hello, " + name); 
    }
}
package onlyfun.caterpillar;

import java.util.logging.*; 

public class HelloProxy implements IHello { 
    private Logger logger = 
            Logger.getLogger(this.getClass().getName());
    
    private IHello helloObject; 

    public HelloProxy(IHello helloObject) { 
        this.helloObject = helloObject; 
    } 

    public void hello(String name) { 
        // 日誌服務
        log("hello method starts....");      

        // 執行商務邏輯
        helloObject.hello(name);
        
        // 日誌服務
        log("hello method ends...."); 
    } 
    
    private void log(String msg) {
        logger.log(Level.INFO, msg);
    }
}
package onlyfun.caterpillar;

public class ProxyDemo {
    public static void main(String[] args) {
        IHello proxy = 
            new HelloProxy(new HelloSpeaker());
        proxy.hello("Justin");
    }
} 

動態 aop

jdk本身即有可協助開發動態代理功能的API等相關類別,您不必為特定物件與方法撰寫特定的代理物件,使用動態代理,可以使得一個處理者(Handler)服務於各個物件

主要的概念是使用Proxy.newProxyInstance()靜態方法建立一個代理物件,建立代理物件時必須告知所要代理的介面,

操作所建立的代理物件,在每次操作時會呼叫InvocationHandler的invoke()方法,invoke()方法會傳入被代理物件的方法名稱與執行參數,

實際上要執行的方法交由method.invoke(),您在method.invoke()前後加上記錄動作,method.invoke()傳回的物件是實際方法執行過後的回傳結果。

使用LogHandler的bind()方法來綁定被代理物件

package onlyfun.caterpillar;

import java.util.logging.*; 
import java.lang.reflect.*; 

public class LogHandler implements InvocationHandler { 
    private Logger logger = 
            Logger.getLogger(this.getClass().getName()); 
    
    private Object delegate;

    public Object bind(Object delegate) { 
        this.delegate = delegate; 
        return Proxy.newProxyInstance( 
                           delegate.getClass().getClassLoader(), 
                           delegate.getClass().getInterfaces(), 
                           this); 
    } 

    public Object invoke(Object proxy, Method method, 
                         Object[] args) throws Throwable { 
        Object result = null; 
        
        try { 
            log("method starts..." + method);
            
            result = method.invoke(delegate, args);
            
            logger.log(Level.INFO, "method ends..." + method); 
        } catch (Exception e){ 
            log(e.toString()); 
        }
        
        return result; 
    } 
    
    private void log(String message) {
        logger.log(Level.INFO, message);
    }
}

  • (同前) IHello.java
package onlyfun.caterpillar;

public interface IHello {
    public void hello(String name);
}
package onlyfun.caterpillar;

public class HelloSpeaker implements IHello {
    public void hello(String name) {
        System.out.println("Hello, " + name); 
    }
}
package onlyfun.caterpillar;

public class ProxyDemo {
    public static void main(String[] args) {
        LogHandler logHandler  = new LogHandler(); 
        
        IHello helloProxy = 
                (IHello) logHandler.bind(new HelloSpeaker()); 
        helloProxy.hello("Justin");
    }
}

靜態與動態

動態aop 與 靜態 aop 中,(主要介面) I_Hello.java 與 (主要功能)HelloSpeaker?.java 內容相同

  • 靜態aop

主要不同為: HelloProxy?.java

main 呼叫: IHello proxy = new HelloProxy?(new HelloSpeaker?());

  • 動態aop

主要不同為: LogHandler?.java

main 呼叫:IHello helloProxy = (IHello) logHandler.bind(new HelloSpeaker?());

其中 I_Hello 與 HelloSpeaker? 相同於 動態aop 方法

AOP 專有名詞

  • Aspect

將散落於各個商務物件之中的Cross-cutting concerns收集起來,設計各個獨立可重用的物件,這些物件稱之為Aspect,

例如在 動態代理 中將日誌的動作設計為一個LogHandler類別,LogHandler類別在AOP的術語就是Aspect的一個具體實例

  • Advice

Aspect的具體實作稱之為Advice,以日誌的動作而言,Advice中會包括真正的日誌程式碼是如何實作的,

像是動態代理 中的LogHandler類別就是Advice的一個具體實例,Advice中包括了Cross-cutting concerns的行為或所要提供的服務。

  • Joinpoint

Aspect在應用程式執行時加入商務流程的點或時機稱之為Joinpoint

  • Pointcut

Pointcut是一個定義,藉由這個定義您可以指定某個Aspect在哪些Joinpoint時被應用至應用程式之上

  • Target

一個Advice被應用的對象或目標物件,

例如 動態代理 中的HelloSpeaker就是LogHandler這個Advice的Target。

  • Introduction

對於一個現存的類別,Introduction可以為其增加行為,而不用修改該類別的程式,具體的說,您可以為某個已撰寫、編譯完成的類別,在執行時期動態加入一些方法或行為,而不用修改或新增任何一行程式碼。

  • Proxy

在之前 從代理機制初探 AOP 與 動態代理 中,已經使用實際的程式範例介紹過代理機制的實現,Spring的AOP主要是透過動態代理來完成。

  • Weave

Advice被應用至物件之上的過程稱之為縫合(Weave)

http://caterpillar.onlyfun.net/Gossip/SpringGossip/images/AOPConcept-3.JPG

eclipse 開發環境

詳見SpringEclipse

Last modified 13 years ago Last modified on Sep 19, 2011, 12:57:06 PM