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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章