1. 程式人生 > >對Spring框架初步認識

對Spring框架初步認識

還沒接觸過Spring,看得有點模糊。。

一、Spring誕生

Spring是一個開源框架,目前在開源社群的人氣很旺,被認為是最有前途的開源框架之一。她是由Rod Johnson建立的,她的誕生是為了簡化企業級系統的開發。說道Spring就不得不說EJB,因為Spring在某種意義上是EJB的替代品,她是一種輕量級的容器。用過EJB的人都知道EJB很複雜,為了一個簡單的功能你不得不編寫多個Java檔案和部署檔案,他是一種重量級的容器。也許你不瞭解EJB,你可能對“輕(重)量級”和“容器”比較陌生,那麼這裡我簡單介紹一下。

1、什麼是容器

感性的來講,容器就是可以用來裝東西的物品。那麼在程式設計領域就是指用來裝物件(OO的思想,如果你連OO都不瞭解,建議你去學習OO先)的物件。然而這個物件比較特別,它不僅要容納其他物件,還要維護各個物件之間的關係。這麼講可能還是太抽象,來看一個簡單的例子:

程式碼片斷1:

publicclass Container  

    
publicvoid init() 
    

    Speaker s 
=new Speaker(); 
    Greeting g 
=new Greeting(s); 
    }
 
}

可以看到這裡的Container類(容器)在初始化的時候會生成一個Speaker物件和一個Greeting物件,並且維持了它們的關係,當系統要用這些物件的時候,直接問容器要就可以了。這就是容器最基本的功能,維護系統中的例項(物件)。如果到這裡你還是感到模糊的話,別擔心,我後面還會有相關的解釋。

2、輕量級與重量級

所謂“重量級”是相對於“輕量級”來講的,也可以說“輕量級”是相對於重量級來講的。在Spring出現之前,企業級開發一般都採用EJB,因為它提供的事務管理,宣告式事務支援,持久化,分佈計算等等都“簡化”了企業級應用的開發。我這裡的“簡化”打了雙引號,因為這是相對的。重量級容器是一種入侵式的,也就是說你要用EJB提供的功能就必須在你的程式碼中體現出來你使用的是EJB,比如繼承一個介面,宣告一個成員變數。這樣就把你的程式碼繫結在EJB技術上了,而且EJB需要JBOSS這樣的容器支援,所以稱之為“重量級”。

相對而言“輕量級”就是非入侵式的,用Spring開發的系統中的類不需要依賴Spring中的類,不需要容器支援(當然Spring本身是一個容器),而且Spring的大小和執行開支都很微量。一般來說,如果系統不需要分佈計算或者宣告式事務支援那麼Spring是一個更好的選擇。

二、幾個核心概念

在我看來Spring的核心就是兩個概念,反向控制(IoC),面向切面程式設計(AOP)。還有一個相關的概念是POJO。

1、POJO

看到過的POJO全稱有兩個,Plain Ordinary Java Object,Plain Old Java Object,兩個差不多,意思都是普通的Java類,所以也不用去管誰對誰錯。POJO可以看做是簡單的JavaBean(具有一系列Getter,Setter方法的類)。嚴格區分這裡面的概念沒有太大意義,瞭解一下就行。

2、IoC

IoC的全稱是Inversion of Control,中文翻譯反向控制或者逆向控制。這裡的反向是相對EJB來講的。EJB使用JNDI來查詢需要的物件,是主動的,而Spring是把依賴的物件注入給相應的類(這裡涉及到另外一個概念“依賴注入”,稍後解釋),是被動的,所以稱之為“反向”。先看一段程式碼,這裡的區別就很容易理解了。

程式碼片段2:

publicvoid greet() 

Speaker s 
=new Speaker(); 
s.sayHello(); 
}

程式碼片段3:

publicvoid greet() 

Speaker s 
= (Speaker)context.lookup("ejb/Speaker"); 
s.sayHello(); 
}


程式碼片段4:

publicclass Greeting  

    
public Speaker s; 
    
public Greeting(Speaker s) 
    

        
this.s = s; 
    }
 
    
publicvoid greet() 
    

        s.sayHello(); 
    }
 
}

