聲明:以下僅個人觀點,不做實際項目運行的參考;
閱讀正文的前提知識:
一. 單例模式:
單例概念(百度): 單例模式,是一種常用的軟件設計模式。在它的核心結構中只包含一個被稱爲單例的特殊類。通過單例模式可以保證系統中,應用該模式的類一個類只有一個實例。即一個類只有一個對象實例。
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