1. 程式人生 > >Spring實戰(1):初步瞭解Spring

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開發的簡單介紹。