我們可以對比一下這三段程式碼。其中片段2是不用容器的編碼,片段3是EJB編碼,片段4是Spring編碼。結合程式碼片段1,你能看出來Spring編碼的優越之處嗎?也許你會覺得Spring的編碼是最複雜的。不過沒關係,我在後面會解釋Spring編碼的好處。

這裡我想先解釋一下“依賴注入”。根據我給的例子可以看出,Greeting類依賴Speaker類。片段2和片段3都是主動的去獲取Speaker,雖然獲取的方式不同。但是片段4並沒有去獲取或者例項化Speaker類,而是在greeting函式中直接使用了s。你也許很容易就發現了,在建構函式中有一個s被注入(可能你平時用的是,傳入)。在哪裡注入的呢?請回頭看一下程式碼片段1,這就是使用容器的好處,由容器來維護各個類之間的依賴關係(一般通過Setter來注入依賴,而不是建構函式,我這裡是為了簡化示例程式碼)。Greeting並不需要關心Speaker是哪裡來的或是從哪裡獲得Speaker,只需要關注自己分內的事情,也就是讓Speaker說一句問候的話。

3、AOP

AOP全稱是Aspect-Oriented Programming,中文翻譯是面向方面的程式設計或者面向切面的程式設計。你應該熟悉面向過程的程式設計,面向物件的程式設計,但是面向切面的程式設計你也許是第一次聽說。其實這些概念聽起來很玄,說到底也就是一句話的事情。

現在的系統往往強調減小模組之間的耦合度,AOP技術就是用來幫助實現這一目標的。舉例來說,假如上文的Greeting系統含有日誌模組,安全模組,事務管理模組,那麼每一次greet的時候,都會有這三個模組參與,以日誌模組為例,每次greet之後,都要記錄下greet的內容。而對於Speaker或者Greeting物件來說,它們並不知道自己的行為被記錄下來了,它們還是像以前一樣的工作,並沒有任何區別。只是容器控制了日誌行為。如果這裡你有點糊塗,沒關係,等講到具體Spring配置和實現的時候你就明白了。

假如我們現在為Greeting系統加入一個Valediction功能,那麼AOP模式的系統結構如下:

GRETTING

VALEDITION

日誌 安全 事務

這些模組是貫穿在整個系統中的,為系統的不同的功能提供服務,可以稱每個模組是一個“切面”。其實“切面”是一種抽象,把系統不同部分的公共行為抽取出來形成一個獨立的模組,並且在適當的地方(也就是切入點,後文會解釋)把這些被抽取出來的功能再插入系統的不同部分。

從某種角度上來講“切面”是一個非常形象的描述,它好像在系統的功能之上橫切一刀,要想讓系統的功能繼續,就必須先過了這個切面。這些切面監視並攔截系統的行為,在某些(被指定的)行為執行之前或之後執行一些附加的任務(比如記錄日誌)。而系統的功能流程(比如Greeting)並不知道這些切面的存在,更不依賴於這些切面,這樣就降低了系統模組之間的耦合度。

三、Spring初體驗

這一節我用一個具體的例子Greeting,來說明使用Spring開發的一般流程和方法,以及Spring配置檔案的寫法。

首先建立一個Speaker類,你可以把這個類看做是POJO。

程式碼片段5:

publicclass Speaker  

    
publicvoid sayHello() 
    

        System.out.println(
"Hello!"); 
    }
 
}


再建立一個Greeting類。

程式碼片段6:

publicclass Greeting  

    
private Speaker speaker; 
    
publicvoid setSpeaker(Speaker speaker) 
    

        
this.speaker = speaker; 
    }
 
    
publicvoid greet() 
    

        speaker.sayHello(); 
    }
 
}


然後要建立一個Spring的配置檔案把這兩個類關聯起來。

程式碼片段7(applicationContext.xml):

<bean id="Speaker" class="Speaker"> 
    
<bean id="Greeting" class="Greeting"> 
        
<property name="speaker"> 
            
<ref bean="Speaker"/>
        
</property>
   
</bean>

要用Spring Framework必須把Spring的包加入到Classpath中,我用的是Eclipse+MyEclipse,這些工作是自動完成的。推薦用Spring的配置檔案編輯器來編輯,純手工編寫很容易出錯。我先分析一下這個xml檔案的結構,然後再做測試。從節點開始,先聲明瞭兩個,第二個bean有一個speaker屬性(property)要求被注入,注入的內容是另外一個bean Speaker。這裡的命名是符合JavaBean規範的,也就是說如果是speaker屬性,那麼Spring容器就會呼叫setSpeaker()來注入這個屬性。是reference的意思,表示引用另外一個bean。

