1. 程式人生 > >Spring實戰 | 第一部分 Spring的核心(第一章 Spring之旅)

Spring實戰 | 第一部分 Spring的核心(第一章 Spring之旅)

為了降低Spring開發的複雜性,Spring採取了以下4鐘關鍵策略:

  • 基於POJO的輕量級和最小侵入性程式設計;
  • 通過依賴注入和麵向介面實現鬆耦合;
  • 基於切面和慣性進行宣告式程式設計;
  • 通過切面和模板減少樣式程式碼。

1、激發POJO潛能(POJO即普通的java類)

2、依賴注入(dependency injection DI)

依賴注入這個詞讓人望而生畏,現在已經演變成一項複雜的程式設計技巧或設計模式理念。但事實證明依賴注入並不像它聽上去那麼複雜,在專案中應用DI,你會發現你的程式碼會變得異常簡單而且更容易理解和測試。

① DI功能是如何實現的

程式都是由多個類組合而成的,這些類之間進行協作完成特定的業務邏輯。按照傳統的做法,每個物件負責管理與自己相互協作的物件(即它所依賴的物件)的引用,這將會導致高度耦合和難以測試的程式碼。

② 耦合具有兩面性。

一方面,緊耦合的程式碼難以測試、難以複用、難以理解,並且典型地表現出“打地鼠”式的bug特性(修復一個bug,將會出現一個或者更多的新bug)。另一方面,一定程式的耦合又是必須的,完全沒有耦合的程式碼啥也做不了。為了完成有實際意義的功能,不同的類必須以適當的方式進行互動。總而言之,耦合是必須的,但應當被小心謹慎的管理。

通過DI,物件的依賴關係將由系統中負責協調各隊的第三方元件在常見物件的時候進行設定。物件無需自行建立或管理他們的依賴關係,如下圖,依賴關係將自動注入到需要他們的物件中去。

③ 依賴注入的方式

spring支援3種依賴注入的方式

  • 屬性注入

屬性注入即通過setter方法注入bean的屬性值或依賴的物件

屬性注入使用元素,使用name屬性指定bean的屬性名稱,value屬性或子節點屬性值

屬性注入是實際開發中最常見的注入方式

public void setName(String name)
{
    System.out.println("setName:"+name);
    this.name=name;
}
<bean id="helloWorld" class="spring.bean.HelloWorld">
    <property name="name" value="Spring"></property>
</bean>
  • 構造器注入

通過構造方法注入bean的屬性值或者依賴的物件(引用),保證了bean例項在例項化後就可以使用

構造器注入在元素裡宣告屬性,沒有name屬性

建立一個People物件

package com.container;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class People {
        private String name;
        private String sex;
        private int age;

        public People(String name, int age) {
            this.name = name;
            this.age = age;
        }

        public People(String name, String sex) {
            this.name = name;
            this.sex = sex;
        }

        @Override
        public String toString() {
            return "people{" +
                    "name='" + name + '\'' +
                    ", sex='" + sex + '\'' +
                    ", age=" + age +
                    '}';
        }

        public static void main(String[] args) {
            ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-config.xml");
            People people1 = (com.container.People) applicationContext.getBean("people1");
            System.out.println(people1);
            People people2 = (com.container.People) applicationContext.getBean("people2");
            System.out.println(people2);
        }
    }
<?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="people1" class="com.container.People">
        <constructor-arg value="江疏影" type="java.lang.String"></constructor-arg>
        <constructor-arg value="20" type="int"></constructor-arg>
    </bean>
    <bean id="people2" class="com.container.People">
        <constructor-arg value="江疏影" type="java.lang.String"></constructor-arg>
        <constructor-arg value="man" type="java.lang.String"></constructor-arg>
    </bean>
</beans>

  • 工廠方法注入(知道就行,不推薦)

3、應用切面

DI能夠讓相互協作的軟體元件保持鬆散耦合,而面向切面程式設計(aspect-oriented programming,AOP)允許你把遍佈應用各處的功能分離出來形成可重用的元件。

面向切面程式設計往往被定義為促使軟體系統實現關注點分離的一項技術。系統由許多不同的元件組成,每一個元件各負責一塊特定功能。除了實現自身核心的功能外,這些元件還經常承擔著額外的職責。諸如日誌、事務管理和安全這樣的系統服務也會融入到核心業務元件中,這些系統服務通常被稱為橫切關注點,因為它們會跨越系統的多個元件。

如果將這些關注點全部分散到多個元件中去,你的程式碼將會帶來雙重的複雜性。

  • 實現系統關注點功能的程式碼將會重複的出現在多個元件中,這就意味著如果你要改變這些關注點的邏輯,必須修改各個模組中的相關實現。即使你把關注點物件抽象為一個獨立的模組,其它模組只要呼叫它的方法,但方法的呼叫還是會出重複出現在各個模組中。
  • 元件會因為那些與自身核心業務無關的程式碼而變得混亂。一個向地址簿增加地址條目的方法應該只關注如何新增地址,而不應該關注它是不是安全的或者是否需要支援事務。

