最近發現了一個不錯的spring介紹的文章,寫的很好,思路很清晰,容易理解IOC以及AOP的內容,并且也簡單明了的介紹了很多AOP的概念,摘抄下來以供分享
理解Spring
最近研究Spring,她包含的編程思想讓我耳目一新。所以寫下這篇入門級文章供新手參考。我不是什么Spring的資深研究人員,我只是現學現賣。所以文章也只能是膚淺單薄,錯誤難免,還請見諒。
一、 Spring誕生
Spring
是一個開源框架,目前在開源社區的人氣很旺,被認為是最有前途的開源框架之一。她是由Rod Johnson創建的,她的誕生是為了簡化企業級系統的開
發。說道Spring就不得不說EJB,因為Spring在某種意義上是EJB的替代品,她是一種輕量級的容器。用過EJB的人都知道EJB很復雜,為了
一個簡單的功能你不得不編寫多個Java文件和部署文件,他是一種重量級的容器。也許你不了解EJB,你可能對“輕(重)量級”和“容器”比較陌生,那么
這里我簡單介紹一下。
1、什么是容器
“容器”,這個概念困擾我好久。從學習Tomcat開始就一直對此感到困惑。感性的來講,容器就是可
以用來裝東西的物品。那么在編程領域就是指用來裝對象(OO的思想,如果你連OO都不了解,建議你去學習OO先)的對象。然而這個對象比較特別,它不僅要
容納其他對象,還要維護各個對象之間的關系。這么講可能還是太抽象,來看一個簡單的例子:
代碼片斷1:
1. public class Container
2. {
3. public void init()
4. {
5. Speaker s = new Speaker();
6. Greeting g = new Greeting(s);
7. }
8. }
可以看到這里的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:
1. public void greet()
2. {
3. Speaker s = new Speaker();
4. s.sayHello();
5. }
代碼片段3:
1. public void greet()
2. {
3. Speaker s = (Speaker)context.lookup("ejb/Speaker");
4. s.sayHello();
5. }
代碼片段4:
1. public class Greeting
2. {
3. public Speaker s;
4. public Greeting(Speaker s)
5. {
6. this.s = s;
7. }
8. public void greet()
9. {
10. s.sayHello();
11. }
12. }
我們可以對比一下這三段代碼。其中片段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模式的系統結構如下:
G|RET|TIN|G
V|ALE|DIT|ION
| | |
日志 安全 事務
這些模塊是貫穿在整個系統中的,為系統的不同的功能提供服務,可以稱每個模塊是一個“切面”。其實“切面”是一種抽象,把系統不同部分的公共行為抽取出來形成一個獨立的模塊,并且在適當的地方(也就是切入點,后文會解釋)把這些被抽取出來的功能再插入系統的不同部分。
從
某種角度上來講“切面”是一個非常形象的描述,它好像在系統的功能之上橫切一刀,要想讓系統的功能繼續,就必須先過了這個切面。這些切面監視并攔截系統的
行為,在某些(被指定的)行為執行之前或之后執行一些附加的任務(比如記錄日志)。而系統的功能流程(比如Greeting)并不知道這些切面的存在,更
不依賴于這些切面,這樣就降低了系統模塊之間的耦合度。
三、 Spring初體驗
這一節我用一個具體的例子Greeting,來說明使用Spring開發的一般流程和方法,以及Spring配置文件的寫法。
首先創建一個Speaker類,你可以把這個類看做是POJO。
代碼片段5:
1. public class Speaker
2. {
3. public void sayHello()
4. {
5. System.out.println("Hello!");
6. }
7. }
再創建一個Greeting類。
代碼片段6:
1. public class Greeting
2. {
3. private Speaker speaker;
4. public void setSpeaker(Speaker speaker)
5. {
6. this.speaker = speaker;
7. }
8. public void greet()
9. {
10. speaker.sayHello();
11. }
12. }
然后要創建一個Spring的配置文件把這兩個類關聯起來。
代碼片段7(applicationContext.xml):
1. <?xml version="1.0" encoding="UTF-8"?>
2. <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN"
3. "http://www.springframework.org/dtd/spring-beans.dtd">
4.
5. <beans>
6. <bean id="Speaker" class="Speaker"></bean>
7. <bean id="Greeting" class="Greeting">
8. <property name="speaker">
9. <ref bean="Speaker"/>
10. </property>
11. </bean>
12. </beans>
要用Spring Framework必須把Spring的包加入到Classpath中,我用的是
Eclipse+MyEclipse,這些工作是自動完成的。推薦用Spring的配置文件編輯器來編輯,純手工編寫很容易出錯。我先分析一下這個xml
文件的結構,然后再做測試。從<beans>節點開始,先聲明了兩個<bean>,第二個bean有一個speaker屬性
(property)要求被注入,注入的內容是另外一個bean Speaker。這里的命名是符合JavaBean規范的,也就是說如果是
speaker屬性,那么Spring容器就會調用setSpeaker()來注入這個屬性。<ref>是reference的意思,表示引
用另外一個bean。
下面看一段簡單的測試代碼:
代碼片段8:
1. public static void main(String[] args)
2. {
3. ApplicationContext context =
4. New ClassPathXmlApplicationContext("applicationContext.xml");
5. Greeting greeting = (Greeting)context.getBean("Greeting");
6. greeting.greet();
7. }
這段代碼很簡單,如果你上文都看懂了,那么這里應該沒有問題。值得注意的是Spring有兩種方式來創建容器
(我們不再用上文我們自己編寫的Container),一種是ApplicationContext,另外一種是BeanFactory。
ApplicationContext更強大一些,而且使用上兩者沒有太大區別,所以一般說來都用ApplicationContext。Spring容
器幫助我們維護我們在配置文件中聲明的Bean以及它們之間的依賴關系,我們的Bean只需要關注自己的核心業務。
四、 面向接口的編程
看了這么多,也許你并沒有覺得Spring給開發帶來了很多便利。那是因為我舉的例子還不能突出Spring的優越之處,接下來我將通過接口編程來體現Spring的強大。
假
如現在要求擴展Greeting的功能,要讓Speaker用不同的語言來問候,也就是說有不同的Speaker,比如ChineseSpeaker,
EnglishSpeaker。那么對上文提到的三種編碼方式(代碼片段2、3、4)分別加以修改,你會發現很麻煩。假如下次又要加入一個西班牙語,又
得重復勞動。很自然的會考慮到使用一個ISpeaker接口來簡化工作,,更改后的代碼如下(這里沒有列出接口的相關代碼,我想你應該明白怎么寫):
代碼片段9:
1. public void greet()
2. {
3. ISpeaker s = new ChineseSpeaker();
4. s.sayHello();
5. }
代碼片段10:
1. public void greet()
2. {
3. ISpeaker s = (ISpeaker)context.lookup("ejb/ChineseSpeaker");
4. s.sayHello();
5. }
代碼片段11:
1. public class Greeting
2. {
3. public ISpeaker s;
4. public Greet(ISpeaker s)
5. {
6. this.s = s;
7. }
8. public void greet()
9. {
10. s.sayHello();
11. }
12. }
對比三段代碼,你會發現,第一種方法還是把具體的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:
1. public class LogAdvice implements MethodBeforeAdvice
2. {
3. public void before(Method arg0, Object[] arg1, Object arg2)throws Throwable
4. {
5. System.out.println("Speaker called!");
6. }
7. }
這里涉及到一個類,MethodBeforeAdvice,這個類是Spring類庫提供的,類似的還有AfterReturningAdvice等等,從字面就能理解它們的含義。先不急著理解這個類,我稍后解釋。我們繼續看如何把這個類應用到我們的系統中去。
代碼片段13:
1. <beans>
2. <bean id="Speaker" class="Speaker"/>
3. <bean id="Greeting" class="Greeting">
4. <property name="speaker">
5. <ref bean="SpeakerProxy"/>
6. </property>
7. </bean>
8. <bean id="LogAdvice" class="LogAdvice"/>
9. <bean id="SpeakerProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
10. <property name="proxyInterfaces">
11. <value>ISpeaker</value>
12. </property>
13. <property name="interceptorNames">
14. <list>
15. <value>LogAdvice</value>
16. </list>
17. </property>
18. <property name="target">
19. <ref local="Speaker"/>
20. </property>
21. </bean>
22. </beans>
可以看到我們的配置文件中多了兩個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一下就知道了。
posted on 2009-07-15 13:34
acerbic coffee 閱讀(262)
評論(0) 編輯 收藏