歡迎光臨
每天分享高質量文章

面試官:“談談Spring中都用到了那些設計樣式?”。

來自公眾號:JavaGuide

JDK 中用到了那些設計樣式?Spring 中用到了那些設計樣式?這兩個問題,在面試中比較常見。我在網上搜索了一下關於 Spring 中設計樣式的講解幾乎都是千篇一律,而且大部分都年代久遠。所以,花了幾天時間自己總結了一下,由於我的個人能力有限,文中如有任何錯誤各位都可以指出。另外,文章篇幅有限,對於設計樣式以及一些原始碼的解讀我只是一筆帶過,這篇文章的主要目的是回顧一下 Spring 中的常見的設計樣式。

Design Patterns(設計樣式) 表示面向物件軟體開發中最好的計算機程式設計實踐。 Spring 框架中廣泛使用了不同型別的設計樣式,下麵我們來看看到底有哪些設計樣式?

控制反轉(IoC)和依賴註入(DI)

IoC(Inversion of Control,控制翻轉) 是Spring 中一個非常非常重要的概念,它不是什麼技術,而是一種解耦的設計思想。它的主要目的是藉助於“第三方”(Spring 中的 IOC 容器) 實現具有依賴關係的物件之間的解耦(IOC容易管理物件,你只管使用即可),從而降低程式碼之間的耦合度。IOC 是一個原則,而不是一個樣式,以下樣式(但不限於)實現了IoC原則。

ioc-patterns

Spring IOC 容器就像是一個工廠一樣,當我們需要建立一個物件的時候,只需要配置好配置檔案/註解即可,完全不用考慮物件是如何被創建出來的。 IOC 容器負責建立物件,將物件連線在一起,配置這些物件,並從建立中處理這些物件的整個生命週期,直到它們被完全銷毀。

在實際專案中一個 Service 類如果有幾百甚至上千個類作為它的底層,我們需要實體化這個 Service,你可能要每次都要搞清這個 Service 所有底層類的建構式,這可能會把人逼瘋。如果利用 IOC 的話,你只需要配置好,然後在需要的地方取用就行了,這大大增加了專案的可維護性且降低了開發難度。關於Spring IOC 的理解,推薦看這一下知乎的一個回答:https://www.zhihu.com/question/23277575/answer/169698662 ,非常不錯。

控制翻轉怎麼理解呢? 舉個例子:”物件a 依賴了物件 b,當物件 a 需要使用 物件 b的時候必須自己去建立。但是當系統引入了 IOC 容器後, 物件a 和物件 b 之前就失去了直接的聯絡。這個時候,當物件 a 需要使用 物件 b的時候, 我們可以指定 IOC 容器去建立一個物件b註入到物件 a 中”。 物件 a 獲得依賴物件 b 的過程,由主動行為變為了被動行為,控制權翻轉,這就是控制反轉名字的由來。

DI(Dependecy Inject,依賴註入)是實現控制反轉的一種設計樣式,依賴註入就是將實體變數傳入到一個物件中去。

工廠設計樣式

Spring使用工廠樣式可以透過 BeanFactory 或 ApplicationContext 建立 bean 物件。

兩者對比:

  • BeanFactory :延遲註入(使用到某個 bean 的時候才會註入),相比於BeanFactory來說會佔用更少的記憶體,程式啟動速度更快。
  • ApplicationContext :容器啟動的時候,不管你用沒用到,一次性建立所有 bean 。BeanFactory 僅提供了最基本的依賴註入支援,ApplicationContext 擴充套件了 BeanFactory ,除了有BeanFactory的功能還有額外更多功能,所以一般開發人員使用ApplicationContext會更多。

ApplicationContext的三個實現類:

  1. ClassPathXmlApplication:把背景關係檔案當成類路徑資源。
  2. FileSystemXmlApplication:從檔案系統中的 XML 檔案載入背景關係定義資訊。
  3. XmlWebApplicationContext:從Web系統中的XML檔案載入背景關係定義資訊。

