Spring之旅第一站(不得不佩服老外...)

第1章 Spring之旅

說明

  • 一、本文抄寫了《Spring 實戰》重點內容,參考了GitHub上的代碼
  • 二、每一個人的學習方式不同,但目的是同樣的,活學活用。最近一直在聽《咱們不同》
  • 三、本文只爲記錄做爲之後參考,要想真正領悟Spring的強大,請看原書。
  • 四、代碼和筆記在這裏GitHub,對你有幫助的話,歡迎點贊。

本章內容:java

  • Spring的bean容器
  • 介紹Spring的核心模塊
  • 更爲強大的Spring生態系統
  • Spring的新功能

對於Java程序員來講,這是一個很好的時代..........git

Spring是Java歷史中很重要的組成部分。程序員

在誕生之初,建立Spring的主要目的是用來代替更加劇量級的企業級Java技術,尤爲是EJB。相對於EJB來講,Spring提供了 更加輕量級和簡單的編程模型.它加強了簡單老式的Java對象(Plain Old Java Object POJO)的功能,使其具有了以前只有EJB和其餘企業級Java規範才具備的功能。github

儘管J2EE可以遇上Spring的步伐,但Spring也沒有中止前進(咱們程序員也同樣,根本停不下來...),Spring繼續在其餘領域發展,移動開發、社交API集成、NoSQL數據庫、雲計算以及大數據都是Spring正在涉足和創新的領域。Spring的前景會更加美好(Java也是,Java9的模塊化,只是咱們須要學習的還有不少).spring

對於Java開發者來講,這是一個很好的時代數據庫

1.1簡化Java開發

Spring是一個開源框架,最先由Rod Johnson建立,Spring是爲了解決企業級應用開發的複雜性而建立的,使用Spring可讓簡單的JavaBean實現以前只有EJB才能完成的事,但Spring不只僅侷限於服務器端的開發,任何Java應用都能在簡單性、可測試性、和鬆耦合等方面從Spring中受益。express

一個Sping組件能夠是任何形式的POJO。全部的理念均可以追溯到Spring最恨本的使命上:簡化Java開發。編程

爲了下降Java開發的複雜性,Spring採起了如下4種關鍵策略:設計模式

  • 基於POJO的輕量級和最小入侵性編程;
  • 經過依賴注入和麪向接口實現鬆耦合;
  • 基於切面和慣例進行聲明式編程:
  • 經過切面和模板減小樣板代碼;

1.1.1 激發POJO的潛力

不少框架經過強迫應用繼承他們的類或實現它們的接口從而致使應用於框架綁死。安全

Spring避免因自身的API而弄亂你的應用代碼。Spring不會強迫你實現Spring所規範的接口或繼承Spring所規範的類,相反,在基於Spring的構建的應用中,它的類一般沒有任何跡象代表你使用了Spring。最壞的場景是,一個類或許會使用Spring註解,但它依舊是POJO。

package com.guo.spring

public class HelloWordBean {
  public String sayHello() {
    return "Hello World"
  }
}

能夠看到,這是一個簡單普通的Java類——POJO。沒有任何地方代表它是一個Spring組件。Spring的非侵入式編程模型意味着這個類在Spring應用和非Spring應用中均可以發揮一樣的做用。

儘管簡單,但POJO同樣能夠擁有魔力,Spring賦予POJO魔力的方式之一就是經過DI來裝配它們。

1.1.2 依賴注入

依賴注入如今已經演變成一項複雜的編程技巧或設計模式的理念

任何一個有實際意義的應用都會由兩個或更多的類組成,這些類相互之間進行協做來完成特定的業務邏輯。按照傳統的作法,每一個對象負責管理與本身相互協做的對象的引用(即它所依賴的對象),這就會致使高度耦合和難以測試的代碼。

/**
 * Created by guo on 20/2/2018.
 * damsel表示:少女
 */
public class DamselRescuingKnight implements Knight {
    private RescueDamselQuest quest;
    public DamselRescuingKnight ( RescueDamselQuest quest) {
        //與RescueDamselQuest緊耦合
        this.quest = new RescueDamselQuest();
    }
    @Override
    public void embarkOnQuest() {
         quest.embark();
    }
}

