1. 程式人生 > >Spring實戰 第一章 1.1 簡化Java開發

Spring實戰 第一章 1.1 簡化Java開發

第一章 付諸實踐

這一章包含下面幾個主題:

  • Spring的Bean容器
  • 瀏覽Spring的核心模組
  • Spring微系統
  • Spring 4的新內容

對於Java開發者來說,這是一個好的時代。

在過去的20年中,Java經歷了好的時候,也經歷了壞的時候。儘管有一些粗糙的地方,比如:Applets、
EJB、JDO和無數的日誌框架,Java有豐富多樣的歷史,有很多企業已經建立的平臺。其中,Spring一直
都是其中最重要的組成部分。

在早期,Spring被建立用於替代笨重的Java企業技術,比如EJB。相比於EJB,Spring提供了一個更加精
簡的程式設計模型。它提供了簡單Java物件(POJO)更大的權力,相對於EJB及其他Java企業規範。

隨著時間的推移,EJB及Java企業規範2.0版本本身也提供了一個簡單的POJO模型。現在,EJB的一些概
念,如DI和AOP都來自於Spring。

儘管現在J2EE(即總所周知的JEE)能夠趕上Spring,但是Spring從未停止演進。即使是現在,Spring開始進步的時候,
J2EE都是開始在探索,而不是創新。移動開發、社交API的整合、NoSql資料庫、雲端計算和大資料,僅僅是Spring創新
的一些方面。而且未來,Spring會繼續發展。

就像我說的,對於Java程式設計師來說,這是一個好的時代。

這是一本關於Spring的書。在這一章中,我們會概述Spring,為你帶來一個簡單Spring概述。這一章將給你
一個Spring解決問題的方法,本書的其餘部分會深入學習。

1.1 簡化Java開發