Example:

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

public class App {
    public static void main(String[] args) {
        ApplicationContext context = new FileSystemXmlApplicationContext(
                "C:/work/IOC Containers/springframework.applicationcontext/src/main/resources/bean-factory-config.xml");

        HelloApplicationContext obj = (HelloApplicationContext) context.getBean("helloApplicationContext");
        obj.getMsg();
    }
}

單例設計樣式

在我們的系統中,有一些物件其實我們只需要一個,比如說:執行緒池、快取、對話方塊、登錄檔、日誌物件、充當印表機、顯示卡等裝置驅動程式的物件。事實上,這一類物件只能有一個實體,如果製造出多個實體就可能會導致一些問題的產生,比如:程式的行為異常、資源使用過量、或者不一致性的結果。

使用單例樣式的好處:

  • 對於頻繁使用的物件,可以省略建立物件所花費的時間,這對於那些重量級物件而言,是非常可觀的一筆系統開銷;
  • 由於 new 操作的次數減少,因而對系統記憶體的使用頻率也會降低,這將減輕 GC 壓力,縮短 GC 停頓時間。

Spring 中 bean 的預設作用域就是 singleton(單例)的。 除了 singleton 作用域,Spring 中 bean 還有下麵幾種作用域:

  • prototype : 每次請求都會建立一個新的 bean 實體。
  • request : 每一次HTTP請求都會產生一個新的bean,該bean僅在當前HTTP request內有效。
  • session : 每一次HTTP請求都會產生一個新的 bean,該bean僅在當前 HTTP session 內有效。
  • global-session: 全域性session作用域,僅僅在基於portlet的web應用中才有意義,Spring5已經沒有了。Portlet是能夠生成語意程式碼(例如:HTML)片段的小型Java Web外掛。它們基於portlet容器,可以像servlet一樣處理HTTP請求。但是,與 servlet 不同,每個 portlet 都有不同的會話

Spring 實現單例的方式:

  • xml:
  • 註解:@Scope(value = "singleton")

Spring 透過 ConcurrentHashMap 實現單例登錄檔的特殊方式實現單例樣式。Spring 實現單例的核心程式碼如下:

// 透過 ConcurrentHashMap(執行緒安全) 實現單例登錄檔
private final Map singletonObjects = new ConcurrentHashMap(64);

public Object getSingleton(String beanName, ObjectFactory> singletonFactory) {
        Assert.notNull(beanName, "'beanName' must not be null");
        synchronized (this.singletonObjects) {
            // 檢查快取中是否存在實體  
            Object singletonObject = this.singletonObjects.get(beanName);
            if (singletonObject == null) {
                //...省略了很多程式碼
                try {
                    singletonObject = singletonFactory.getObject();
                }
                //...省略了很多程式碼
                // 如果實體物件在不存在,我們註冊到單例登錄檔中。
                addSingleton(beanName, singletonObject);
            }
            return (singletonObject != NULL_OBJECT ? singletonObject : null);
        }
    }
    //將物件新增到單例登錄檔
    protected void addSingleton(String beanName, Object singletonObject) {
            synchronized (this.singletonObjects) {
                this.singletonObjects.put(beanName, (singletonObject != null ? singletonObject : NULL_OBJECT));

            }
        }
}

代理設計樣式

代理樣式在 AOP 中的應用

AOP(Aspect-Oriented Programming:面向切麵程式設計)能夠將那些與業務無關,卻為業務模組所共同呼叫的邏輯或責任(例如事務處理、日誌管理、許可權控制等)封裝起來,便於減少系統的重覆程式碼,降低模組間的耦合度,並有利於未來的可拓展性和可維護性。