DamselRescueingKnight在它的構造函數中自行建立了RescueDamselQuest。這使得二者牢牢的耦合在一塊兒。所以極大的限制了騎士執行探險的能力。在這樣一個測試中 ,你必須保證當騎士embarkOnQuest方法被調用的時候,探險embark方法也要被調用。可是沒有一個簡單明瞭的方式可以測試。

耦合具備兩面性:

  • 緊密耦合的代碼難以岑氏,難以複用,難以理解,而且在典型的表現出"打地鼠"式的BUG特性,(修復一個bug,將會出現一個或更多的bug).
  • 必定的程度耦合又是必須的,徹底沒有耦合的代碼什麼都作不了。爲了完成更有實際意義的功能,不一樣的類必須以適當的方式進行交互,總而言之,耦合是必須的,但須要謹慎對待

經過DI,對象的依賴關係將由系統中負責協調各對象的第三方組件在建立對象的時候設定,對象無需自行建立或管理他們的依賴關係,依賴關係將被自動注入到須要它們的對象中。

依賴注入會將所依賴的關係自動交給目標對象,而不是讓對象本身去獲取依賴

BraveKnight足夠靈活能夠接受任何賦予他的探險任務。

/**
 * Created by guo on 20/2/2018.
 */
public class BraveKnight implements Knight {
    public Quest quest;

    public BraveKnight(Quest quest) {          //Quest被注入進來
        this.quest = quest;
    }

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

不一樣於以前的DamselRescuingKnight,BraveKnight沒有自行建立探險任務,而是在構造的時候把探險任務做爲構造參數傳入。這是依賴注入的方式之一,即構造注入(constructor injection).

須要注意的是,傳入的探險類型是一個Quest,也就是全部的探險任務都必須實現的一個接口。因此BraveKnight可以響應RescueDamselQuest、SlayDragonQuest、MakeRoundTableRounderQuesst 等任意的Quest實現。

這裏的要點是BraveKnight沒有有特定的Quest實現發生耦合。對他來講,被要求挑戰的探險任務只要實現了Quest接口,那麼具體的是那種類型就可有可無了。這就是DI帶來最大的收益——鬆耦合。**若是一個對象只經過接口(而不是具體的實現或初始化過程)來代表依賴關係,那麼這種依賴就可以在對象自己絕不狀況的狀況下,用不一樣的具體實例進行替換。

對依賴進行替換的一個最經常使用方法就是在測試的時候使用mock實現。

import static org.mockito.Mockito.*;

public class BraveKnightTest {
    @Test
    public void knightShouldEmbarkQuest() {
        Quest mockQuest = mock(Quest.class);               //建立mock Quest
        BraveKnight knight = new BraveKnight(mockQuest);   //注入mock Quest;
        knight.embarkOnQuest();
        verify(mockQuest,times(1)).embark();
    }
}

能夠經過mock框架Mockito去建立一個Quest接口的mock實現。經過這個mock對象,就能夠建立一個新的BraveKnight實例,並經過構造器注入到這個mock Quest。當調用embarkOnQUest方法時,你能夠要求Mockito框架驗證Quest的mock實現的embark方法僅僅被調用了一次。

將Quest注入到Knight中

但願BraveKnight所進行的探險任務是殺死一隻怪龍,

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中了,與其餘入門不一樣的是,SlayDragonQuest沒有使用System.out.println();,而是在構造方法中請求一個更爲通用的PrintStream。

建立應用組件之間協做的行爲成爲裝配。Spring有多種裝配Bean的方式,採用XML是一種經常使用的方式。
knights.xml,該文件將BraveKnight,SlayDragonQuest和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="guo.knights.BraveKnight">
        <constructor-arg ref="quest"/>                        <!--注入Quest bean-->
    </bean>
    <bean id="quest" class="guo.knights.SlayDragonQuest">     <!--建立SlayDragonQuest-->
        <constructor-arg value="#{T(System).out}"/>
    </bean>
</beans>

在這裏,BraveKnight和SlayDragonQuest被聲明爲Spring中的bean。就BraveKnight bean來說,他在構造時傳入對SlayDragonQuest bean的引用,將其做爲構造器參數。同時,SlayDragonQuest bean 的聲明使用了Spring表達式語言(Spring Expression Language),將System.out(一個PrintStream)傳入到了SlayDragonQuest的構造器中,

在SpEL中, 使用T()運算符會調用類做用域的方法和常量. 例如, 在SpEL中使用Java的Math類, 咱們能夠像下面的示例這樣使用T()運算符:

T(java.lang.Math)

T()運算符的結果會返回一個java.lang.Math類對象.

Spring提供了基於Java的配置可做爲XML的替代方案。

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

/**
 * Created by guo on 20/2/2018.
 */
@Configuration
public class KnightConfig {
    @Bean
    public Knight knight() {
        return new BraveKnight(quest());
    }
    @Bean
    public Quest quest() {
        return new SlayDragonQuest(System.out);
    }
}

無論使用的是基於XML的配置仍是基於Java的配置,DI所帶來的收益都是相同的。儘管BraveKnight依賴於Quest,可是它並不知道傳遞給它的是什麼類型的Quest,與之相似,SlayDragonQuest依賴於PrintStream,可是編譯時,並不知道PrintStream長啥樣子。只有Spring經過他的配置,可以瞭解這些組成部分是如何裝配起來的。這樣就能夠在不改變 所依賴的類的狀況下,修改依賴關係。

**接下來,咱們只須要裝載XML配置文件,並把應用啓動起來。

Spring經過應用上下文(Application context) 裝載bean的定義,並把它們組裝起來。Spring應用上下文全權負責對象的建立個組裝,Spring自帶了多種應用上下文的實現,他們之間的主要區別僅僅在於如何加載配置。

由於knights.xml中的bean是使用XML文件進行配置的,因此選擇ClassPathXmlApplicationContext做爲應用上下文相對是比較合適的。該類加載位於應用程序類路徑下的一個或多個Xml配置文件。

public class KnightMain {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext context =
                new ClassPathXmlApplicationContext("spring/knights.xml");    //加載Sprinig應用上下文
        Knight knight = context.getBean(Knight.class);                                       //獲取knight bean
        knight.embarkOnQuest();                                                              //使用knight調用方法
        context.close();                                                                     //關閉應用上下文
    }
}