下圖展示了這種複雜性,左邊的業務程式碼與系統服務結合的過於緊密。每個物件不但要知道它需要記日誌、進行安全控制和參與事務,還要親自執行這些服務。

AOP能夠使這些服務模組化,並以宣告的方式將它們應用到它們需要影響的元件中去。而造成的結果就是這些元件會具有更高的內聚性並且會更加關注自身的業務,完全不需要了解設計系統服務所帶來的複雜性。總之,AOP能夠確保POJO的簡單性。

如下圖所示,我們可以把切面想象為覆蓋在很多元件之上的一個外殼。應用是由那些實現各自業務功能的模組組成,藉助AOP,可以使用各種功能層去包裹核心業務層,這些層以宣告的方式靈活的應用在系統中,你的核心應用甚至不知道他們的存在,這是一個非常強大的理念,可以將安全、事務和日誌關注點與核心業務邏輯相分離。

通過少量的XML配置,宣告一個Spring切面。

4、使用模組消除樣板式程式碼

二、Spring容器

Spring容器,顧名思義是用來容納東西的,裝的就是Bean。Spring容器負責建立、配置、管理Bean。spring容器有兩個核心介面:BeanFactory和ApplicationContext介面,後者是前者的子介面。在基於spring的Java EE程式中,所有的元件都被當成Bean來處理,包括資料來源物件、hibernate的sessionFactory、事務管理等,程式中的所有Java類都可以被當成spring容器中的bean。

在基於Spring的應用中,你的應用物件生存於Spring容器(container)中。如下圖所示,Spring容器負責建立物件,裝配它們,配置它們並管理它們的整個生命週期,從生存到死亡(在這裡可能就是new到finalize())。

1、spring容器

spring容器的核心介面是BeanFactory,它有一個子介面就是ApplicationContext。ApplicationContext也被稱為spring上下文。

呼叫者只需要使用getBean()方法即可獲得指定bean的引用。對於大部分的Java程式而言,使用ApplicationContext作為spring容易更為方便。其常用的實現類有FileSystemXmlApplicationContext、ClassPathXmlApplicationContext和AnnotationConfigXmlApplicationContext。如果Java web中使用spring容器,則通常有XmlWebApplicationContext、AnnotationConfigWebApplicationContext兩個容器。

建立spring容器的例項時,必須提供spring容器管理的bean的配置檔案,也就是我們常說的spring.xml配置檔案。因此在建立beanFactory時配置檔案作為引數傳入。xml配置檔案一般以resource物件傳入。resource是spring提供的資源訪問介面,通過該介面spring更簡單、透明的訪問磁碟,網路系統和類路徑上的相關資源。

對於獨立的Java EE應用程式,可以通過如下方法來例項化BeanFactory。

//在當前專案類路徑下搜尋配置檔案
ApplicationContext appContext = new ClassPathXmlApplicationContext("beans_7_3_3.xml");
//在檔案系統搜尋配置檔案
appContext = new FileSystemXmlApplicationContext("D:\\spring-tool-workspace\\myspring\\src\\beans_7_3_3.xml");
//獲取chinese的Bean,並且返回的型別為Chinese
Person chinese = appContext.getBean("chinese", Chinese.class);
chinese.useAxe();

2、使用ApplicationContext

大部分時間,都不會使用beanFactory例項作為spring容器,而是使用ApplicationContext作為spring容器,因此spring容器也被稱為spring上下文。ApplicationContext增強了beanFactory的功能,提供了很多有用、方便開發的功能。

在web中可以利用如contextLoader的支援類,在web應用啟動的時候自動建立ApplicationContext。

除了提供beanFactory所支援的全部功能外,application還額外的提供如下功能:

① ApplicationContext會預設初始化所有的singleton bean(單例bean),也可以通過配置取消。

② ApplicationContext繼承了messageSource介面,因此提供國際化支援。

③ 資源訪問,比如URL和檔案。

④ 事件機制。

⑤ 同時載入多個配置檔案。

⑥ 以宣告式方式啟動並建立spring容器。

ApplicationContext包括beanFactory的所有功能,並提供了一些額外的功能,優先使用ApplicationContext。對於在記憶體消耗的才使用beanFactory。

當系統建立ApplicationContext容器時,會預設初始化singleton bean,包括呼叫構造器建立該bean的例項,通過元素驅動spring呼叫setting方法注入所依賴的物件。這就意味著,系統前期建立ApplicationContext會有很大的開銷,但是一旦初始化完成後面獲取bean例項就會擁有較好的效能。為了阻止在使用ApplicationContext作為spring容器初始化singleton bean可以在元素新增lazy-init="true"屬性。

3、ApplicationContext的國際化支援

ApplicationContext介面繼承了MessageSource介面,因此具備國際化功能。

//MessageSource介面提供的國際化的兩個方法
String getMessage(String code, Object [] args, Locale loc){
}
String getMessage(String code, Object[]args, String default, Locale loc){
}

