Spring實戰(1):初步瞭解Spring
Spring是什麼啊 O_O
春天?emm…我現在所說的Spring是指一個開源框架
PS!!!從現在開始,一定要很熟悉的知道下面這些英文單詞首字母組成的簡稱,後面不再做解釋
EJB:Enterprise JavaBean ——企業級JavaBean
JDO:Java Data Object ——Java資料物件
POJO:Plain Old java Object ——簡單老式Java物件
DI:Dependency Injection ——依賴注入
AOP:Aspect-Oriented Programming ——面向切面程式設計
Spring到底是什麼?它用來幹嘛呢?
Spring是為了解決企業級應用開發的複雜性而建立 的,使用Spring可以讓簡單的JavaBean來實現之前只有EJB和其他企業級Java規範才能完成的事情。相對於EJB來說,Spring提供了更加輕量級和簡單的程式設計模型,它增強了POJO的功能。
但Spring不僅僅侷限於伺服器端開發,任何Java應用都能在簡單性、可測試性和鬆耦合等方面從Spring中獲益。
Spring的目標是全方位簡化Java開發,它採取了以下4種關鍵策略:
- 基於POJO的輕量級和最小侵入性程式設計
- 通過依賴注入和麵向介面實現鬆耦合
- 基於切面和慣例進行宣告式程式設計
- 通過切面和模版減少樣板式程式碼
Spring如何達到它的目的(簡化Java開發)?
激發POJO的潛能
在基於Spring構建的應用中,它的類通常沒有任何痕跡表明我們使用Spring。也就是說,Spring會竭力避免因自身的API而弄亂我們的應用程式碼,不會強迫我們實現Spring規範的介面或繼承Spring規範的類。
儘管我們可能只是寫了一個形式非常簡單的POJO,Spring也會發揮它的作用讓POJO一樣可以具有魔力——通過DI來裝配它們。也就是說,Spring可以激發POJO的潛能。
可能類裡面使用了Spring的註解,但是去掉註解,它仍然是一個普通的Java類。
依賴注入(DI)
依賴注入已經演變成一項複雜的程式設計技巧或設計模式理念,可以幫助應用物件彼此之間保持鬆散耦合。應用DI可以讓我們的程式碼變得異常簡單並且更容易理解和測試。
耦合涉及到什麼?
耦合具有兩面性,一方面,緊密耦合的程式碼難以測試、難以複用、難以理解,並且bug也是一個接一個;另一方面,一定程度的耦合又是必須的,完全沒有耦合的程式碼什麼也做不了。所以耦合性高的程式碼將會給開發者的測試和維護帶來巨大的麻煩,因為它們往往牽一髮而動全身。但為了完成有實際意義的功能,不同的類必須以適當的方式進行互動。
Spring就解決了這個關鍵的問題,它將物件之間的依賴關係轉而用配置檔案來管理。也就是它的依賴注入機制。
依賴注入機制就是面向介面程式設計,物件通過介面來表明依賴關係。這就是依賴注入機制帶來的最大的收益——鬆耦合。
如果一個物件至通過介面(而不是具體實現或初始化過程)來表明依賴關係,那麼這種依賴就能夠在物件本身毫不知情的情況下,用不同的具體實現進行替換。
接下來我們以經典的騎士例子來說明依賴注入機制.
接下來我們來看一個騎士的例子:
編寫一個類來實現“勇敢的騎士要去拯救被綁架的少女”。
1.1 一個騎士的介面
public interface Knight {
public void embarkOnQuest();
//embarkOnQuest()方法為騎士開始執行任務。
}
這個騎士現在要去拯救少女啦~
所以我們要實現拯救少女這個動作的類
1.2 拯救少女的動作
public class RescueDamselQuest {
public void embark() {
System.out.println("騎士去拯救少女啦!");
}
}
再實現拯救少女的騎士
1.3 : 騎士去拯救少女
public class DamselRescuingKnight implements Knight {
private RescueDamselQuest quest;
public DamselRescuingKnight() {
this.quest = new RescueDamselQuest();
}
public void embarkOnQuest() {
quest.embark();
}
}
我們可以從程式碼中看到,DamselRescuingKnight在它的建構函式裡自行建立了RescueDamselQuest。這使得DamselRescuingKnight和RescueDamselQuest緊密耦合到了一起,也就是說如果一個少女需要救援,這個騎士能夠召之而來,但是如果去做其他的活動,這個騎士就無能為力了,極大地限制了騎士執行探險的能力。
騎士應該是無所不能的,接著我們對上面的程式碼進行改動。
首先我們把騎士要執行的任務抽象為一個介面。
1.4 : 騎士要執行的任務介面Quest
public interface Quest {
public void embark();
//embark()方法代表開始執行任務
}
然後我們讓拯救少女這個任務繼承Quest介面
1.5:可以執行各種任務的勇敢騎士
public class BraveKnight implements Knight {
private Quest quest;
public BraveKnight(Quest quest) { //Quest被注入進來
this.quest = quest;
}
public void embarkOnQuest() {
quest.embark();
}
}
這個勇敢的騎士不像上一個騎士,沒有自行建立探險任務,而是在構造的時候把探險任務作為構造器引數傳入。這是DI的方式之一,即構造器注入。
為了驗證程式1.5中Quest是否成功注入,我們使用使用mock測試來測試一下。
mock:在測試過程中,對於某些不容易獲取的物件,用一個虛擬的物件來建立以便測試的方法。
1.6 為了測試BraveKnight,需要注入一個mock Quest
import static org.mockito.Mockito.*;
import org.junit.Test;
public class BraveKnightTest{
@Test
public void knightShouldEmbarkOnQuest() {
Quest mockQuest = mock(Quest.class);
//建立mock Quest
BraveKnight knight = new BraveKnight(mockQuest);
//注入mock Quest
Knight.embarkQuest();
//times(1): 在上述條件下, 驗證embark()是否只被呼叫了一次
}
}
這個勇敢的騎士現在不止可以拯救少女,還可以斬殺惡龍。
1.7 斬殺惡龍的任務
public class SlayDragonQuest implements Quest {
private PrintStream stream;
//這裡並沒有直接指定輸出的格式, 輸出格式由使用者在構造方法中決定
public SlayDragonQuest(PrintStream stream) {
this.stream = stream;
}
public void embark() {
stream.println("騎士在斬殺惡龍!");
}
}
現在SlayDragonQuest實現了Quest介面,這樣它就適合注入到BraveKnight中去了。
可是,我們要怎樣將SlayDragonQuest交給BraveKnight呢?
這就使用到了依賴注入機制的裝配(wiring)。
建立應用元件之間協作的行為通常稱為裝配。Spring有多種裝配bean的方式,比如使用XML,或者基於Java的配置。接下來我們來裝配SlayDragonQuest。
1.8 knight.xml 將SlayDragonQuest、BraveKnight和PrintStream裝配在一起
<?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.xsd">
<!--建立SlayDragonQuest-->
<bean id="quest" class="com.springinaction.knights.SlayDragonQuest">
<constructor-arg value="#{T(System).out}" />
</bean>
<!--注入Quest bean-->
<bean id="knight" class="com.springinaction.knights.BraveKnight">
<constructor-arg ref="quest"/>
</bean>
</beans>
PS!!!XML配置檔案一定要放在resources目錄下
BraveKnight和SlayDragonQuest被宣告為Spring中的bean。就BraveKnight bean來講,它在構造時傳入了對SlayDragonQuest bean的引用,將其作為構造器引數。
SlayDragonQuest bean的宣告中使用了Spring表示式語言(SpEL),將System.out傳入到了SlayDragonQuest的構造器中。
- 在SpEL中,使用T()運算子會呼叫類作用域的方法和常量。例如,在SpEL中使用Java的Math類,我們可以像下面的示例這樣使用T()運算子:
T(java.lang.Math)
T()運算子的結果會返回一個java.lang.Math類物件。
Spring還支援使用Java來描述配置。
1.9 Spring提供了基於Java的配置,可作為XML的替代方案
@Configuration
public class KnightConfig {
@Bean
public Knight knight() {
return new BraveKnight(quest());
}
@Bean
public Queue quest() {
return new SlayDragonQuest(System.out);
}
}
不管使用XML還是Java配置,DI所帶來的收益都是相同的。這樣我們就可以在不改變所依賴的類的情況下,修改依賴關係。
現在已經聲明瞭BraveKnight和SlayDragonQuest的關係,接下來只需要裝載XML配置檔案,並把應用啟動起來。
Spring通過應用上下文(Application Context)裝載Bean的定義並把它們組裝起來,Spring應用上下文全權負責物件的建立和組裝。
Spring提供了多種Application Context,可列舉如下:
- AnnotationConfigApplicationContext——從Java配置檔案中載入應用上下文
- AnnotationConfigWebApplicationContext——從Java配置檔案中載入Spring web應用上下文
- ClassPathXmlApplicationContext——從classpath(resources目錄)下載入XML格式的應用上下文定義檔案
- FileSystemXmlApplicationContext——從指定檔案系統目錄下載入XML格式的應用上下文定義檔案
- XmlWebApplicationContext——從classpath(resources目錄)下載入XML格式的Spring web應用上下文
現在假設我們載入的為XML配置檔案,那麼應該使用ClassPathXmlApplicationContext來載入knight.xml。
1.10 : KnightMain.java載入包含Knight的Spring上下文
public class KnightMain {
public static void main(String[] args) throws Exception {
ClassPathXmlApplicationContext context =
new ClassPathXmlApplicationContext("classpath*:knight.xml");
Knight knight = (Knight) context.getBean("knight");
//獲取Knight Bean
knight.embarkOnQuest();
//使用knight
context.close();
}
}
這裡的main()方法基於knights.xml檔案建立了Spring應用上下文。隨後它呼叫該應用上下文獲取一個ID為knight的bean。得到Knight物件的引用後,只需簡單呼叫embarkOnQuest()方法就可以執行所賦予的探險任務了。
通過這個例子,我們大概瞭解了什麼是DI,以及它的作用。現在我們再關注Spring簡化Java開發的下一個理念:基於切面進行宣告式程式設計。
應用切面(AOP)
DI能讓相互協作的軟體元件保持鬆散耦合,而AOP允許我們把遍佈應用各處的功能分離出來形成可重用的元件。
系統由許多不同的元件組成,每一個元件各負責一塊特定功能。而AOP往往被定義為促使軟體系統實現關注點分離的一項技術。
可以說AOP是OOP的補充和完善。
OOP引入封裝、繼承、多型等概念來建立一種物件層次結構,用於模擬公共行為的一個集合。不過OOP允許開發者定義縱向的關係,但並不適合定義橫向的關係,例如日誌功能。日誌程式碼往往橫向地散佈在所有物件層次中,而與它對應的物件的核心功能毫無關係對於其他型別的程式碼,如安全性、異常處理和透明的持續性也都是如此,這種散佈在各處的無關的程式碼被稱為橫切(cross cutting),在OOP設計中,它導致了大量程式碼的重複,而不利於各個模組的重用。AOP技術恰恰相反,它利用一種稱為”橫切”的技術,剖解開封裝的物件內部,並將那些影響了多個類的公共行為封裝到一個可重用模組,並將其命名為”Aspect”,即切面。所謂”切面”,簡單說就是那些與業務無關,卻為業務模組所共同呼叫的邏輯或責任封裝起來,便於減少系統的重複程式碼,降低模組之間的耦合度,並有利於未來的可操作性和可維護性。
使用“橫切”技術,AOP把軟體系統分為兩個部分:核心關注點和橫切關注點。業務處理的主要流程是核心關注點,與之關係不大的部分是橫切關注點。橫切關注點的一個特點是,他們經常發生在核心關注點的多處,而各處基本相似,比如許可權認證、日誌、事物。AOP的作用在於分離系統中的各種關注點,將核心關注點和橫切關注點分離開來。
我們可以把切面想象為覆蓋在很多元件之上的一個外殼。應用是由那些實現各自業務功能的模組組成的。藉助AOP,可以使用各種功能層去包裹核心業務層。這些層以宣告的方式靈活地應用到系統中,核心應用甚至根本不知道它們的存在。
為了示範Spring中如何應用切面,讓我們重新回到騎士的例子,併為它新增一個切面。
我們現在能夠知道騎士所做的事情,是因為吟遊詩人用詩歌記載了騎士的事蹟並將其進行傳唱。所以我們需要使用吟遊詩人這個服務類來記載騎士的所有事蹟。
1.11 吟遊詩人類
public class Minstrel {
private PrintStream stream;
public Minstrel(PrintStream stream) {
this.stream = stream;
}
public void singBeforeQuest() { //探險前呼叫
stream.println("啦啦啦~騎士真勇敢啊!");
}
public void singAfterQuest() { //探險後呼叫
stream.println("哈哈哈~勇敢的騎士執行任務回來啦!");
}
}
接下來讓我們將BraveKnight和Minstrel進行結合,讓詩人傳唱騎士的事蹟。
1.12 修改BarveKnight,讓它呼叫Minstrel方法
public class BraveKnight implements Knight {
private Quest quest;
private Minstrel minstrell;
public BraveKnight(Quest quest, Minstrel minstrel) { //Quest被注入進來
this.quest = quest;
this.minstrell = minstrel;
}
public void embarkOnQuest() {
minstrell.singBeforeQuest();
quest.embark();
minstrell.singAfterQuest();
}
}
好像完成了?
但是再仔細想想,管理吟遊詩人是騎士應該做的事情嗎?並不是,傳唱事蹟是吟遊詩人的職責。簡單的程式碼開始變得複雜…
但利用AOP,將Minstrel抽象為一個切面,在Spring配置檔案中宣告它,就可以解決這個問題。
1.13 修改knight.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:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--建立SlayDragonQuest-->
<bean id="quest" class="com.springinaction.knights.SlayDragonQuest">
<constructor-arg value="#{T(System).out}" />
</bean>
<!--注入Quest bean-->
<bean id="knight" class="com.springinaction.knights.BraveKnight">
<constructor-arg ref="quest"/>
</bean>
<bean id="minstrel" class="com.springinaction.knights.Minstrel">
<constructor-arg value="#{T(System).out}" />
</bean>
<aop:config>
<aop:aspect ref="minstrel">
<!--定義切點, 即定義從哪裡切入-->
<aop:pointcut id="embark" expression="execution(* *.embarkOnQuest(..))" />
<!--宣告前置通知, 在切入點之前執行的方法-->
<aop:before pointcut-ref="embark" method="singBeforeQuest" />
<!--聲明後置通知, 在切入點之後執行的方法-->
<aop:after pointcut-ref="embark" method="singAfterQuest" />
</aop:aspect>
</aop:config>
</beans>
通過XML配置,就把Minstrel宣告為一個Spring切面了。但Minstrel仍然是一個POJO,沒有任何程式碼表明它要被作為一個切面使用。
使用模版消除樣板式程式碼
我們通常為了實現通用的和簡單的任務,會寫一些樣板式的程式碼。樣板式程式碼的一個常見範例就是使用JDBC訪問資料庫查詢資料。
1.14 查詢資料庫獲得員工姓名和薪水
public Employee getEmployeeById(long id) {
Connection conn = null;
PreparedStatement stmt = null;
ResultSet rs = null;
try {
conn = dataSource.getConnection();
stmt = conn.preparedStatement(
"select id, firstname, lastname, salary from employee where id=?");
stmt.setLong(1, id);
rs = stmt.executeQuery();
Employee employee = null;
if (rs.next()) {
employee = new Employee();
employee.setId(rs.getLong("id"));
employee.setFirstName(rs.getString("firstname"));
employee.setLastName(rs.getString("lastname"));
employee.setSalary(rs.getBigDecimal("salary"));
}
return employee;
} catch (SQLException e) {
} finally {
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
}
}
if (stmt != null) {
try {
stmt.close();
} catch (SQLException e) {
}
}
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
}
}
}
return null;
}
可以看到,只有少量的程式碼與查詢員工邏輯有關係,其他的程式碼都是JDBC的樣板程式碼。
Spring旨在通過模版封裝來消除樣板式程式碼,Spring的JdbcTemplate使得執行資料庫操作時,可以避免傳統的JDBC樣板式程式碼。
1.15 : 使用Spring模板消除樣板式程式碼
public Employee getEmployeeById(long id) {
//SQL查詢
return jdbcTemplate.queryForObject(
"select id, firstname, lastname, salary from employee where id=?",
new RowMapper<Employee>() {
//將結果匹配為物件
public Employee mapRow(ResultSet rs, int int rowNum) throws SQLException {
Employee employee = new Employee();
employee.setId(rs.getLong("id"));
employee.setFirstName(rs.getString("firstname"));
employee.setLastName(rs.getString("lastname"));
employee.setSalary(rs.getBigDecimal("salary"));
employee.setName(resultSet.getString("name"));
return employee;
}
}, id); //指定查詢引數
}
這樣使用起來就簡單多了。
以上就是Spring通過面向POJO程式設計、DI、AOP和模板技術來簡化Java開發的簡單介紹。