輸出以下:
Embarking on quest to slay the dragon!!,順便還能夠學英語,一箭雙鵰。

這裏的main()方法基於knight.xml文件建立了spring應用上下文。隨後他調用該應用上下文獲取一個ID爲knighht的bean。獲得Knighht對象的引用後,只須要簡單調用embarkOnQuest方法就能夠執行所賦予的探險任務了。只有knights.xml知道哪一個騎士執行力那種任務。

1.1.3 應用切面

DI可以讓相互協做的軟件組件保持鬆耦合,而面向切面編程(aspect-oriented programming AOP) 容許你把遍及應用各處的功能分離出來造成可重用的組件。

面向切面編程每每被定義爲促使軟件系統實現關注點的分離一項技術,系統由許多不一樣的組件組成,每一個組件各負責一特定的功能。除了實現自身核心的功能以外,這些組件還常常承擔着額外的職責。諸如日誌、事務管理、和安全這樣的系統服務常常融入到自身具備核心業務邏輯的組件中。這些系統一般被稱爲橫切關注點。,所以他們會跨越系統多個組件。

若是將這些關注點分散到多個組件中去,你的代碼將會帶來雙重的複雜性。

  • 實現系統關注點功能的代碼嫁給你會重複出如今多個組件中。這意味着若是你要改變這些關注點的邏輯,必須修噶各個模塊中相關的實現。即便你把這些關注點抽象成一個獨立的模塊,其餘模塊只是調用方法。但方法的調用仍是會重複出如今各個模塊中。
  • 組件會由於那些與自身核心業務無關的代碼而變得混亂。一個向地址薄增長地址條目的方法應該只關注如何添加地址。而不該該關注它是否是安全的,或者是否須要事務的支持。

AOP能使這些服務模塊化,並以聲明的方式將它們應用到它們要影響的組件中去。所形成的結果就是這些組件會具備哥哥你好的內聚性而且會更加關注自身的業務,安全不須要了解涉及系統服務所帶來的複雜性。總之AOP能確保POJO的簡單性。