spring國際化的支援,其實是建立在Java國際化的基礎上的。其核心思路將程式中需要國際化的訊息寫入資原始檔,而程式碼中僅僅使用國際化資訊響應的key。

4、ApplicationContext的事件機制

ApplicationContext的事件機制是觀察者設計模式的實現。通過ApplicationEvent和ApplicationListener介面實現,前者是被觀察者,後者是觀察者。

spring事件框架有兩個核心的介面:

ApplicationEvent(事件):必須由ApplicationContext來發布。

ApplicationListener(監聽器):實現了此介面就可以擔任容器中的監聽器bean。

實際上,spring的事件機制是由事件(實現ApplicationEvent介面的類)、事件源(也就是spring容器,並且有Java程式碼顯示的觸發)、監聽器(ApplicationListener介面實現類)。這就像我們在頁面點選一個button。button是事件源,單機的這個動作就是事件,處理函式就是監聽器。

以下程式碼演示spring事件機制:

import org.springframework.context.ApplicationEvent;

public class EmailEvent extends ApplicationEvent{
    private String address;
    private String text;
    public EmailEvent(Object source) {
        super(source);
    }

    public EmailEvent(Object source, String address, String text) {
        super(source);
        this.address = address;
        this.text = text;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    public String getText() {
        return text;
    }

    public void setText(String text) {
        this.text = text;
    }
}
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class EmailNotifier implements ApplicationListener {
    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        //處理email事件
        if(event instanceof EmailEvent){
            EmailEvent email = (EmailEvent) event;
            System.out.println(email.getAddress()+"  "+email.getText());
        }else {
            //輸出spring容器的內建事件
            System.out.println("其它事件:"+event);
        }
    }

    public static void main(String[] args) {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans_7_4_4.xml");
        EmailEvent emailEvent = applicationContext.getBean("emailEvent",EmailEvent.class);
        applicationContext.publishEvent(emailEvent);
    }
}
<?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 class="EmailNotifier"></bean>
    <bean id="emailEvent" class="EmailEvent">
        <constructor-arg value="test"></constructor-arg>
        <constructor-arg value="[email protected]"></constructor-arg>
        <constructor-arg value="this is a test"></constructor-arg>
    </bean>
</beans>

從上面的程式碼可以看出,事件監聽器不僅監聽到了我們程式顯示觸發的事件,還監聽了spring容器內建的事件。如果實際開發需要,我們可以在spring容器初始化或銷燬時回撥自定義方法,就可以通過上面的事件監聽機制來完成。

spring提供瞭如下幾個內建物件:

ContextRefreshedEvent、ContextStartedEvent、ContextClosedEvent、ContextStoppedEvent、RequestHandledEvent。

5、讓bean獲取spring容器

上面都是通過ApplicationContext建立spring容器,再呼叫spring容器的getBean()方法獲取bean。這種情況下,程式總是持有spring容器的引用。但是在web應用中,我們可以用宣告式的方法來建立spring容器:在web.xml檔案中配置一個監聽,讓這個監聽類幫我們來建立spring容器,前端MVC框架直接呼叫bean,使用依賴注入功能,無需訪問spring容器本身。

在某些特殊情況下,bean需要實現某個功能(比如:bean需要輸出國際化資訊,或向spring容器釋出事件),這些功能都需要藉助spring容器來完成。就是說我們需要將spring容器作為一個bean來注入到其它bean中,只不過spring容器bean是一個容器級別的bean。

為了讓bean獲取它所在容器的引用,可以讓bean實現beanFactoryAware介面。該介面只有一個方法setBeanFactory(BeanFactory beanFactory)方法,方法的beanFactory引數指向spring容器,會由spring容器注入。我們bean中定義一個setter方法後,通常都是由在配置檔案中配置元素來驅動spring容器來注入依賴bean的,但是這裡我們並沒有這樣做,這是因為一個bean如果實現了beanFactory介面,spring在建立該bean時,會自動注入spring容器本身。與beanFactoryAware介面類似的還有BeanNameAware、ResourceLoaderAware介面,這些介面都會提供類似的setter方法,這些方法會由spring容器來注入。

6、bean的生命週期

正確理解Spring bean的生命週期非常重要,因為你或許要利用Spring提供的擴充套件點來自定義bean的建立過程。下圖展示了bean裝載到Spring應用上下文中的一個典型的生命週期過程。

正如你所見,在bean準備就緒前,bean工廠執行了若干啟動步驟。

現在你已經瞭解瞭如何建立和載入一個Spring容器。但是一個空的容器並沒有太大的價值,在你把東西放進去之前,它什麼也沒有。為了從Spring的DI中受益,我們必須將應用物件裝配進Spring容器中。我們將在第二章對bean裝配進行更詳細的探討。

我們現在首先瀏覽一下Spring的體系結果,瞭解一下Spring框架的基本組成部分和最新版本的Spring所釋出的新特性。

三、俯瞰Spring風景線

未完待