Spring AOP 就是基於動態代理的,如果要代理的物件,實現了某個介面,那麼Spring AOP會使用JDK Proxy,去建立代理物件,而對於沒有實現介面的物件,就無法使用 JDK Proxy 去進行代理了,這時候Spring AOP會使用Cglib ,這時候Spring AOP會使用 Cglib 生成一個被代理物件的子類來作為代理,如下圖所示:

SpringAOPProcess

當然你也可以使用 AspectJ ,Spring AOP 已經集成了AspectJ ,AspectJ 應該算的上是 Java 生態系統中最完整的 AOP 框架了。

使用 AOP 之後我們可以把一些通用功能抽象出來,在需要用到的地方直接使用即可,這樣大大簡化了程式碼量。我們需要增加新功能時也方便,這樣也提高了系統擴充套件性。日誌功能、事務管理等等場景都用到了 AOP 。

Spring AOP 和 AspectJ AOP 有什麼區別?

Spring AOP 屬於執行時增強,而 AspectJ 是編譯時增強。 Spring AOP 基於代理(Proxying),而 AspectJ 基於位元組碼操作(Bytecode Manipulation)。

Spring AOP 已經集成了 AspectJ ,AspectJ 應該算的上是 Java 生態系統中最完整的 AOP 框架了。AspectJ 相比於 Spring AOP 功能更加強大,但是 Spring AOP 相對來說更簡單,

如果我們的切麵比較少,那麼兩者效能差異不大。但是,當切麵太多的話,最好選擇 AspectJ ,它比Spring AOP 快很多。

模板方法

模板方法樣式是一種行為設計樣式,它定義一個操作中的演演算法的骨架,而將一些步驟延遲到子類中。 模板方法使得子類可以不改變一個演演算法的結構即可重定義該演演算法的某些特定步驟的實現方式。

模板方法UML圖
public abstract class Template {
    //這是我們的模板方法
    public final void TemplateMethod(){
        PrimitiveOperation1();  
        PrimitiveOperation2();
        PrimitiveOperation3();
    }

    protected void  PrimitiveOperation1(){
        //當前類實現
    }

    //被子類實現的方法
    protected abstract void PrimitiveOperation2();
    protected abstract void PrimitiveOperation3();

}
public class TemplateImpl extends Template {

    @Override
    public void PrimitiveOperation2() {
        //當前類實現
    }

    @Override
    public void PrimitiveOperation3() {
        //當前類實現
    }
}

Spring 中 jdbcTemplatehibernateTemplate 等以 Template 結尾的對資料庫操作的類,它們就使用到了模板樣式。一般情況下,我們都是使用繼承的方式來實現模板樣式,但是 Spring 並沒有使用這種方式,而是使用Callback 樣式與模板方法樣式配合,既達到了程式碼復用的效果,同時增加了靈活性。

觀察者樣式

觀察者樣式是一種物件行為型樣式。它表示的是一種物件與物件之間具有依賴關係,當一個物件發生改變的時候,這個物件所依賴的物件也會做出反應。Spring 事件驅動模型就是觀察者樣式很經典的一個應用。Spring 事件驅動模型非常有用,在很多場景都可以解耦我們的程式碼。比如我們每次新增商品的時候都需要重新更新商品索引,這個時候就可以利用觀察者樣式來解決這個問題。

Spring 事件驅動模型中的三種角色

事件角色

ApplicationEvent (org.springframework.context包下)充當事件的角色,這是一個抽象類,它繼承了java.util.EventObject並實現了 java.io.Serializable介面。

Spring 中預設存在以下事件,他們都是對 ApplicationContextEvent 的實現(繼承自ApplicationContextEvent):

  • ContextStartedEventApplicationContext 啟動後觸發的事件;
  • ContextStoppedEventApplicationContext 停止後觸發的事件;
  • ContextRefreshedEventApplicationContext 初始化或掃清完成後觸發的事件;
  • ContextClosedEventApplicationContext 關閉後觸發的事件。
ApplicationEvent-Subclass

事件監聽者角色