咱們能夠把切面想象爲覆蓋在不少組件之上的一個外殼。應用是由哪些實現各自業務功能模塊組成的,藉助AOP,可使用各類功能層去包裹核心業務層,,這些層以聲明的方式靈活的應用到系統中,你的核心應用甚至根本不知道他們的存在。這是一個很是強大的理念,能夠將安全,事務,日誌關注點與核心業務相分離。**

每個人都熟知騎士所作的任何事情,這是由於詠遊詩人用詩歌記載了騎士的事蹟並將其進行傳唱。假設咱們須要使用詠遊詩人這個服務類來記載騎士的全部事蹟。

詠遊詩人是中世界的音樂記錄器

/**
 * Created by guo on 20/2/2018.
 * 詠遊詩人,做爲騎士的一個切面
 */
public class Minstrel {
    private PrintStream stream;

    public Minstrel(PrintStream stream) {
        this.stream = stream;
    }
    public void singBeforeQuest() {
        stream.println("Fa la la ,the Knight is so brabe");      //探險以前調用
    }
    public void singAfterQuest() {
        stream.println("Tee hee hhe,the brave knight " + "did embark on a quest");   //探險以後調用
    }
}

Minstrel只有兩個簡單的方法的類,在騎士執行每個探險任務以前,singBeforeQuest()被調用;在騎士完成探險任務以後,singAfterQuest()方法被調用。在這兩種狀況下,Minstrel都會經過一個PrintStream類來歌頌騎士的事蹟,這個類經過構造器注入進來。

但利用AOP,你能夠聲明詠遊詩人西部歌頌騎士的 探險事蹟,而騎士自己不直接訪問Minstrel的方法

要將Minstrel抽象爲一個切面,你所須要作的事情就是在一個Spring配置文件中聲明它,,

<bean id="minstrel" class="guo.knights.Minstrel">
   <constructor-arg value="#{T(System).out}"/>                                 <!--聲明Minstrel bean-->
</bean>

<aop:config>
    <aop:aspect ref="minstrel">

       <aop:pointcut id="embark" expression="execution(* * .embarkOnQuest(..))"/>     <!--定義切點-->

        <aop:after pointcut-ref="embark" method="singBeforeQuest"/>                  <!-- 聲明前置通知-->

        <aop:after pointcut-ref="embark" method="singAfterQuest"/>                    <!-- 聲明後置通知-->

    </aop:aspect>
</aop:config>

這裏使用了Spring的aop配置命名空間把Minstrel聲明爲一個切面。

在這兩種方式中,pointcut-ref屬性都引用列名爲爲「embark」的切入點,該切入點實在前面的<poiontcut>元素中定義的,並配置expression屬性來選擇所應用的通知。表達式的語法採用的是aspectJ的切點表達式語言。

Minstrel仍然是一個POJO,沒有任何代碼代表它要被做爲一個切面使用,其次最重要的是Minstrel能夠被應用到BraveKnight中,而BraveKnight不須要顯示的調用它,實際上,BraveKnight徹底不知道MInstrel的存在

public class KnightAopMain {
   public static void main(String[] args) {
       ClassPathXmlApplicationContext context =
               new ClassPathXmlApplicationContext("spring/minstrel-AOP.xml");
      Knight knight = context.getBean(Knight.class);
      //Knight knight = (Knight) context.getBean("knight");
       knight.embarkOnQuest();
       context.close();
   }
}

輸出以下:
Fa la la ,the Knight is so brabe
Embarking on quest to slay the dragon!!,順便還能夠學英語,一箭雙鵰。
Tee hee hhe,the brave knight did embark on a quest

1.1.4 小節

做者已經爲咱們展現了Spring經過面向POJO編程、DI、切面、模板技術來簡化Java開發中的複雜性。在這個工程中,展現了基於XML的配置文件中如何配置bean和切面,但這些文件是如何加載的呢?他們被加載到哪裏呢?接下來讓咱們瞭解下Spring容器,這是應用中的全部bean所駐留的地方。

相關文章
相關標籤/搜索