1. 程式人生 > >JavaWeb三層架構中Service和Dao層物件單例化可行性

JavaWeb三層架構中Service和Dao層物件單例化可行性

宣告:以下個人觀點,僅作參考; 

 

閱讀正文的前提知識:

 

一. 單例模式:

 

單例概念(百度): 單例模式,是一種常用的軟體設計模式。在它的核心結構中只包含一個被稱為單例的特殊類。通過單例模式可以保證系統中,應用該模式的類一個類只有一個例項。即一個類只有一個物件例項。

Java中的單例模式: 從專案開始到結束, 某一Java類僅產生一個例項物件; java中的例項一般通過new呼叫構造建立( 工廠模式除外 ), 為了達到單例的目的,  需要將類的構造方法私有化(private), 僅提供public方法獲得例項, 並在該方法中完成單例邏輯;

完整的單例模式示例如下:

餓漢式(類載入時,即第一次用到該類時;):

//餓漢式:建立物件例項的時候直接初始化  空間換時間
public class SingletonOne {
	//1、建立類中私有構造
	private SingletonOne(){ }
	//2、建立該型別的私有靜態例項
	private static SingletonOne instance=new SingletonOne();
	//3、建立公有靜態方法返回靜態例項物件
	public static SingletonOne getInstance(){
		return instance;
	}
}

懶漢式

(使用物件時, 實際建立該類,第一次使用較慢, 此處為保證前程安全, 且基於實用主義僅採用雙重校驗, 其它實現執行緒安全的方法還有(靜態內部類/列舉等))

//懶漢式:類內例項物件建立時並不直接初始化,直到第一次呼叫get方法時,才完成初始化操作
//時間換空間
public class SingletonTwo {
	//1、建立私有構造方法
	private SingletonTwo(){}
	//2、建立靜態的該類例項物件
	private static SingletonTwo instance=null;
	//3、建立開放的靜態方法提供例項物件
	public static SingletonTwo getInstance(){
		if (instance == null) {
			synchronized (SingletonTwo.class) {
				if (instance == null) {
					instance = new SingletonTwo();
				}
			}
		}
		return instance;
	}
}

懶漢式和餓漢式差別在於: 實際建立物件的時機不同, 基於目前Web專案中一般時間(效能)要求大於空間要求, 故: JavaWeb中個人推薦使用餓漢式, 其特點: 利用空間換時間, 專案載入完成後執行響應快, 且直接利用類載入的時機遮蔽了執行緒安全問題, 使得類的定義相對簡單;

|→→→→→→→→→→→→時間線→→→→→→→→→→→→→→→→→→→→

|   餓漢式:  類載入   建立例項    工廠方法被呼叫                    返回例項給呼叫者

|   懶漢式:  類載入                     工廠方法被呼叫   建立例項   返回例項給呼叫者

|→→→→→→→→→→→→時間線→→→→→→→→→→→→→→→→→→→→

單例模式在應用層面的特點:

1. 單例的成員變數: 多執行緒操作時, 該成員變數唯一, 故可以作為全域性變數的配置 (但是需要注意的是, 多執行緒修改時的同步問題, 可以使用同步鎖方式避免) ;

2. 單例的方法: 多執行緒操作時, 可以共享方法, 但是個各個執行緒又有自己單獨的方法區域性屬性變數, 故可以實現單個例項方法提供全域性分別使用的目的, 從而不必每個執行緒自行例項化物件再呼叫, 即節約時間又節約空間;

 

二.Java類的載入時機:

 

1. 類載入器分類:

    i. 啟動類載入器(Bootstrap ClassLoader):  載入Java核心庫, 屬於JVM一部分;

    ii. 擴充套件類載入器(Extendsion ClassLoader): 載入java擴充套件庫;

    iii. 應用程式類載入器(Application ClassLoader): 載入使用者自定義類;

    iiii.自定義類載入器: 本人不瞭解, 先不講;

2. 因為講的都是使用者自定義類,所以細講 iii 的應用程式類載入器:

    i. 一般情況下自定義類的載入時機:

        a.  new物件時;

        b. 呼叫類的靜態資源(靜態成員變數 / 靜態方法)時; 

        c. 子類載入時;

        d. 反射操作時;

        e. 帶主方法的類;

    ii. 在使用類前的適當時機, 預先主動進行類載入:

      類載入的本質是將類的位元組碼檔案放入記憶體中, 當我們需要獲取某一個類的位元組碼物件時, 自然就驅動虛擬機器去載入該類:

        a. Class.forName(類的全限定名);

        b. this.getClass().getClassLoader().loadClass();

        另外:在類中定義一個無關的靜態成員變數, 需要載入類時, 訪問一次該變數即可 (個人感覺此方法優點耍滑頭的意思0.0......) ;