ApplicationListener 充當了事件監聽者角色,它是一個介面,裡面只定義了一個 onApplicationEvent()方法來處理ApplicationEventApplicationListener介面類原始碼如下,可以看出介面定義看出介面中的事件只要實現了 ApplicationEvent就可以了。所以,在 Spring中我們只要實現 ApplicationListener 介面實現 onApplicationEvent() 方法即可完成監聽事件

package org.springframework.context;
import java.util.EventListener;
@FunctionalInterface
public interface ApplicationListener<E extends ApplicationEventextends EventListener {
    void onApplicationEvent(E var1);
}

事件釋出者角色

ApplicationEventPublisher 充當了事件的釋出者,它也是一個介面。

@FunctionalInterface
public interface ApplicationEventPublisher {
    default void publishEvent(ApplicationEvent event) {
        this.publishEvent((Object)event);
    }

    void publishEvent(Object var1);
}

ApplicationEventPublisher 介面的publishEvent()這個方法在AbstractApplicationContext類中被實現,閱讀這個方法的實現,你會發現實際上事件真正是透過ApplicationEventMulticaster來廣播出去的。具體內容過多,就不在這裡分析了,後面可能會單獨寫一篇文章提到。

Spring 的事件流程總結

  1. 定義一個事件: 實現一個繼承自 ApplicationEvent,並且寫相應的建構式;
  2. 定義一個事件監聽者:實現 ApplicationListener 介面,重寫 onApplicationEvent() 方法;
  3. 使用事件釋出者釋出訊息: 可以透過 ApplicationEventPublisher 的 publishEvent() 方法釋出訊息。

Example:

// 定義一個事件,繼承自ApplicationEvent並且寫相應的建構式
public class DemoEvent extends ApplicationEvent{
    private static final long serialVersionUID = 1L;

    private String message;

    public DemoEvent(Object source,String message){
        super(source);
        this.message = message;
    }

    public String getMessage() {
         return message;
          }


// 定義一個事件監聽者,實現ApplicationListener介面,重寫 onApplicationEvent() 方法;
@Component
public class DemoListener implements ApplicationListener<DemoEvent>{

    //使用onApplicationEvent接收訊息
    @Override
    public void onApplicationEvent(DemoEvent event) {
        String msg = event.getMessage();
        System.out.println("接收到的資訊是:"+msg);
    }

}
// 釋出事件,可以透過ApplicationEventPublisher  的 publishEvent() 方法釋出訊息。
@Component
public class DemoPublisher {

    @Autowired
    ApplicationContext applicationContext;