Spring是由Rod Johnson開發的開源框架,該框架最初在他的書:Expert One-on-One: J2EE Design and
Development (Wrox, 2002, http://amzn.com/0764543857) 中提出。Spring是為了解決企業開發的複雜
任務Java程式都可以從其簡單性、可測試性和鬆耦合中獲得好處。

Bean或者其他的名稱,雖然Spring使用Bean或者JavaBean指代應用元件,但是這不意味著Bean元件必須遵
循JavaBeans規範。一個Spring Bean必須是一個POJO型別。在這本書中,我假定JavaBean的寬鬆定義就是POJO。

就像你在本書中看到的,Spring可以做很多事情。但是幾乎所有Spring提供的理念、都集中在其根本使命,
即簡化Java開發。

這是一個大膽的宣告,某些框架聲稱可以簡化某一些事情,但是Spring可以簡化Java廣闊的開發。這個需要更
多的解釋。Spring怎麼簡化Java開發。

為了簡化Java開發,Spring引入了下面4各核心策略:

  • 輕量級及最簡化POJO的開發
  • 通過DI和麵向介面程式設計降低依賴
  • 通過面向切面和公共約束進行宣告式的開發
  • 通過面向切面和模板消除樣板程式碼

基本上,Spring能做的絕大多數事情都可以追溯到上面的4個策略的某一方面或者幾個方面。在本章的其餘部分,
我將會對上面的四個方面進行展開分析,並且通過例子說明,Spring是怎麼簡化Java開發的。

1.1.1 釋放POJO的力量

如果你做了一段時間的Java開發,你就會發現,很多框架需要你去實現預定義的類或者實現預定義的介面,
這種侵入性的程式設計模型的最簡單例子就是EJB-2的無狀態會話Bean。但是,即使是這麼一個容易實現的目
標,在Struts的早期版本、Webwork、 Tapestry和許多其他的Java規範和框架中都很容易見到。

Spring儘量避免它自己的API去汙染你的程式碼,它絕不會強迫你去實現一個Spring標準的介面或者繼承一個
Spring標準的類。相反,基於Spring的應用開發通常沒有一個明顯的標誌說他們是基於Spring的。有時,一
個被Spring註解註釋的類可能也被其他的註解註釋。

為了描述這種情況,請考慮下面的HelloworldBean的例子:

//Spring並不會對HelloWoldBean做出任何要求

package com.habuma.spring;

public class HelloWorldBean{
    public String sayHello(){
        return "Hello World";       <===這個就是所有需要的東西
    }
}

如你所見,這是一個簡單且普通的Java類,也就是一個POJO。沒有任何特殊說明,指明它是一個Spring的
元件。Spring的非侵入性程式設計模型表明,該類可以在Spring中使用,也可以在其他的應用程式中使用。

儘管POJO的形式簡單,但是它的力量是巨大的。Spring提高POJO力量的一種方式就是使用DI去組裝它們。
讓我們看看DI是怎麼幫助應用程式元件之間進行解耦的。

1.1.2 依賴注入(Injecting Dependencies) –DI

DI這個詞剛聽起來覺得是害怕的,它可能是相當複雜的程式設計技術或者設計模式。但事實證明,DI一點都不像
它聽起來那麼難。通過在應用中使用DI,你會發現你的應用程式變淡簡單、容易理解並且易於測試。

DI是怎麼工作的

一個正常的應用程式都是有兩個或者更多個相互協作的類組合起來的。傳統上,每個物件都會儲存它所以來的
物件的引用。這個會導致高度耦合並且難於測試。

併入,考慮下面的Knight類:

// RescueDamselQuest的quest物件只能應用在DamselRescuingKnight中
package com.springinaction.knights;

public class DamselRescuingKnight implements Knight{
    private RescueDamselQuest quest;

    public DamselRescuingKnight(){
        this.quest = new RescueDamselQuest();
    }
    public void embarkOnQuest(){
        quest.embark();
    }
}

如上所示,騎士建立了一個少婦需要營救的請求(RescueDamselQuest)在它自己的建構函式中。這個會
使騎士與少婦請求繫結到一起,這嚴重限制了騎士的能力。如果一個少婦需要營救,那沒有問題。但是如果
一頭大象需要被殺死,那麼騎士什麼都做不了,只能坐在旁邊觀看。

更難的是,如果對該類寫單元測試是很難得。在這個測試中,你可能需要斷言,當騎士的embarkOnQuest
被呼叫時,那個請求embark方法也被呼叫,但是這裡沒有什麼函式能夠完成這個任務。不幸的是,DamselRescuingKnight是不可測試的。

耦合是一個特別糟糕的主意,一方面,耦合的程式碼難於測試、難於重用、難以理解並且他經常導致“打地鼠”的Bug行為(一種修改一個Bug通常會引起其他新的一個甚至更多的新bug的行為)。另一方面,一定
數量的耦合程式碼是必須的,完全不耦合的程式碼將什麼事情都不做。為了去做一些有用的事情,類需要知道彼此。耦合是必須的,但是必須被小心的管理。

使用DI,物件在建立的時候被一些確定系統物件座標的第三方去給予出其依賴,物件不需要去建立或者獲取
其依賴,像下圖描述的那樣,依賴被注入進了需要他們的物件。

的

為了描述這個要點,請看下面BraveKnight的例子:該騎士不僅勇敢,而且當任何一種請求來臨是都可以解
決。

//一個靈活的騎士

package com.springinaction.knights;

/**
 * Created by Robert.wang on 2016-3-9-0009.
 */
public class BraveKnight implements Knight {
    private Quest quest;

    public BraveKnight(Quest quest) {
        this.quest = quest;
    }

    @Override
    public void embarkOnQuest() {
        quest.embark();
    }
}

就像在上面看到的一樣,BraveKnight不像DamselRescuingKnight 一樣建立自己的Quest,而是在構造函
數的引數中傳入Quest,這樣的DI就是著名的建構函式注入(Constructor injection)。

更加的,那個Quest只是一個介面,所有實現該介面的實現都可以傳入。所以BraveKnight可以處理不同的
需求。

關鍵點就是BraveKnight沒有跟任何特定的Quest進行繫結。它不在乎是什麼樣的請求,只要該請求實現了Quest介面就可以。這個就是DI的好處–鬆耦合。如果一個物件的依賴只是一個介面,那麼你可以將他的實
現從一個換成另外一個。

其中最常見的方式是將介面換成一個測試的實現。你可能沒有辦法測試DamselRescuingKnight ,但是你可以很容易地測試BraveKnight,通過模擬一個Quest的實現,像下面這樣:

import org.junit.Test;

import static org.mockito.Mockito.*;
import org.junit.Test;

/**
 * Created by Robert.wang on 2016-3-9-0009.
 */
public class BraveKnightTest {

    @Test
    public void testEmbarkOnQuest() throws Exception {
        Quest quest = mock(Quest.class);
        BraveKnight knight = new BraveKnight(quest);
        knight.embarkOnQuest();
        verify(quest, times(1)).embark();
    }
}

這裡,你可以使用mock物件框架,即知名的mockito框架去建立一個mock的Quest實現。使用了mock對
象,你可以建立一個BraveKnight的例項,通過構造器注入mock的Quest物件,然後呼叫BraveKnight的
embarkOnQuest方法,然後通過mockito的verify去驗證mock的Quest物件的embark方法是否被呼叫一
次。

注入一個Quest進Knight

既然你BraveKnight物件可以處理任何你想傳遞給他的Quest物件,假設你想傳遞一個殺死恐龍任何,那麼
你可以傳遞一個SlayDragonQuest給他是合適的。

import java.io.PrintStream;

/**
 * Created by Robert.wang on 2016-3-9-0009.
 */
public class SlayDragonQuest implements Quest {
    private PrintStream stream;

    public SlayDragonQuest(PrintStream stream) {
        this.stream = stream;
    }

    @Override
    public void embark() {
        stream.println("Embarking on quest to slay the dragon");
    }
}

就像你所看到的一樣,SlayDragonQuest實現了Quest介面,使得它適合BraveKnight。你可能會注意到,
不像一些簡單的java程式那樣直接使用System.out.println。SlayDragonQuest通過構造器注入一個
PrintStream使得它更加的通用。在這裡最大的問題就是,你怎麼傳遞SlayDragonQuest給BraveKnight,
怎麼傳遞PrintStream給SlayDragonQuest。

應用元件之間建立關聯的行為通常稱為佈線或者裝配(wiring)。在Spring中,元件之間的裝配方式有很多
種,但是一個通常的方式是使用XML。接下來的清單展示了一個簡單的Spring配置檔案–knights.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">
    <bean id="knight" class="com.springinaction.knights.BraveKnight">
        <!--quest注入quest的Bean-->
        <constructor-arg ref="quest"/>
    </bean>
    <!--建立Quest-->
    <bean id="quest" class="com.springinaction.knights.SlayDragonQuest">
        <constructor-arg value="#{$(System).out}"/>
    </bean>
</beans>

這裡,BraveKnight和SlayDragonQuest被宣告為Bean,在BraveKnight Bean中,通過傳遞一個Quest的
引用作為建構函式的引數。同時,SlayDragonQuest使用Spring表示式語言傳遞一個System.out的構造函
數引數給SlayDragonQuest物件。如果XML配置檔案不適合你的口味,你可以使用Java方式進行配置。如
下:

package com.springinaction.knights.config;

import com.springinaction.knights.BraveKnight;
import com.springinaction.knights.Knight;
import com.springinaction.knights.Quest;
import com.springinaction.knights.SlayDragonQuest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * Created by Robert.wang on 2016-3-9-0009.
 */
@Configuration
public class KnightConfig {
    @Bean
    public Knight knight() {
        return new BraveKnight(quest());
    }

    @Bean
    public Quest quest() {
        return new SlayDragonQuest(System.out);
    }
}

不管使用xml還是java,依賴注入的好處都是一樣的。儘管BraveKnight依賴Quest,但是它不需要知道具體
是什麼Quest,同樣的,SlayDragonQuest也不需要知道具體的PrintStream型別。在Spring中,僅僅通過
配置使得所有的片段組裝在一起。這個就使得可以去改變他們之間的依賴關係而不需要去修改類的實現。

這個例子已經展示了一個簡單的Spring裝配。現在你不要考慮太多,我們在第二章的時候會更加深入地探
討,我們可以發現另外一個裝配方式,即通過自動發現功能。

現在你已經聲明瞭BraveKnight和SlayDragonQuest之間的關係,接下來就是載入配置檔案並且執行應用。

怎麼執行

在一個Spring應用程式中,一個應用程式上下文(Application Context)會載入Bean定義並且將他們組裝在一起。Spring上下文完全負責建立並組裝Bean,並將他們組成應用程式。Spring提供了幾種不同的應用上
下文,他們之間的不同就是載入配置檔案的方式的不同。

當使用xml方式的時候,使用ClassPathXMLApplicationContext,這個類通過載入在引用程式目錄下面的一
個或者幾個xml配置檔案。下面的main方法載入knights.xml檔案並獲取Knight的一個物件引用。

    @Test
    public void main() {
//        載入Spring上下文
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("knights.xml");
//        獲取knight的bean
        Knight knight = context.getBean(Knight.class);
//        使用Bean方法
        knight.embarkOnQuest();

        context.close();
    }

現在,讓我們看看Spring簡化Java開發的第二個策略,基於方面程式設計(AOP)

1.1.3 AOP

雖然DI可以使得你的應用程式元件之間是鬆耦合的,但是AOP可以使得你可以在你應用程式中去捕獲Bean的
功能。

AOP通常被定義為分離軟體關注點的一種技術。系統通常由一些具有特定功能的元件組成。但是,通常這些
元件也附帶一些除了核心功能之外的一些功能。系統服務,如日誌記錄、事務管理和安全性,通常會在每個
元件中都是需要的。這些系統服務通常被稱為橫切關注點(cross-cutting concerns),因為他們會在系統中切割多個元件。

通過傳遞這些橫切關注點,你會提供你應用程式的複雜性:

  • 程式碼重複。這就意味著你如果修改其中一個功能,你修改需要許多的元件。即使你把關注點抽象為一個單獨的模組,這樣對你元件的影響是一個單一的方法,該方法呼叫也會在多個地方重複。
  • 你的元件中充斥這與它核心功能不一致的程式碼。如新增一個條目到一個地址簿中,我們應該只關心如何新增地址,而不是關心是否安全或者是否具有事務一致性。

如下圖所示,左邊的業務物件緊密耦合右邊的系統服務。每一個業務物件不僅需要知道他們的日誌記錄、安全性和事務性,而且還得實現自己的核心功能。

這裡寫圖片描述

AOP可以模組化這些服務,並且通過宣告式的方式應用這些服務,這將導致元件更加具有凝聚力,並且元件專注於自己特定的功能,對可能涉及的系統服務完全不知情。簡單來說,就是讓POJO始終保持扁平。

它可能有助於幫助對業務方面的思考,包括許多元件的應用程式,如下所示:

這裡寫圖片描述

它的核心就是業務模組實現業務功能,使用AOP,你可以覆蓋這些業務功能。這一層可以通過一種友好的方
式來進行靈活的應用。這是一個強大的概念,因為它可以分離核心功能與系統服務。為了演示如何在Spring
中使用,請看下面的例子。

實現AOP

假設,你需要記錄騎士的來及去的服務,如下:

package com.springinaction.knights;

import java.io.PrintStream;

/**
 * Created by Robert.wang on 2016-3-9-0009.
 */
public class Minstrel {
    private PrintStream stream;

    public Minstrel(PrintStream stream) {
        this.stream = stream;
    }

    //called before quest
    public void singleBeforeQuest() {
        stream.println("Fa la la, the knight is so brave");
    }

    //called after quest
    public void singleAfterQuest() {
        stream.println("Tee hee hee, the brave knight did embark on a quest");
    }
}

就像你看到的一樣,Minstrel是一個包含兩個方法的簡單物件,這是簡單的,將這個注入進我們之前的代
碼,如下所示:

package com.springinaction.knights;

/**
 * Created by Robert.wang on 2016-3-9-0009.
 */
public class BraveKnight implements Knight {
    private Quest quest;
    private Minstrel minstrel;

    public BraveKnight(Quest quest, Minstrel minstrel) {
        this.quest = quest;
        this.minstrel = minstrel;
    }

    public void embarkOnQuest() {
        minstrel.singleBeforeQuest();
        quest.embark();
        minstrel.singleAfterQuest();
    }
}

現在,你需要做的就是在Spring的配置檔案中加入Ministrel的建構函式引數。但是,等等….

好像看起來不對,這個真的是騎士本身關注的嗎?騎士應該不必要做這個工作。畢竟,這是一個歌手的工
作,歌頌騎士的努力,為什麼其實一直在提醒歌手呢?

另外,由於騎士必須知道歌手,你被迫傳遞歌手給騎士,這個不僅使騎士的程式碼複雜,而且讓我很困惑,當
我需要一個騎士而沒有一個歌手的時候,如果Ministrel為null,在程式碼中還得進行非空判斷,簡單的
BraveKnight程式碼開始變得複雜。但是使用AOP,你可以宣佈歌手必須歌唱騎士的任務,並且,釋放騎士,直接處理歌手的方法。

在Spring配置檔案中,你需要做的就是將歌手宣告為一個切面。如下:

<?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">
    <bean id="knight" class="com.springinaction.knights.BraveKnight">
        <!--quest注入quest的Bean-->
        <constructor-arg ref="quest"/>
    </bean>
    <!--建立Quest-->
    <bean id="quest" class="com.springinaction.knights.SlayDragonQuest">
        <constructor-arg value="#{T(System).out}"/>
    </bean>

    <!--定義歌手的Bean-->
    <bean id="ministrel" class="com.springinaction.knights.Minstrel">
        <constructor-arg value="#{T(System).out}"/>
    </bean>

    <aop:config>
        <aop:aspect ref="ministrel">
            <!--定義切點-->
            <aop:pointcut id="embark" expression="execution(* *.embarkOnQuest(..))"/>
            <!--定義前置通知-->
            <aop:before pointcut-ref="embark" method="singleBeforeQuest"/>
            <!--定義後置通知-->
            <aop:after method="singleAfterQuest" pointcut-ref="embark"/>
        </aop:aspect>
    </aop:config>
</beans>

使用Spring的AOP配置一個Ministrel作為切面,在切面裡面,定義一個切點,然後定義前置通知(before
advice)和後置通知(after advice)。在兩個例子中,pointcut-ref屬性都使用了一個embark的引用,這
個切點是通過pointcut元素定義的,它表明通知應該應用在什麼地方,表示式的語法遵循AspectJ的切點表
達式語法。

關於AspectJ語法,不用擔心,我們在第四章會深入討論。現在,你只需要知道,在BraveKnight呼叫
embarkOnQuest,前後,分別呼叫Ministrel的singleBeforeQuest和singleAfterQuest就行了。這就是所
有的一切。使用一點xml配置,你就將歌手分配為了一個切面,在第四章,你會看到更多的例子。

首先,Ministrel始終是一個POJO,沒有任何說明他是用來作為切面的。作為一個切面是通過Spring配置文
件實現的。其次,也是最重要的,Ministrel可以應用到BraveKnight而不需要BraveKnight直接呼叫它,實
際上,BraveKnight根本不知道Ministrel的存在。

需要指出的是,你可以使用Spring的魔法使得Ministrel作為一個切面,但是Ministrel必須首先是一個Spring
的Bean,關鍵的就是你可以使用任何Spring Bean作為切面,而且可以將其注入其他的Bean中。

使用AOP是愉快的。但是Spring AOP可以做更實際的事情。等下你會看到,Spring AOP可以提供服務,如
宣告式事務和安全。

但是現在,讓我們看看Spring簡化Java開發的其他方面。

1.1.4 消除樣板程式碼

你是不是曾經寫程式碼的時候感覺之前也寫過同樣的程式碼?這是不是已經看到了,我的朋友。這個就是樣板代
碼,那些你寫過無數遍去完成相同的任何或者簡單的任務的程式碼。

不幸的是,有很多地方涉及Java API的一堆樣板程式碼。一個通常的樣板程式碼的示例就是連線JDBC並且去查下資料,如果你使用過JDBC,可能寫過跟下面相似的程式碼:

 public Employee getEmployeeById(long id) {
        Connection conn = null;
        PreparedStatement stmt = null;
        ResultSet rs = null;

        try {
            conn = dataSource.getConnection();
            //選取僱員
            stmt = conn.prepareStatement("select id, firstname, lastname, salary from " +
                    "employee where 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"));
            }
            return employee;
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            if (rs != null) {
                try {
                    rs.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            if (stmt != null) {
                try {
                    stmt.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            if (conn != null) {
                try {
                    conn.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
        return null;
    }

以上的程式碼實現通過僱員ID查詢僱員資訊,但是,我們敢打賭,你看的很艱難,那是因為,關鍵的程式碼被一
堆JDBC的程式碼所淹沒。首先你必須建立一個連線,然後必須建立一個語句,並且最後查詢結果。並且,為了
避免JDBC的各種問題,必須丟擲異常,對異常進行檢測並處理,最後還得對JDBC連線進行清理。關閉連線、語句和結果集。這些也有可能引發JDBC異常,還得進行處理。

最重要的是,你必須為所有的JDBC操作寫相同的程式碼,一個員工的查詢只是一小部分程式碼,其他大部分是JDBC的操作。JDBC不是唯一的樣板程式碼的例子,有許多其他類似的樣板程式碼,如:JMS、JNDI和其他服務的往往涉及相同的重複程式碼。

Spring消除樣板程式碼的方法是將其封裝在模板中,比如Spring 的JdbcTemplate就將所有的JDBC樣板程式碼封裝起來,使用的時候只需要注意業務邏輯即可。

比如使用Spring JdbcTemplate可以將getEmployyeeId寫成下面的模式。

public Employee getEmployeeByIdd(long id) {
        return jdbcTemplate.queryForObject("select id, firstname, lastname, salary 
        from employee where id=?", new RowMapper<Employee>() {
            @Override
            public Employee mapRow(ResultSet resultSet, int i) throws SQLException {
                Employee employee = new Employee();
                employee.setId(resultSet.getLong("id"));
                employee.setFirstName(resultSet.getString("firstname"));
                employee.setLastName(resultSet.getString("lastname"));
                return employee;
            }
        }, id);
    }

可以看出,新版的getEmployeeByIdd是非常簡單並且只關注獲取僱員的這個業務。

我們已經看到,Spring是怎麼通過面向POJO程式設計、DI、AOP及模板來簡化Java開發的。同時,向你展示了在XML中怎麼配置Bean以及怎麼配置切面,但是這些檔案是如何被載入的呢?他們又裝了什麼?讓我們看看Spring的容器,這裡是你的應用程式的Bean存放的地方。