-----------------------------------------------------前提知識到此結束-------------------------------------------------------------

 

正文部分:

 

三.JavaWeb三層架構中Service和Dao層物件單例化的必要性

 

JavaWeb的三層架構中, 我們需要預先啟動伺服器, 伺服器啟動完畢開始接受使用者訪問. 而使用者訪問時, 需要儘可能的快速響應.

其次JavaWeb中, Servlet預設以 單例多執行緒模式執行,即一個Servlet類僅例項化一個物件;

故:

 

首先  假設: Service和Dao層每次使用時均需要建立物件, 即: 每次客戶端請求Servlet訪問, Servlet中每個方法各自建立新的Sercice層物件, 該Sercice層物件的每個方法再各自建立新的Dao層物件. 因為每收到一個請求Servlet都會建立新的執行緒去處理, 故當前有多少個方法被呼叫, 就會相應建立多少個Service層和Dao層物件, 嚴重浪費CPU和記憶體資源, 同時降低伺服器響應速度;

然後  我們針對單個Servlet執行緒進行優化: 使得每個Servlet的執行緒中僅產生一個Service層和一個Dao層物件:

要實現該目的, 我們只需保證每個執行緒中Web層和Service層的方法共用一個Service層和Dao層物件即可, 此時僅需要將每個方法的各自例項化Service和Dao層物件提取為成員物件, 即可實現;

然後  我們針對單個Servlet物件進一步優化, 使得單個Servlet方法執行期間僅產生一個Service層和一個Dao層物件:

回顧Java整體機制, 不難找出最簡單有效的方法: 即將Service層和Dao層物件的定義變為static, 因為一個類的靜態變數僅此一份, 即可實現;

最後  實際專案中不可能只存在一個Servlet, 即使有BaseServlet幫助我們分模組分功能合併Servlet, 但大型專案功能和模組數量也不可小覷, 故需要再進一步優化, 使得所有Servlet的所有執行緒在訪問同一Service層和Dao層物件時, 使用同一個物件:

 

>補充話題背景知識部分:

或許, 此時部分讀者心中有疑問, 這樣的話, 還能正確地進行層間引數的傳遞嗎?

    答案是肯定的, 原因就是Servlet的單例多執行緒模式, 而多執行緒呼叫同一方法時, 區域性變數是每個執行緒一份的 (因為每個執行緒都有單獨的記憶體空間) . 而層間引數傳遞完全可由區域性變數+方法引數實現;

繼續最後的優化之前簡單介紹一下設計模式中的 "工廠模式":  

先講個小案例, 說明工廠模式的應用場景:

       Java中萬物皆物件思想, 相比大家都已經很熟悉, 生活中一盤菜是物件, 使用的手機也是一部物件.

        菜我們可以自己做, 因為其簡單, 我們知道使用什麼原料, 且更改原料也很簡單. 但是手機我們卻沒辦法自己製造, 故需要從網上購買, 電商售賣的手機最終由手機廠家來生產.

        而且一般我們並不關係手機的具體原材料, 僅僅關心其功能是否完善好用. 所以,手機完全可以使用不同廠家和規格的原料和不同品牌的元件, 甚至: 同一款手機,也可以使用不同的原料和配件來實現完全相同的功能.

        另外,生活中另一個問題, 我們並不是每天都自己做飯, 尤其是快節奏的今天, 此時食堂/飯館/外賣的意義就出現了, 將大眾需求進行統一處理, 降低時間和資源成本, 此處的食堂/飯館/外賣也可以看做一種工廠.

Java中, 我們在呼叫某些類的方法時, 內部實現太複雜, 呼叫者初始化起來比較困難, 僅需要功能, 或者說並不關心類的內部具體怎麼實現. 故需要類自己去處理, 並進行自我初始化, 並提供一個靜態的共有方法getInstance()返回自身的例項物件, 這就是Java中"工廠模式"達到的的目的之一: 高內聚;

另外, 在Java中, 我們Service層並不想去關心Dao層具體由哪個類實現, 以及如何實現, 就如我們並不關心手機內部天線的生產廠家一樣, 故此時我們只需要按手機型號選擇即可, 具體實現交給廠家;     這裡就引出了Java的面向介面程式設計, 我們例項化Service層和Dao層物件時, 使用該類物件的介面接收, 好比規定手機型號, 具體例項的建立交給工廠類去做(工廠類載入配置檔案....不再分講). 這裡就是Java中"工廠模式"達到的目的之二: 低耦合, 此時工廠類就是解耦的中間人;