下面看一段簡單的測試程式碼:

程式碼片段8:

publicstaticvoid main(String[] args)  

        ApplicationContext context 
=  
            New ClassPathXmlApplicationContext(
"applicationContext.xml"); 
        Greeting greeting 
= (Greeting)context.getBean("Greeting"); 
        greeting.greet(); 
}

這段程式碼很簡單,如果你上文都看懂了,那麼這裡應該沒有問題。值得注意的是Spring有兩種方式來建立容器(我們不再用上文我們自己編寫的Container),一種是ApplicationContext,另外一種是BeanFactory。ApplicationContext更強大一些,而且使用上兩者沒有太大區別,所以一般說來都用ApplicationContext。Spring容器幫助我們維護我們在配置檔案中宣告的Bean以及它們之間的依賴關係,我們的Bean只需要關注自己的核心業務。

四、面向介面的程式設計

看了這麼多,也許你並沒有覺得Spring給開發帶來了很多便利。那是因為我舉的例子還不能突出Spring的優越之處,接下來我將通過介面程式設計來體現Spring的強大。

假如現在要求擴充套件Greeting的功能,要讓Speaker用不同的語言來問候,也就是說有不同的Speaker,比如ChineseSpeaker, EnglishSpeaker。那麼對上文提到的三種編碼方式(程式碼片段2、3、4)分別加以修改,你會發現很麻煩。假如下次又要加入一個西班牙語,又得重複勞動。很自然的會考慮到使用一個ISpeaker介面來簡化工作,,更改後的程式碼如下(這裡沒有列出介面的相關程式碼,我想你應該明白怎麼寫):

程式碼片段9:

publicvoid greet() 

ISpeaker s 
=new ChineseSpeaker(); 
s.sayHello(); 
}

程式碼片段10:

publicvoid greet() 

ISpeaker s 
= (ISpeaker)context.lookup("ejb/ChineseSpeaker"); 
s.sayHello(); 
}

程式碼片段11:

publicclass Greeting  

    
public ISpeaker s; 
    
public Greet(ISpeaker s) 
    

        
this.s = s; 
    }
 
    
publicvoid greet() 
    

        s.sayHello(); 
    }
 
}

對比三段程式碼,你會發現,第一種方法還是把具體的Speaker硬編碼到程式碼中了,第二中方法稍微好一點,但是沒有本質改變,而第三種方法就不一樣了,程式碼中並沒有關於具體Speaker的資訊。也就是說,如果下次還有什麼改動的話,第三種方法的Greeting類是不需要修改,編譯的。根據上文Spring的使用介紹,只需要改動xml檔案就能給Greeting注入不同的Speaker了,這樣程式碼的擴充套件性是不是提高了很多?

關於Spring的介面程式設計還有很多東西可以去挖掘,後文還會提到有關Spring Proxy的介面程式設計,我這裡先介紹這麼多,有興趣話可以去google更多的資料。

五、應用Spring中的切面

Spring生來支援AOP,首先來看幾個概念:

1、切面(Aspect):切面是系統中抽象出來的的某一個功能模組,上文已經有過介紹,這裡不再多說。

2、通知(Advice):通知是切面的具體實現。也就是說你的切面要完成什麼功能,具體怎麼做就是在通知裡面完成的。這個名稱似乎有點讓人費解,等後面看了程式碼就明白了。

3、切入點(Pointcut):切入點定義了通知應該應用到系統的哪些地方。Spring只能控制到方法(有的AOP框架可以控制到屬性),也就是說你能在方法呼叫之前或者之後選擇切入,執行額外的操作。

4、目標物件(Target):目標物件是被通知的物件。它可以是任何類,包括你自己編寫的或者第三方類。有了AOP以後,目標物件就只需要關注自己的核心業務,其他的功能,比如日誌,就由AOP框架支援完成。

5、代理(Proxy):簡單的講,代理就是將通知應用到目標物件後產生的物件。Spring在執行時會給每個目標物件生成一個代理物件,以後所有對目標物件的操作都會通過代理物件來完成。只有這樣通知才可能切入目標物件。對系統的其他部分來說,這個過程是透明的,也就是看起來跟沒用代理一樣。