    public void publish(String message){
        //釋出事件
        applicationContext.publishEvent(new DemoEvent(this, message));
    }
}

當呼叫 DemoPublisher 的 publish() 方法的時候,比如 demoPublisher.publish("你好") ,控制檯就會打印出:接收到的資訊是:你好 。

配接器樣式

配接器樣式(Adapter Pattern) 將一個介面轉換成客戶希望的另一個介面,配接器樣式使介面不相容的那些類可以一起工作,其別名為包裝器(Wrapper)。

spring AOP中的配接器樣式

我們知道 Spring AOP 的實現是基於代理樣式,但是 Spring AOP 的增強或通知(Advice)使用到了配接器樣式,與之相關的介面是AdvisorAdapter 。Advice 常用的型別有:BeforeAdvice(標的方法呼叫前,前置通知)、AfterAdvice(標的方法呼叫後,後置通知)、AfterReturningAdvice(標的方法執行結束後,return之前)等等。每個型別Advice(通知)都有對應的攔截器:MethodBeforeAdviceInterceptorAfterReturningAdviceAdapterAfterReturningAdviceInterceptor。Spring預定義的通知要透過對應的配接器,適配成 MethodInterceptor介面(方法攔截器)型別的物件(如:MethodBeforeAdviceInterceptor 負責適配 MethodBeforeAdvice)。

spring MVC中的配接器樣式

在Spring MVC中,DispatcherServlet 根據請求資訊呼叫 HandlerMapping,解析請求對應的 Handler。解析到對應的 Handler(也就是我們平常說的 Controller 控制器)後,開始由HandlerAdapter 配接器處理。HandlerAdapter 作為期望介面,具體的配接器實現類用於對標的類進行適配,Controller 作為需要適配的類。

為什麼要在 Spring MVC 中使用配接器樣式? Spring MVC 中的 Controller 種類眾多,不同型別的 Controller 透過不同的方法來對請求進行處理。如果不利用配接器樣式的話,DispatcherServlet 直接獲取對應型別的 Controller,需要的自行來判斷,像下麵這段程式碼一樣:

if(mappedHandler.getHandler() instanceof MultiActionController){  
   ((MultiActionController)mappedHandler.getHandler()).xxx  
}else if(mappedHandler.getHandler() instanceof XXX){  
    ...  
}else if(...){  
   ...  
}  

假如我們再增加一個 Controller型別就要在上面程式碼中再加入一行 判斷陳述句,這種形式就使得程式難以維護,也違反了設計樣式中的開閉原則 – 對擴充套件開放,對修改關閉。

裝飾者樣式

裝飾者樣式可以動態地給物件新增一些額外的屬性或行為。相比於使用繼承,裝飾者樣式更加靈活。簡單點兒說就是當我們需要修改原有的功能,但我們又不願直接去修改原有的程式碼時,設計一個Decorator套在原有程式碼外面。其實在 JDK 中就有很多地方用到了裝飾者樣式,比如 InputStream家族,InputStream 類下有 FileInputStream (讀取檔案)、BufferedInputStream (增加快取,使讀取檔案速度大大提升)等子類都在不修改InputStream 程式碼的情況下擴充套件了它的功能。

裝飾者樣式示意圖

Spring 中配置 DataSource 的時候,DataSource 可能是不同的資料庫和資料源。我們能否根據客戶的需求在少修改原有類的程式碼下動態切換不同的資料源?這個時候就要用到裝飾者樣式(這一點我自己還沒太理解具體原理)。Spring 中用到的包裝器樣式在類名上含有 Wrapper或者 Decorator。這些類基本上都是動態地給一個物件新增一些額外的職責

總結

Spring 框架中用到了哪些設計樣式:

  • 工廠設計樣式 : Spring使用工廠樣式透過 BeanFactoryApplicationContext 建立 bean 物件。
  • 代理設計樣式 : Spring AOP 功能的實現。
  • 單例設計樣式 : Spring 中的 Bean 預設都是單例的。
  • 模板方法樣式 : Spring 中 jdbcTemplatehibernateTemplate 等以 Template 結尾的對資料庫操作的類,它們就使用到了模板樣式。
  • 包裝器設計樣式 : 我們的專案需要連線多個資料庫,而且不同的客戶在每次訪問中根據需要會去訪問不同的資料庫。這種樣式讓我們可以根據客戶的需求能夠動態切換不同的資料源。
  • 觀察者樣式: Spring 事件驅動模型就是觀察者樣式很經典的一個應用。
  • 配接器樣式 :Spring AOP 的增強或通知(Advice)使用到了配接器樣式、spring MVC 中也是用到了配接器樣式適配Controller
  • ……

參考

  • 《Spring技術內幕》
  • https://blog.eduonix.com/java-programming-2/learn-design-patterns-used-spring-framework/
  • http://blog.yeamin.top/2018/03/27/單例樣式-Spring單例實現原理分析/
  • https://www.tutorialsteacher.com/ioc/inversion-of-control
  • https://design-patterns.readthedocs.io/zh_CN/latest/behavioral_patterns/observer.html
  • https://juejin.im/post/5a8eb261f265da4e9e307230
  • https://juejin.im/post/5ba28986f265da0abc2b6084
贊(0)

分享創造快樂