第三, 在Java中, 我們有許多工作在每個類中都要進行相同流程的處理, 此時我們就需要流程化工作的抽取, 一般我們定義為工具類, 但在需要時也可以作為 "工廠模式" 的附加功能;

補充話題背景知識部分結束<

 

此處繼續進行擴充套件"多執行緒方法區域性變數作用域"和"工廠模式"之前的優化工作:

我們想要 "使得所有Servlet的所有執行緒在訪問同一Service層和Dao層物件時, 使用同一個物件" , 就需要用到單例模式. 即在Service層和Dao層所有類均採用單例模式, 這樣就初步實現了本階段優化的目的;

但是:

    i. 目前多各類存在高度相同的業務程式碼, 故需要進行抽取;

    ii. 考慮公司資料量陡增, 優化各層實現邏輯等,我們需要儘可能地進行層與層間解耦;

因此:

    僅考慮解耦,則只需要結合"面向介面程式設計"和"工廠模式"即可,除了在呼叫層採用介面接收之外, 工廠類定義如下:

import java.lang.reflect.InvocationTargetException;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.ResourceBundle;

public class BeanFactory2 {
    public static Object getinstance(String className){//工廠方法,返回給定類名的例項化物件
        ResourceBundle bundle = ResourceBundle.getBundle("beans");//建立ResourceBundle資源載入物件
        String classLongName = bundle.getString(className);
        try {
            return Class.forName(classLongName).getConstructor().newInstance();//例項化物件
        } catch (Exception e) {
            return null;//若報錯返回null
        }
    }
}

同時考慮解耦和單例模式: 我們抽取成工具類, 而該工具類專門用來例項化單例並返回, 同時考慮上述的解耦思想, 此時把工具類的處理邏輯作為工廠類的附加功能即可, 具體實現如下:

import java.util.Enumeration;
import java.util.HashMap;
import java.util.ResourceBundle;

public class BeanFactory {
    private static HashMap<String, Object> objectMap;//定義Map集合,存放類名和實體類Key-Value關係
    //餓漢模式
    //類載入時例項化出所有單例物件的集合
    static{
        ResourceBundle bundle = ResourceBundle.getBundle("beans");//建立ResourceBundle資源載入物件
        Enumeration<String> keys = bundle.getKeys();//獲取配置檔案中的所有Key(類名)值列舉
        //建立類Map集合,類名作為鍵,類例項物件作為值
        objectMap = new HashMap<>();//例項化Map集合
        //遍歷列舉物件, 獲取所有key值,根據key值獲取物件類的全限定名,並例項化物件存放到Map中
        while(keys.hasMoreElements()){
            String classNameStr = keys.nextElement();//獲取單個key
            String classLongName = bundle.getString(classNameStr);//獲取對應全限定名
            try {
                Object object = Class.forName(classLongName).getConstructor().newInstance();//例項化物件
                objectMap.put(classNameStr,object);//放入Map集合中
            } catch (Exception e) {
                objectMap.put(classNameStr,null);//若報錯放入null
            }
        }
    }
    public static Object getinstance(String className){//工廠方法,返回給定類名的例項化物件
        return objectMap.get(className);
    }
}

請注意: 此時因為列舉是無序的, 在使用單例模式工廠時,除非將dao層類資訊單獨配置beans屬性檔案,在service層類物件之前全部載入完畢(或使用其它方法達到類似目的), 否則不要在service層物件中定義全域性dao層物件, 此時應該每個方法分別使用getInstance方法獲取單例物件的地址來使用;

總結:

    1. 以上單例模式工廠類, 在博主的測試專案中執行正常

    2. 為了更進一步的提高專案載入的完整度, 可以利用文中方法在專案載入時, 也就是在ServletContextListener監聽器中直接使用反射預先載入單例模式工廠類, 從而優化使用者的體驗;

以上就是博主對"JavaWeb三層架構中Service和Dao層物件單例化的必要性"的論述, 即實現此處的單例化是個很有優勢的選擇, 同時附帶介紹了層間解耦思想,希望對大家有所幫助.

寫部落格唯一的目的: 本著對讀者負責的態度,要求自己以及其嚴謹的態度學習研究某一方面知識.

轉載請標明出處: 划船一哥 :https://blog.csdn.net/weixin_42711325/article/details/83340431