我為了簡化,只介紹這5個概念。通過這幾個概念應該能夠理解Spring的切面程式設計了。如果需要深入瞭解Spring AOP的話再去學習其他概念也很快的。

下面通過一個實際的例子來說明Spring的切面程式設計。繼續上文Greeting的例子,我們想在Speaker每次說話之前記錄Speaker被呼叫了。

首先建立一個LogAdvice類:

程式碼片段12:

publicclass LogAdvice implements MethodBeforeAdvice  

    
publicvoid before(Method arg0, Object[] arg1, Object arg2)throws Throwable  
    

        System.out.println(
"Speaker called!"); 
    }
 
}

這裡涉及到一個類,MethodBeforeAdvice,這個類是Spring類庫提供的,類似的還有AfterReturningAdvice等等,從字面就能理解它們的含義。先不急著理解這個類,我稍後解釋。我們繼續看如何把這個類應用到我們的系統中去。

程式碼片段13:

<bean id="Speaker" class="Speaker"/> 
    
<bean id="Greeting" class="Greeting"> 
        
<property name="speaker"> 
            
<ref bean="SpeakerProxy"/> 
    
<bean id="LogAdvice" class="LogAdvice"/> 
    
<bean id="SpeakerProxy" class="org.springframework.aop.framework.ProxyFactoryBean"> 
        
<property name="proxyInterfaces"> 
            ISpeaker 
        
<property name="interceptorNames"> 
                LogAdvice 
        
<property name="target"> 
            
<ref local="Speaker"/>

可以看到我們的配置檔案中多了兩個bean,一個LogAdvice,另外一個SpeakerProxy。LogAdvice很簡單。我著重分析一下SpeakerProxy。這個Bean實際上是由Spring提供的ProxyFactoryBean實現。下面定義了三個依賴注入的屬性。

1、proxyInterfactes:這個屬性定義了這個Proxy要實現哪些介面,可以是一個,也可以是多個(多個的話,要用list標籤)。我前面講過Proxy是在執行是動態建立的,那麼這個屬性就告訴Spring建立這個Proxy的時候實現哪些介面。

2、interceptorNames:這個屬性定義了Proxy被切入了哪些通知,這裡只有一個LogAdvice。

3、target:這個屬性定義了被代理的物件。在這個例子中target是Speaker。

這樣的定義實際上約束了被代理的物件必須實現一個介面,這與上文講的面向介面的程式設計有點類似。其實可以這樣理解,介面的定義可以讓系統的其他部分不受影響,以前用ISpeaker介面來呼叫,現在加入了Proxy還是一樣的。但實際上內容已經不一樣了,以前是Speaker,現在是一個Proxy。而target屬性讓proxy知道具體的方法實現在哪裡。Proxy可以看作是target的一個包裝。當然Spring並沒有強制要求用介面,通過CGLIB(一個高效的程式碼生成開源類庫)也可以直接根據目標物件生成子類,但這種方式並不推薦。

我們還像以前一樣的測試我們的Greeting系統,測試程式碼和程式碼片段8是一樣的。執行結果如下:

Speaker called!

Hello!

看到效果了吧!而且你可以發現,我們加入Log功能並沒有改變以前的程式碼,甚至測試程式碼都沒有改變,這就是AOP的魅力所在!我們更改的只是配置檔案。

下面解釋一下剛才落下的MethodBeforeAdvice。關於這個類我並不詳細介紹,因為這涉及到Spring中的另外一個概念“連線點(Jointpoint)”,我詳細介紹一個before這個方法。這個方法有三個引數arg0表示目標物件在哪個點被切入了,既然是MethodBeforeAdvice,那當然是在Method之前被切入了。那麼arg0就是表示的那個Method。第二個引數arg1是Method的引數,所以型別是Object[]。第三個引數就是目標物件了,在Greeting例子中arg2的型別實際上是Speaker。

在Greeting例子中,我們並沒有指定目標物件的哪些方法要被切入,而是預設切入所有方法呼叫(雖然Speaker只有一個方法)。通過自定義Pointcut,可以控制切入點,我這裡不再介紹了,因為這並不影響理解Spring AOP,有興趣的話去google一下就知道了。