?
最近研究Spring,她包含的編程思想讓我耳目一新。所以寫下這篇入門級(jí)文章供新手參考。我不是什么Spring的資深研究人員,我只是現(xiàn)學(xué)現(xiàn)賣。所以文章也只能是膚淺單薄,錯(cuò)誤難免,還請(qǐng)見諒。
一、??? Spring誕生
Spring是一個(gè)開源框架,目前在開源社區(qū)的人氣很旺,被認(rèn)為是最有前途的開源框架之一。她是由Rod Johnson創(chuàng)建的,她的誕生是為了簡(jiǎn)化企業(yè)級(jí)系統(tǒng)的開發(fā)。說道Spring就不得不說EJB,因?yàn)镾pring在某種意義上是EJB的替代品,她是一種輕量級(jí)的容器。用過EJB的人都知道EJB很復(fù)雜,為了一個(gè)簡(jiǎn)單的功能你不得不編寫多個(gè)Java文件和部署文件,他是一種重量級(jí)的容器。也許你不了解EJB,你可能對(duì)“輕(重)量級(jí)”和“容器”比較陌生,那么這里我簡(jiǎn)單介紹一下。
1、什么是容器
“容器”,這個(gè)概念困擾我好久。從學(xué)習(xí)Tomcat開始就一直對(duì)此感到困惑。感性的來(lái)講,容器就是可以用來(lái)裝東西的物品。那么在編程領(lǐng)域就是指用來(lái)裝對(duì)象(OO的思想,如果你連OO都不了解,建議你去學(xué)習(xí)OO先)的對(duì)象。然而這個(gè)對(duì)象比較特別,它不僅要容納其他對(duì)象,還要維護(hù)各個(gè)對(duì)象之間的關(guān)系。這么講可能還是太抽象,來(lái)看一個(gè)簡(jiǎn)單的例子:
代碼片斷1:
public class Container?
{
??? public void init()
??? {
??? Speaker s = new Speaker();
??? Greeting g = new Greeting(s);
??? }
}
可以看到這里的Container類(容器)在初始化的時(shí)候會(huì)生成一個(gè)Speaker對(duì)象和一個(gè)Greeting對(duì)象,并且維持了它們的關(guān)系,當(dāng)系統(tǒng)要用這些對(duì)象的時(shí)候,直接問容器要就可以了。這就是容器最基本的功能,維護(hù)系統(tǒng)中的實(shí)例(對(duì)象)。如果到這里你還是感到模糊的話,別擔(dān)心,我后面還會(huì)有相關(guān)的解釋。
2、輕量級(jí)與重量級(jí)
所謂“重量級(jí)”是相對(duì)于“輕量級(jí)”來(lái)講的,也可以說“輕量級(jí)”是相對(duì)于重量級(jí)來(lái)講的。在Spring出現(xiàn)之前,企業(yè)級(jí)開發(fā)一般都采用EJB,因?yàn)樗峁┑氖聞?wù)管理,聲明式事務(wù)支持,持久化,分布計(jì)算等等都“簡(jiǎn)化”了企業(yè)級(jí)應(yīng)用的開發(fā)。我這里的“簡(jiǎn)化”打了雙引號(hào),因?yàn)檫@是相對(duì)的。重量級(jí)容器是一種入侵式的,也就是說你要用EJB提供的功能就必須在你的代碼中體現(xiàn)出來(lái)你使用的是EJB,比如繼承一個(gè)接口,聲明一個(gè)成員變量。這樣就把你的代碼綁定在EJB技術(shù)上了,而且EJB需要JBOSS這樣的容器支持,所以稱之為“重量級(jí)”。
相對(duì)而言“輕量級(jí)”就是非入侵式的,用Spring開發(fā)的系統(tǒng)中的類不需要依賴Spring中的類,不需要容器支持(當(dāng)然Spring本身是一個(gè)容器),而且Spring的大小和運(yùn)行開支都很微量。一般來(lái)說,如果系統(tǒng)不需要分布計(jì)算或者聲明式事務(wù)支持那么Spring是一個(gè)更好的選擇。
二、??? 幾個(gè)核心概念
在我看來(lái)Spring的核心就是兩個(gè)概念,反向控制(IoC),面向切面編程(AOP)。還有一個(gè)相關(guān)的概念是POJO,我也會(huì)略帶介紹。
1、POJO
我所看到過的POJO全稱有兩個(gè),Plain Ordinary Java Object,Plain Old Java Object,兩個(gè)差不多,意思都是普通的Java類,所以也不用去管誰(shuí)對(duì)誰(shuí)錯(cuò)。POJO可以看做是簡(jiǎn)單的JavaBean(具有一系列Getter,Setter方法的類)。嚴(yán)格區(qū)分這里面的概念沒有太大意義,了解一下就行。
2、??? IoC
IoC的全稱是Inversion of Control,中文翻譯反向控制或者逆向控制。這里的反向是相對(duì)EJB來(lái)講的。EJB使用JNDI來(lái)查找需要的對(duì)象,是主動(dòng)的,而Spring是把依賴的對(duì)象注入給相應(yīng)的類(這里涉及到另外一個(gè)概念“依賴注入”,稍后解釋),是被動(dòng)的,所以稱之為“反向”。先看一段代碼,這里的區(qū)別就很容易理解了。
代碼片段2:
public void greet()
{
Speaker s = new Speaker();
s.sayHello();
}
代碼片段3:
public void greet()
{
Speaker s = (Speaker)context.lookup("ejb/Speaker");
s.sayHello();
}
代碼片段4:
public class Greeting?
{
??? public Speaker s;
??? public Greeting(Speaker s)
??? {
??????? this.s = s;
??? }
??? public void greet()
??? {
??????? s.sayHello();
??? }
}
我們可以對(duì)比一下這三段代碼。其中片段2是不用容器的編碼,片段3是EJB編碼,片段4是Spring編碼。結(jié)合代碼片段1,你能看出來(lái)Spring編碼的優(yōu)越之處嗎?也許你會(huì)覺得Spring的編碼是最復(fù)雜的。不過沒關(guān)系,我在后面會(huì)解釋Spring編碼的好處。
這里我想先解釋一下“依賴注入”。根據(jù)我給的例子可以看出,Greeting類依賴Speaker類。片段2和片段3都是主動(dòng)的去獲取Speaker,雖然獲取的方式不同。但是片段4并沒有去獲取或者實(shí)例化Speaker類,而是在greeting函數(shù)中直接使用了s。你也許很容易就發(fā)現(xiàn)了,在構(gòu)造函數(shù)中有一個(gè)s被注入(可能你平時(shí)用的是,傳入)。在哪里注入的呢?請(qǐng)回頭看一下代碼片段1,這就是使用容器的好處,由容器來(lái)維護(hù)各個(gè)類之間的依賴關(guān)系(一般通過Setter來(lái)注入依賴,而不是構(gòu)造函數(shù),我這里是為了簡(jiǎn)化示例代碼)。Greeting并不需要關(guān)心Speaker是哪里來(lái)的或是從哪里獲得Speaker,只需要關(guān)注自己分內(nèi)的事情,也就是讓Speaker說一句問候的話。
3、??? AOP
AOP全稱是Aspect-Oriented Programming,中文翻譯是面向方面的編程或者面向切面的編程。你應(yīng)該熟悉面向過程的編程,面向?qū)ο蟮木幊?,但是面向切面的編程你也許是第一次聽說。其實(shí)這些概念聽起來(lái)很玄,說到底也就是一句話的事情。
現(xiàn)在的系統(tǒng)往往強(qiáng)調(diào)減小模塊之間的耦合度,AOP技術(shù)就是用來(lái)幫助實(shí)現(xiàn)這一目標(biāo)的。舉例來(lái)說,假如上文的Greeting系統(tǒng)含有日志模塊,安全模塊,事務(wù)管理模塊,那么每一次greet的時(shí)候,都會(huì)有這三個(gè)模塊參與,以日志模塊為例,每次greet之后,都要記錄下greet的內(nèi)容。而對(duì)于Speaker或者Greeting對(duì)象來(lái)說,它們并不知道自己的行為被記錄下來(lái)了,它們還是像以前一樣的工作,并沒有任何區(qū)別。只是容器控制了日志行為。如果這里你有點(diǎn)糊涂,沒關(guān)系,等講到具體Spring配置和實(shí)現(xiàn)的時(shí)候你就明白了。
假如我們現(xiàn)在為Greeting系統(tǒng)加入一個(gè)Valediction功能,那么AOP模式的系統(tǒng)結(jié)構(gòu)如下:
G|RET|TIN|G
V|ALE|DIT|ION
|?? |?? |
日志 安全 事務(wù)
這些模塊是貫穿在整個(gè)系統(tǒng)中的,為系統(tǒng)的不同的功能提供服務(wù),可以稱每個(gè)模塊是一個(gè)“切面”。其實(shí)“切面”是一種抽象,把系統(tǒng)不同部分的公共行為抽取出來(lái)形成一個(gè)獨(dú)立的模塊,并且在適當(dāng)?shù)牡胤剑ㄒ簿褪乔腥朦c(diǎn),后文會(huì)解釋)把這些被抽取出來(lái)的功能再插入系統(tǒng)的不同部分。
從某種角度上來(lái)講“切面”是一個(gè)非常形象的描述,它好像在系統(tǒng)的功能之上橫切一刀,要想讓系統(tǒng)的功能繼續(xù),就必須先過了這個(gè)切面。這些切面監(jiān)視并攔截系統(tǒng)的行為,在某些(被指定的)行為執(zhí)行之前或之后執(zhí)行一些附加的任務(wù)(比如記錄日志)。而系統(tǒng)的功能流程(比如Greeting)并不知道這些切面的存在,更不依賴于這些切面,這樣就降低了系統(tǒng)模塊之間的耦合度。
三、??? Spring初體驗(yàn)
這一節(jié)我用一個(gè)具體的例子Greeting,來(lái)說明使用Spring開發(fā)的一般流程和方法,以及Spring配置文件的寫法。
首先創(chuàng)建一個(gè)Speaker類,你可以把這個(gè)類看做是POJO。
代碼片段5:
public class Speaker?
{
??? public void sayHello()
??? {
??????? System.out.println("Hello!");
??? }
}
再創(chuàng)建一個(gè)Greeting類。
代碼片段6:
public class Greeting?
{
??? private Speaker speaker;
??? public void setSpeaker(Speaker speaker)
??? {
??????? this.speaker = speaker;
??? }
??? public void greet()
??? {
??????? speaker.sayHello();
??? }
}
然后要?jiǎng)?chuàng)建一個(gè)Spring的配置文件把這兩個(gè)類關(guān)聯(lián)起來(lái)。
代碼片段7(applicationContext.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="Speaker" class="Speaker"></bean>
??? <bean id="Greeting" class="Greeting">
??????? <property name="speaker">
??????????? <ref bean="Speaker"/>
??????? </property>
??? </bean>
</beans>
要用Spring Framework必須把Spring的包加入到Classpath中,我用的是Eclipse+MyEclipse,這些工作是自動(dòng)完成的。推薦用Spring的配置文件編輯器來(lái)編輯,純手工編寫很容易出錯(cuò)。我先分析一下這個(gè)xml文件的結(jié)構(gòu),然后再做測(cè)試。從<beans>節(jié)點(diǎn)開始,先聲明了兩個(gè)<bean>,第二個(gè)bean有一個(gè)speaker屬性(property)要求被注入,注入的內(nèi)容是另外一個(gè)bean Speaker。這里的命名是符合JavaBean規(guī)范的,也就是說如果是speaker屬性,那么Spring容器就會(huì)調(diào)用setSpeaker()來(lái)注入這個(gè)屬性。<ref>是reference的意思,表示引用另外一個(gè)bean。
下面看一段簡(jiǎn)單的測(cè)試代碼:
代碼片段8:
public static void main(String[] args)?
{
??????? ApplicationContext context =?
??????????? New ClassPathXmlApplicationContext("applicationContext.xml");
??????? Greeting greeting = (Greeting)context.getBean("Greeting");
??????? greeting.greet();
}
這段代碼很簡(jiǎn)單,如果你上文都看懂了,那么這里應(yīng)該沒有問題。值得注意的是Spring有兩種方式來(lái)創(chuàng)建容器(我們不再用上文我們自己編寫的Container),一種是ApplicationContext,另外一種是BeanFactory。ApplicationContext更強(qiáng)大一些,而且使用上兩者沒有太大區(qū)別,所以一般說來(lái)都用ApplicationContext。Spring容器幫助我們維護(hù)我們?cè)谂渲梦募新暶鞯腂ean以及它們之間的依賴關(guān)系,我們的Bean只需要關(guān)注自己的核心業(yè)務(wù)。
四、??? 面向接口的編程
看了這么多,也許你并沒有覺得Spring給開發(fā)帶來(lái)了很多便利。那是因?yàn)槲遗e的例子還不能突出Spring的優(yōu)越之處,接下來(lái)我將通過接口編程來(lái)體現(xiàn)Spring的強(qiáng)大。
假如現(xiàn)在要求擴(kuò)展Greeting的功能,要讓Speaker用不同的語(yǔ)言來(lái)問候,也就是說有不同的Speaker,比如ChineseSpeaker, EnglishSpeaker。那么對(duì)上文提到的三種編碼方式(代碼片段2、3、4)分別加以修改,你會(huì)發(fā)現(xiàn)很麻煩。假如下次又要加入一個(gè)西班牙語(yǔ),又得重復(fù)勞動(dòng)。很自然的會(huì)考慮到使用一個(gè)ISpeaker接口來(lái)簡(jiǎn)化工作,,更改后的代碼如下(這里沒有列出接口的相關(guān)代碼,我想你應(yīng)該明白怎么寫):????????
代碼片段9:
public void greet()
{
ISpeaker s = new ChineseSpeaker();
s.sayHello();
}
代碼片段10:
public void greet()
{
ISpeaker s = (ISpeaker)context.lookup("ejb/ChineseSpeaker");
s.sayHello();
}
代碼片段11:
public class Greeting?
{
??? public ISpeaker s;
??? public Greet(ISpeaker s)
??? {
??????? this.s = s;
??? }
??? public void greet()
??? {
??????? s.sayHello();
??? }
}
對(duì)比三段代碼,你會(huì)發(fā)現(xiàn),第一種方法還是把具體的Speaker硬編碼到代碼中了,第二中方法稍微好一點(diǎn),但是沒有本質(zhì)改變,而第三種方法就不一樣了,代碼中并沒有關(guān)于具體Speaker的信息。也就是說,如果下次還有什么改動(dòng)的話,第三種方法的Greeting類是不需要修改,編譯的。根據(jù)上文Spring的使用介紹,只需要改動(dòng)xml文件就能給Greeting注入不同的Speaker了,這樣代碼的擴(kuò)展性是不是提高了很多?
關(guān)于Spring的接口編程還有很多東西可以去挖掘,后文還會(huì)提到有關(guān)Spring Proxy的接口編程,我這里先介紹這么多,有興趣話可以去google更多的資料。
五、??? 應(yīng)用Spring中的切面
Spring生來(lái)支持AOP,首先來(lái)看幾個(gè)概念:
1、??? 切面(Aspect):切面是系統(tǒng)中抽象出來(lái)的的某一個(gè)功能模塊,上文已經(jīng)有過介紹,這里不再多說。
2、??? 通知(Advice):通知是切面的具體實(shí)現(xiàn)。也就是說你的切面要完成什么功能,具體怎么做就是在通知里面完成的。這個(gè)名稱似乎有點(diǎn)讓人費(fèi)解,等后面看了代碼就明白了。
3、??? 切入點(diǎn)(Pointcut):切入點(diǎn)定義了通知應(yīng)該應(yīng)用到系統(tǒng)的哪些地方。Spring只能控制到方法(有的AOP框架可以控制到屬性),也就是說你能在方法調(diào)用之前或者之后選擇切入,執(zhí)行額外的操作。
4、??? 目標(biāo)對(duì)象(Target):目標(biāo)對(duì)象是被通知的對(duì)象。它可以是任何類,包括你自己編寫的或者第三方類。有了AOP以后,目標(biāo)對(duì)象就只需要關(guān)注自己的核心業(yè)務(wù),其他的功能,比如日志,就由AOP框架支持完成。
5、??? 代理(Proxy):簡(jiǎn)單的講,代理就是將通知應(yīng)用到目標(biāo)對(duì)象后產(chǎn)生的對(duì)象。Spring在運(yùn)行時(shí)會(huì)給每個(gè)目標(biāo)對(duì)象生成一個(gè)代理對(duì)象,以后所有對(duì)目標(biāo)對(duì)象的操作都會(huì)通過代理對(duì)象來(lái)完成。只有這樣通知才可能切入目標(biāo)對(duì)象。對(duì)系統(tǒng)的其他部分來(lái)說,這個(gè)過程是透明的,也就是看起來(lái)跟沒用代理一樣。
我為了簡(jiǎn)化,只介紹這5個(gè)概念。通過這幾個(gè)概念應(yīng)該能夠理解Spring的切面編程了。如果需要深入了解Spring AOP的話再去學(xué)習(xí)其他概念也很快的。
下面通過一個(gè)實(shí)際的例子來(lái)說明Spring的切面編程。繼續(xù)上文Greeting的例子,我們想在Speaker每次說話之前記錄Speaker被調(diào)用了。
首先創(chuàng)建一個(gè)LogAdvice類:
代碼片段12:
public class LogAdvice implements MethodBeforeAdvice?
{
??? public void before(Method arg0, Object[] arg1, Object arg2)throws Throwable?
??? {
??????? System.out.println("Speaker called!");
??? }
}
這里涉及到一個(gè)類,MethodBeforeAdvice,這個(gè)類是Spring類庫(kù)提供的,類似的還有AfterReturningAdvice等等,從字面就能理解它們的含義。先不急著理解這個(gè)類,我稍后解釋。我們繼續(xù)看如何把這個(gè)類應(yīng)用到我們的系統(tǒng)中去。
代碼片段13:
<beans>
??? <bean id="Speaker" class="Speaker"/>
??? <bean id="Greeting" class="Greeting">
??????? <property name="speaker">
??????????? <ref bean="SpeakerProxy"/>
??????? </property>
??? </bean>
??? <bean id="LogAdvice" class="LogAdvice"/>
??? <bean id="SpeakerProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
??????? <property name="proxyInterfaces">
??????????? <value>ISpeaker</value>
??????? </property>
??????? <property name="interceptorNames">
??????????? <list>
??????????????? <value>LogAdvice</value>
??????????? </list>
??????? </property>
??????? <property name="target">
??????????? <ref local="Speaker"/>
??????? </property>
??? </bean>
</beans>
可以看到我們的配置文件中多了兩個(gè)bean,一個(gè)LogAdvice,另外一個(gè)SpeakerProxy。LogAdvice很簡(jiǎn)單。我著重分析一下SpeakerProxy。這個(gè)Bean實(shí)際上是由Spring提供的ProxyFactoryBean實(shí)現(xiàn)。下面定義了三個(gè)依賴注入的屬性。
1、??? proxyInterfactes:這個(gè)屬性定義了這個(gè)Proxy要實(shí)現(xiàn)哪些接口,可以是一個(gè),也可以是多個(gè)(多個(gè)的話,要用list標(biāo)簽)。我前面講過Proxy是在運(yùn)行是動(dòng)態(tài)創(chuàng)建的,那么這個(gè)屬性就告訴Spring創(chuàng)建這個(gè)Proxy的時(shí)候?qū)崿F(xiàn)哪些接口。
2、??? interceptorNames:這個(gè)屬性定義了Proxy被切入了哪些通知,這里只有一個(gè)LogAdvice。
3、??? target:這個(gè)屬性定義了被代理的對(duì)象。在這個(gè)例子中target是Speaker。
這樣的定義實(shí)際上約束了被代理的對(duì)象必須實(shí)現(xiàn)一個(gè)接口,這與上文講的面向接口的編程有點(diǎn)類似。其實(shí)可以這樣理解,接口的定義可以讓系統(tǒng)的其他部分不受影響,以前用ISpeaker接口來(lái)調(diào)用,現(xiàn)在加入了Proxy還是一樣的。但實(shí)際上內(nèi)容已經(jīng)不一樣了,以前是Speaker,現(xiàn)在是一個(gè)Proxy。而target屬性讓proxy知道具體的方法實(shí)現(xiàn)在哪里。Proxy可以看作是target的一個(gè)包裝。當(dāng)然Spring并沒有強(qiáng)制要求用接口,通過CGLIB(一個(gè)高效的代碼生成開源類庫(kù))也可以直接根據(jù)目標(biāo)對(duì)象生成子類,但這種方式并不推薦。
我們還像以前一樣的測(cè)試我們的Greeting系統(tǒng),測(cè)試代碼和代碼片段8是一樣的。運(yùn)行結(jié)果如下:
Speaker called!
Hello!
看到效果了吧!而且你可以發(fā)現(xiàn),我們加入Log功能并沒有改變以前的代碼,甚至測(cè)試代碼都沒有改變,這就是AOP的魅力所在!我們更改的只是配置文件。
下面解釋一下剛才落下的MethodBeforeAdvice。關(guān)于這個(gè)類我并不詳細(xì)介紹,因?yàn)檫@涉及到Spring中的另外一個(gè)概念“連接點(diǎn)(Jointpoint)”,我詳細(xì)介紹一個(gè)before這個(gè)方法。這個(gè)方法有三個(gè)參數(shù)arg0表示目標(biāo)對(duì)象在哪個(gè)點(diǎn)被切入了,既然是MethodBeforeAdvice,那當(dāng)然是在Method之前被切入了。那么arg0就是表示的那個(gè)Method。第二個(gè)參數(shù)arg1是Method的參數(shù),所以類型是Object[]。第三個(gè)參數(shù)就是目標(biāo)對(duì)象了,在Greeting例子中arg2的類型實(shí)際上是Speaker。
在Greeting例子中,我們并沒有指定目標(biāo)對(duì)象的哪些方法要被切入,而是默認(rèn)切入所有方法調(diào)用(雖然Speaker只有一個(gè)方法)。通過自定義Pointcut,可以控制切入點(diǎn),我這里不再介紹了,因?yàn)檫@并不影響理解Spring AOP,有興趣的話去google一下就知道了。
六、實(shí)戰(zhàn)Spring
雖然這部分取名為“實(shí)戰(zhàn)Spring”,但實(shí)際上我并不打算在這里介紹實(shí)際開發(fā)Spring的內(nèi)容,因?yàn)槲覍戇@篇文章的目的是介紹Spring的概念和用Spring開發(fā)的思路,而不是有關(guān)Spring的實(shí)踐和詳細(xì)介紹。文中介紹的內(nèi)容和用Spring做實(shí)際開發(fā)還相去甚遠(yuǎn)。之所以取名“實(shí)戰(zhàn)Spring”是我覺得理解了上文講的內(nèi)容以后,可以開始為深入學(xué)習(xí)Spring和學(xué)習(xí)如何在項(xiàng)目中應(yīng)用Spring了。
要系統(tǒng)的學(xué)習(xí)Spring還是需要閱讀一本詳細(xì)介紹Spring的書,我推薦Spring in Action,因?yàn)槲铱吹木褪沁@本書。希望這篇文章能為你系統(tǒng)的學(xué)習(xí)Spring掃除一些障礙。