?
?
程序員的春天來了!在這章中,您將開始接觸Spring,學習Spring基礎知識。并將看到Spring在實現OCP原則上所做的努力,接觸到為實現OCP原則所產生的兩個設計模式:DI依賴及IoC控制反轉。此外,在最后,您還將學習到Spring在使用時應注意的問題。
什么是Spring以及使用它的意義
Spring框架十分受歡迎,并且發展迅速。其成功原因很大程度上源于它的設計思想。Spring框架的核心思想是 IoC及DI 。用句簡單的話來解釋,就是讓程序的各個組件之間獨立開來,每個零部件只要接口規格一致,就可以自由更換。這就使得應用了Spring框架的項目變得非常靈活。當然,如果僅僅只有這點優勢,Spring也不會取得如此大的成功。除了使用了很好的設計思想,Spring還提供一套非常完整且實用的工具箱,并且這套工具箱秉承一種哲學:別人做得好的,直接拿來用,并在上層提供更方便的功能;別人做得不好的,自已做,并做到最好。因為使用了IoC及DI模式,Spring框架的各個部件都可以自由更換,因此它可以隨時調整,保持每一個工具都十分優秀。
Spring既然被稱為框架(Framework),就說明它包含了您在開發過程中要用到了各種工具,使Java開發人員有一整套可使用的工具包,不必再從輪子造起。在下圖中展示了Spring提供的工具箱:
除了IoC核心以外,Spring在上層提供的工具涉及到J2EE領域的方方面面。在本文的后續部分,會陸續介紹DAO、ORM、JEE、WEB、AOP這幾塊的使用。
什么是IoC及DI
我們已經學習了OCP法則,那么如何才能保證您的代碼遵守了OCP法則呢? Martin Fowler 提出了一種設計模式,叫做Dependency Injection,簡稱DI,中文叫做依賴注入。而通過使用這種模式實現了IoC,即Inversion Of Control,中文翻譯成控制反轉。通過這種模式,可以使項目符合OCP的設計原則。而Spring最核心的理念就是IoC及DI,并在這個設計思想上提供了一大套工具箱。名詞比較抽象,下面直接通過實例來看一下其具體含義。
Spring的基本使用方式
現在制作一個用戶聽收音機的程序,程序中有兩個類:用戶User,收音機Radio。主程序的邏輯為用戶使用收音機:
用戶有姓名及收音機兩個屬性。收音機有一個use()方法。下面來看具體實現:
?
?
package demo; public class Radio { public String use() { return "RADIO"; } }
下面是用戶類的實現1:
package demo; public class User { private String username; private Radio radio; public Radio getRadio() { return radio; } public void setRadio(Radio radio) { this.radio = radio; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } }
1 在講解OCP法則時,講過類的屬性要封裝,因此User類的username及radio屬性要聲明為private,而使用getter及setter方法去操作它們。 用戶類中有兩個屬性:姓名,收音機。現在開始實現業務邏輯,即用戶使用收音機:
package demo; public class UserPlayRadio { public static void main(String[] args) { User user = new User(); Radio radio = new Radio(); user.setUsername("Peter"); user.setRadio(radio); System.out.println(user.getUsername() + " is using " + user.getRadio().use()); } }
首先創建一個用戶,然后創建一個收音機,并把這個收音機交給用戶。最后,用戶取得收音機,并播放它。系統最終返回結果: Peter is using RADIO 這個設計似乎沒什么問題,可是沒過多久,客戶跑過來找您,說這個程序需要添加一個功能:User除了使用收音機以外,還要使用電視。這下麻煩了,這個程序中用戶包含的屬性固定是收音機。如果還要添加看電視機的功能,除去添加電視機類別Tv以外,還必須修改User類,在其中添加電視機的屬性。這樣太不合理了,用戶應該有能力去使用各種各樣的電器??墒堑搅舜a里,多了一個Tv,User類也必須隨之更改。在這種情況下,可以使用接口來優化設計,將User對Radio及可能的Tv類別的依賴關系調整過來。 首先定義一個接口類別:電器Appliance。這個接口規定一個功能:use(),即所有的電器都可以被使用: package demo; public interface Appliance { public String use(); } 接下來是改造User類別,將Radio屬性去掉,代之以接口Appliance:
package demo; public class UserV2 { private String username; private Appliance appliance; public Appliance getAppliance() { return appliance; } public void setAppliance(Appliance appliance) { this.appliance = appliance; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } }
這樣用戶并不知道它將使用什么樣的電器,即用戶對收音機的依賴關系被消除了,這種模式叫做’控制反轉’。下面需要做的是在運行時,把用戶要使用的Appliance實體類(Radio或Tv)通過set方法放進去,即依賴注入。不過現在先別急,還要實現一個新的類別:電視Tv。它實現Appliance接口。同樣地,改造已有的Radio類別,使之實現Applicance接口:
package demo; public class Tv implements Appliance { public String use() { return "TV"; } } package demo; public class RadioV2 implements Appliance{ public String use() { return "RADIO"; } }
工作還沒有完,業務邏輯’用戶使用收音機’也應該做出修改,變成’用戶使用電器’,至于具體使用什么電器,則由主程序在運行時注入,下面讓用戶分別使用收音機及電視機:
package demo; public class UserPlayAppliance { public static void main(String[] args) { UserV2 user = new UserV2(); user.setUsername("Peter"); RadioV2 radio = new RadioV2(); user.setAppliance(radio); System.out.println(user.getUsername() + " is using " + user.getAppliance().use()); Tv tv = new Tv(); user.setAppliance(tv); System.out.println(user.getUsername() + " is using " + user.getAppliance().use()); } }
跑起來玩一下,看看系統的輸出: Peter is using RADIO Peter is using TV 可以看到,用戶聽了收音機,也看了電視機。在代碼中,分別將Radio及Tv賦給user,并且user不關心是什么電器,都可以拿過來play。通過應用IoC及DI模式,使系統附合了OCP法則。User類既保持了它的使用開放性(可以使用新加的Tv類別),也保持了修改的閉合性(新加的Tv類不會導致User類的變化)2 2 還記得在OCP一節中介紹的口訣嗎?這里就是 層層之間用接口 的原則體現。 好的,原理講完了。那么,Spring到底能幫您干些什么呢?簡單來說,Spring最最核心的部分就是幫您把代碼里user.setAppliance(radio)這樣的工作給移到了配置文件中完成了。下面講講如何使用Spring。 首先需要在項目中制作一個Spring的配置文件,這個配置文件是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" xmlns:p="http://www.springframework.org/schema/p" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd"> <bean id="peter" class="demo.UserV2" p:username="Peter" p:appliance-ref='radio' /> <bean id="tom" class="demo.UserV2" p:username="Tom" p:appliance-ref='tv' /> <bean id="radio" class="demo.RadioV2" /> <bean id="tv" class="demo.Tv" /> </beans>
對于這個配置文件,其關鍵內容是聲明了一個名字為peter的bean: <bean id="peter" class="demo.UserV2" p:username="Peter" p:appliance-ref='radio' /> 這個bean為UserV2的實例。兩個p元素是將UserV2的兩個屬性username及appliance注入。配置文件中的’p:username’就相當于調用代碼的setUsername()進行屬性具體值的注入。Spring讀到這個配置文件時,就會調用UserV2中的setUsername方法把屬性注入,p:appliance也是一樣的道理。 此外,您可以發現,username及appliance的注入方式還稍有不同。由于username是一個String類型的屬性,因此我們把需要的值直接寫進去就可以了。這里我們指定用戶的名字為’Peter’。請注意字串類型的屬性值是用雙引號括起來的。對于appliance,由于這個屬性對應我們定義的Appliance接口,因此我們需要注入一個Appliance的實例。在Spring的配置當中,我們需要注入一個Appliance的bean。因此,在配置文件中我們定義了這個bean: <bean id="radio" class="demo.RadioV2" /> RadioV2是一個實現了Appliance接口的實體類,這個bean的id為radio。在peter中我們將radio這個bean注入到了appliance屬性當中。請注意,如果要引用其它的bean,需要在屬性后面加’-ref’,并用單引號引用bean的id: p:appliance-ref='radio' 同樣的道理,還聲明了一個`tom’的用戶,不同的是為這個用戶注入的電器是Tv而不是Radio。關于Spring配置文件的語法,相信您已經有了一個初步的認識,可能一開始還不習慣,我們隨著閱讀Spring的配置文件量不斷增加以及不斷地親自實踐,對它的語法就會慢慢習慣。 此外,配置文件的開始,是Spring的命名空間聲明。什么是命名空間呢?您可以把它理解為Java代碼中的package。XML文件中的命名空間聲明就相當于java代碼中對于的package的import。在Java代碼中,只有引用了相關的package,才能使用package中提供的類。同樣地,只有引入了相關的命名空間,才能使用命名空間中定義的XML元素。在我們的配置文件中,引入了最基本的bean元素,以及簡化配置語言的p元素: xmlns:p="http://www.springframework.org/schema/p" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd" 這樣,我們在配置文件中就可以使用bean。此外,我們在bean中便可以使用p元素來簡化配置。什么叫做’用p元素來簡化配置’呢?舉個例子您就明白了,比如下面這個聲明: <bean id="peter" class="demo.UserV2" p:username="Peter" p:appliance-ref='radio' /> 如果不用p元素,那么上面這個配置默認的Spring配置寫法應該是這樣的: <bean id="peter" class="demo.UserV2"> <property name="username" value="Peter" /> <property name="appliance" ref="radio" /> </bean> 怎么樣,是不是使用p元素后,配置文件簡單多了?在配置文件中完成了注入工作后,我們接下來看看如何使用’peter’及’tom’這兩個User,下面撰寫一個新的UserPlayAppliance類別:
package demo; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class UserPlayApplianceV2 { public static void main(String[] args) { ApplicationContext ctx = new ClassPathXmlApplicationContext "config.xml"); UserV2 peter = (UserV2) ctx.getBean("peter"); System.out.println(peter.getUsername() + " is using " + peter.getAppliance().use()); UserV2 tom = (UserV2) ctx.getBean("tom"); System.out.println(tom.getUsername() + " is using " + tom.getAppliance().use()); } }
在上面的代碼中,首先系統將配置文件 config.xml 讀進Spring的系統上下文類別 ApplicationContext 在這里使用了 ClassPathXmlApplicationContext 進行讀取工作,從名字就可以看得很清楚這個類從class路徑中讀取系統配置文件。除此以外,Spring還提供多種加載類,在后續文章中陸續介紹。 在第11行,從ApplicationContext的getBean()方法提取出配置好的用戶`peter’,此時Spring會幫您創建配置文件中的bean,即User、Radio,并按照配置,通過調用user的setAppliance()方法把radio注入。15-17行同樣道理。跑一下這個程序,看下系統輸出: Peter is using RADIO Tom is using TV 以上說明了Spring的基本使用方法及背后所蘊涵的設計思想。在下一節,您將看到如何使用Java 5的最新Annotation特性,簡化代碼及配置。 Property注入與Constructor注入 我們在前一節通過Spring的配置文件,把某個Bean的某個屬性通過此屬性的setter方法注入到這個Bean當中。這樣的注入方式在Spring稱做Property注入。例如: <bean id="tom" class="demo.UserV2" p:username="Tom" p:appliance-ref='tv' /> username與appliance是通過UserV2中的setter方法被注入的:
package demo; public class UserV2 { private String username; private Appliance appliance; ... public void setAppliance(Appliance appliance) { this.appliance = appliance; } public void setUsername(String username) { this.username = username; } }
除了Property注入以外,Spring還支持Constructor注入。即通過建構方法,在Class被創建時,把Bean注入。我們來看看這樣的方式。首先創建一個Bean,這個Bean在建構方法接收兩個屬性的注入:
package demo; public class Bean { private BeanA beanA; private BeanB beanB; public Bean(BeanA beanA, BeanB beanB) { super(); this.beanA = beanA; this.beanB = beanB; } }
下面我們在Spring配置文件中,通過Bean的建構方法把BeanA及BeanB注入: <bean id="bean" class="Bean"> <constructor-arg ref="beanA" /> <constructor-arg ref="beanB" /> </bean> <bean id="beanA" class="BeanA" /> <bean id="beanB" class="BeanB" /> 請注意, contructor-arg 的書寫順序要與建構方法中的參數順序一致。如果beanA,beanB在 constructor-arg 中的順序顛倒了,那么配置文件中的beanA將被注入Bean的beanB,配置文件中的beanB將被注入Bean的beanA,導致程序出錯,無法執行。 基于Constructor的注入與基于Property的方式在達到的效果和使用目的上沒什么區別。但大多數情況下,使用基于Property方法的注入可以使配置文件看起來更加清淅,也不涉及順序的問題。但使用Constructor方法也有它的好處,如果需要在某個類的建構方法中加入一些與注入息息相關的邏輯代碼時,將不得不采取這種方式。在這一方面沒什么原則可言。只要您多多使用,找到自己最順手的方式就可以了。在下一節當中,我們介紹另一種注入方式,也是Spring 2.5中的新方式:基于Java標記的Bean自動綁定。 使用Spring Annotation進行自動綁定 在Java 5中,Annotation特性被標準化,并且允許用戶定義自己的標記,Spring利用這一新特征,提供了一系列方便好用的Annotation。 回過頭再來看一下上一節的樣例。在配置文件中,有如下的聲明: <bean id="peter" class="demo.UserV2" p:username="Peter" p:appliance-ref='radio' /> 通過 p:appliance-ref=`radio' ,radio這個bean被注入到user中。為了配合注入,在User類中必須提供getter及setter方法,供Spring使用: private Appliance appliance; public Appliance getAppliance() { return appliance; } public void setAppliance(Appliance appliance) { this.appliance = appliance; } Spring從2.5版本開始支持Java Annotation簡化裝配流程。下面看一下使用標記的新配置文件及代碼。首先是User類別的修改:
package demo; import org.springframework.beans.factory.annotation.Autowired; public class UserV3 { private String username; @Autowired private Appliance appliance; public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public Appliance getAppliance() { return appliance; } }
首先是appliance的setter方法被省掉了。在第8行有一個Spring的標記@Autowired。這個標記的作用是將配置文件中,與Appliance同類別的bean自動加載進來??匆幌屡渲梦募?
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd"> <context:annotation-config /> <bean id="peter" class="demo.UserV3" p:username="Peter" /> <bean class="demo.RadioV2" /> </beans>
關于這個配置文件,有以下幾點要說: ?在第9行,新添了一個 context:annotation-config 的元素,這一標記告訴Spring,在代碼中使用了Annotation的特性。這樣代碼中的@Autowired標記才會生效。 ?從第11行可以看到,原來的 p:appliance-ref 的設置省去了。因為在代碼中appliance屬性上聲明了@Autowired標記,Spring會在配置中查找與appliance同類別的bean,并進行自動綁定。因此只需聲明下面的bean即可。 ?在第13行聲明了radio bean,它實現了appliance接口,因此會被自動綁定3}。bean的識別id也可以不寫了。 3 細心的讀者可能會問:如果有多個類都實現了appliance接口,Spring該如何得知綁綁定哪一個實現類?不必著急,請繼續往下看。 完成了配置,下面我們來看一下新的邏輯代碼:
package demo; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class UserPlayApplianceV3 { public static void main(String[] args) { ApplicationContext ctx = new ClassPathXmlApplicationContext( "configV2.xml"); UserV3 peter = (UserV3) ctx.getBean("peter"); System.out.println(peter.getUsername() + " is using " + peter.getAppliance().use()); } }
可以看到使用bean的方法沒有任何變化,底層的自動綁定并沒有影響前端的使用。系統輸出如下: Peter is using RADIO 此時會有另一種情況:如果配置文件中有兩個bean都實現了appliance接口該怎么辦?此時系統該綁定哪一個?答案是系統不能決定,會拋出錯誤。因此,當配置中有兩個相同類別的bean時,需要為其指定類似命名空間的參數,叫做qualifier。下面是一個配置例子:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd"> <context:annotation-config /> <bean id="peter" class="demo.UserV4" p:username="Peter" /> <bean class="demo.RadioV2"> <qualifier value="radio" /> </bean> <bean class="demo.Tv"> <qualifier value="tv" /> </bean> </beans>
在配置中有兩個同屬于一個Appliance類別的bean:Radio及Tv,但為它們指定了各自的qualifier。這樣,在用戶綁定的時候聲明使用哪一個,就不會造成混淆:
package demo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; public class UserV4 { private String username; @Autowired @Qualifier("radio") private Appliance appliance; public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public Appliance getAppliance() { return appliance; } }
下面運行一下業務邏輯:
package demo; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class UserPlayApplianceV4 { public static void main(String[] args) { ApplicationContext ctx = new ClassPathXmlApplicationContext "configV3.xml"); UserV4 peter = (UserV4) ctx.getBean("peter"); System.out.println(peter.getUsername() + " is using " + peter.getAppliance().use()); } }
這一版代碼與前一版沒什么區別,僅僅是配置文件使用剛剛做好的的configV3.xml,輸出如下: 1 Peter is using RADIO 可以看到,雖然配置中有兩個同類別的bean,在qualfier的幫助下,Spring可以正確地綁定需要的bean。這種綁定方式的好處是配置及代碼大大地簡化了,但付出的代價是User類別只能綁定Appliance某種具體的實現類,如果想從Radio切換到Tv,就需要在 User.java 中將@Qualifier切換至’tv’。這是代碼簡化付出的代價。 Spring在使用中需要注意的問題 在上一節使用Spring時,用 ClassPathXmlApplicationContext 把配置讀取進來,并從中取bean。用下面這個代碼看看Spring的這樣一個動作的開銷是多大:
package demo; import org.springframework.context.support.ClassPathXmlApplicationContext; public class ShowTimeConsumingByLoadingContext { private static long start = 0; private static long end = 0; private static final int TIMES = 100; public static void main(String[] args) { long timer = 0; for (int i = 0; i < TIMES; i++) { start = System.currentTimeMillis(); new ClassPathXmlApplicationContext("config.xml"); end = System.currentTimeMillis(); timer += end - start; } System.out.println("the average time of load is: " + timer / TIMES + "ms"); } }
為了使評測結果更加客觀,將計時器分成兩個:start使end。并且將其聲明為static,在系統加載時為其分配內存,這樣聲明使兩個變量的系統開銷影響降至最低。并且在第19行,計時后再計算,減法開銷可忽略。最后,使用循環的方式,進行100次的配置加載,最后取平均值。在我的機器上運行結果為為: 40ms。我的機器配置為:內存2GB為667 MHz DDR2 SDRAM、CPU為2.1 GHz Intel Core 2 Duo??梢娺@個開銷是不可以忽視的。 如果在使用Spring時,在代碼中有很多這樣的加載點,那么每一次用戶調用時,都會有一次開銷。這在J2EE的世界中是不可接受的。因為WEB應用天生就是多線程的,如果100個用戶同時訪問您的項目,這時每個線程里的程序類都要執行: 1 new ClassPathXmlApplicationContext("xxx.xml") 那么這個配置就要被加載100次,系統開銷是非常大的。因此,您在構建J2EE工程時,必須要避免以這種形式使用Spring,否則它將成為系統瓶頸。正確的方法是在J2EE項目啟動時,將所有的配置一次性統一加載到靜態內存中,各個線程共享并由Spring控制bean的生命周期。在第二部分將介紹這種加載方式。 小結 本文介紹了OCP原則,DI及Spring的設計模式,并介紹了一種基于Annotation自動綁定方法。請大家記住那條口訣: 層層之間用接口,類的屬性要封裝
?
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
