《設計模式》之一文帶你理解單例、JDK動態代理、CGLIB動態代理、靜態代理
個人認為我在動態代理方面的分析算是比較深入了,下次更新再修改一下,爭取做到最好,後續還有建造者模式、模板方法、介面卡、外觀、責任鏈、策略和原型模式的深入!各位讀者如果覺得還不錯的可以持續關注哦。謝謝各位!!!
我的github,到時上傳例子程式碼
https://github.com/tihomcode
《設計模式》之一文帶你理解建造者模式、模板方法、介面卡模式、外觀模式
設計模式
設計模式的六大原則
開閉原則(Open Close Principle)
開閉原則就是說對擴充套件開放,對修改關閉。在程式需要進行拓展的時候,不能去修改原有的程式碼,實現一個熱插拔的效果。所以一句話概括就是:為了使程式的擴充套件性好,易於維護和升級。想要達到這樣的效果,我們需要使用介面和抽象類,後面的具體設計中我們會提到這點。
能夠提供擴充套件性
里氏代換原則(Liskov Substitution Principle)
里氏代換原則(Liskov Substitution Principle LSP)面向物件設計的基本原則之一。 里氏代換原則中說,任何基類可以出現的地方,子類一定可以出現。 LSP是繼承複用的基石,只有當衍生類可以替換掉基類,軟體單位的功能不受到影響時,基類才能真正被複用,而衍生類也能夠在基類的基礎上增加新的行為。里氏代換原則是對“開-閉”原則的補充。實現“開-閉”原則的關鍵步驟就是抽象化。而基類與子類的繼承關係就是抽象化的具體實現,所以里氏代換原則是對實現抽象化的具體步驟的規範。—— From Baidu 百科
重寫、繼承
依賴倒轉原則(Dependence Inversion Principle)
這個是開閉原則的基礎,具體內容:真對介面程式設計,依賴於抽象而不依賴於具體。
面向介面程式設計
介面隔離原則(Interface Segregation Principle)
這個原則的意思是:使用多個隔離的介面,比使用單個介面要好。還是一個降低類之間的耦合度的意思,從這兒我們看出,其實設計模式就是一個軟體的設計思想,從大型軟體架構出發,為了升級和維護方便。所以上文中多次出現:降低依賴,降低耦合。
迪米特法則(最少知道原則)(Demeter Principle)
為什麼叫最少知道原則,就是說:一個實體應當儘量少的與其他實體之間發生相互作用,使得系統功能模組相對獨立。
減少模組之間的相互依賴關係
合成複用原則(Composite Reuse Principle)
原則是儘量使用合成/聚合的方式,而不是使用繼承。
單例
什麼是單例
保證在一個JVM中只能存在一個例項,保證物件的唯一性。
必須自行建立這個例項
必須自行向整個程式提供這個例項
應用場景
-
Windows的Task Manager(工作管理員)就是很典型的單例模式(這個很熟悉吧),想想看,是不是呢,你能開啟兩個windows task manager嗎?
-
windows的Recycle Bin(回收站)也是典型的單例應用。在整個系統執行過程中,回收站一直維護著僅有的一個例項。
-
網站的計數器,一般也是採用單例模式實現,否則難以同步。
-
應用程式的日誌應用,一般使用單例模式實現,這一般是由於共享的日誌檔案一直處於開啟狀態,因為只能有一個例項去操作,否則內容不好追加。
-
Web應用的配置物件的讀取,一般也應用單例模式,這個是由於配置檔案是共享的資源。
-
資料庫連線池的設計一般也是採用單例模式,因為資料庫連線是一種資料庫資源。資料庫軟體系統中使用資料庫連線池,主要是節省開啟或者關閉資料庫連線所引起的效率損耗,這種效率上的損耗還是非常昂貴的,因為何用單例模式來維護,就可以大大降低這種損耗。
-
多執行緒的執行緒池的設計一般也是採用單例模式,這是由於執行緒池要方便對池中的執行緒進行控制。
-
作業系統的檔案系統,也是大的單例模式實現的具體例子,一個作業系統只能有一個檔案系統。
-
HttpApplication 也是單位例的典型應用。熟悉ASP.Net(IIS)的整個請求生命週期的人應該知道HttpApplication也是單例模式,所有的HttpModule都共享一個HttpApplication例項.
單例的優缺點
優點:節約記憶體、重複利用、方便管理
缺點:執行緒安全問題
單例建立方式
餓漢式
類初始化時,會立即載入該物件,執行緒天生安全,呼叫效率高。
public class HungryType {
private static final HungryType hungryType = new HungryType();
private HungryType() {
}
public HungryType getInstance() {
return hungryType;
}
}
public class HungryTest {
public static void main(String[] args) {
HungryType hungryType1 = HungryType.getInstance();
HungryType hungryType2 = HungryType.getInstance();
System.out.println(hungryType1==hungryType2); //true
}
}
因為使用了static final保證了唯一性,不可修改,且已經例項化過了,所以呼叫的時候只能拿到唯一的一個hungryType。
懶漢式
類初始化時,不會初始化該物件,真正需要使用的時候才會建立該物件,具備懶載入功能,天生執行緒不安全,需要解決執行緒安全問題,所以效率比較低。
public class LazyType {
private static LazyType lazyType;
private LazyType() {
}
//執行緒安全問題,如果多個執行緒訪問lazyType的時候,可能會建立多個物件,那麼就與單例原則違背了
public static synchronized LazyType getInstance() {
if(lazyType==null) {
lazyType = new LazyType();
}
return lazyType;
}
}
public class LazyTest {
public static void main(String[] args) {
LazyType lazyType1 = LazyType.getInstance();
LazyType lazyType2 = LazyType.getInstance();
System.out.println(lazyType1==lazyType2); //true
}
}
為了解決執行緒安全問題,在方法上加上synchronized關鍵字,但是這樣就使效率下降了。
靜態內部類
結合了懶漢式和餓漢式各自的優點,真正需要物件的時候才會載入,載入類是執行緒安全的。
public class StaticInnerClassType {
private StaticInnerClassType() {
System.out.println("初始化。。。");
}
public static class SingletonClassInstance {
private static final StaticInnerClassType staticInnerClassType = new StaticInnerClassType();
}
//方法沒有同步
public static StaticInnerClassType getInstance() {
System.out.println("getInstance()");
return SingletonClassInstance.staticInnerClassType;
}
}
public class StaticInnerClassTest {
public static void main(String[] args) {
StaticInnerClassType staticInnerClassType1 = StaticInnerClassType.getInstance();
StaticInnerClassType staticInnerClassType2 = StaticInnerClassType.getInstance();
System.out.println(staticInnerClassType1==staticInnerClassType2);
}
}
缺點:需要兩個類去做到這一點,雖然不會建立靜態內部類的物件,但是其 Class 物件還是會被建立,而且是屬於永久區的物件
列舉單例
使用列舉實現單例模式,優點是實現簡單、呼叫效率高,列舉本身就是單例,由JVM從根本上提供保障!避免通過反射和反序列化的漏洞, 缺點是沒有延遲載入。
public class EnumType {
private EnumType() {
}
public static EnumType getInstance() {
return SingletonEnum.INSTANCE.getInstance();
}
static enum SingletonEnum{
INSTANCE;
private EnumType enumType;
private SingletonEnum() {
enumType = new EnumType();
}
public EnumType getInstance() {
return this.enumType;
}
}
}
public class EnumTest {
public static void main(String[] args) {
EnumType enumType1 = EnumType.getInstance();
EnumType enumType2 = EnumType.getInstance();
System.out.println(enumType1==enumType2); //true
}
}
雙重檢測鎖式
因為JVM本質重排序的原因,可能會初始化多次,不推薦使用
public class DoubleDetectionLockType {
//保證可見性,volatile本身可以禁止重排序,但是有部落格說加上後也沒用
private static volatile DoubleDetectionLockType doubleDetectionLockType;
private DoubleDetectionLockType() {
}
public static DoubleDetectionLockType getInstance() {
if(doubleDetectionLockType==null) {
synchronized (DoubleDetectionLockType.class) {
if(doubleDetectionLockType==null) {
//當在new的時候,會進行很多步驟,比如複製記憶體、進行賦值,底層可能會進行重排序
//在多執行緒的情況下,可能本來已經被賦了值,在優化後可能會重複建立
doubleDetectionLockType = new DoubleDetectionLockType();
}
}
}
return doubleDetectionLockType;
}
}
public class DoubleDetectionLockTest {
public static void main(String[] args) {
DoubleDetectionLockType doubleDetectionLockType1 = DoubleDetectionLockType.getInstance();
DoubleDetectionLockType doubleDetectionLockType2 = DoubleDetectionLockType.getInstance();
System.out.println(doubleDetectionLockType1==doubleDetectionLockType2); //true
}
}
單例防止反射漏洞攻擊
在建構函式中,只能允許初始化一次即可解決
public class PreventAttackSingleton {
private static boolean flag = false;
private PreventAttackSingleton() {
if (flag==false) {
flag = !flag;
} else {
throw new RuntimeException("單例模式被侵犯!");
}
}
.....
}
單例建立方式的選擇
如果不需要延遲載入單例,可以使用列舉或者餓漢式,相對來說列舉好於餓漢式。
如果需要延遲載入,可以使用靜態內部類或者懶漢式,相對來說靜態內部類好於懶漢式。
初始化時最好使用的餓漢式,比如讀取配置檔案、Spring初始化。
工廠模式
工廠模式實現了建立者和呼叫者分離,工廠模式分為簡單工廠、工廠方法、抽象工廠模式
工廠模式是我們最常用的例項化物件模式了,是用工廠方法代替new操作的一種模式。利用工廠模式可以降低程式的耦合性,為後期的維護修改提供了很大的便利。將選擇實現類、建立物件統一管理和控制。從而將呼叫者跟我們的實現類解耦。
簡單工廠模式
優點:簡單工廠模式能夠根據外界給定的資訊,決定究竟應該建立哪個具體類的物件。明確區分了各自的職責和權力,有利於整個軟體體系結構的優化。
缺點:很明顯工廠類集中了所有例項的建立邏輯,容易違反GRASPR的高內聚的責任分配原則
工廠方法模式
工廠方法模式Factory Method,又稱多型性工廠模式。在工廠方法模式中,核心的工廠類不再負責所有的產品的建立,而是將具體建立的工作交給子類去做。該核心類成為一個抽象工廠角色,僅負責給出具體工廠子類必須實現的介面,而不接觸哪一個產品類應當被例項化這種細節。
抽象工廠模式
小結和區別
簡單工廠 : 用來生產同一等級結構中的任意產品。(不支援拓展增加產品),就是隻有一個汽車工廠,根據名稱生產不同的車
工廠方法 :用來生產同一等級結構中的固定產品。(支援拓展增加產品),就是分多個汽車工廠,有奧迪工廠、賓士工廠、寶馬工廠,想生產什麼車就到這些不同品牌的工廠去建立
抽象工廠 :用來生產不同產品族的全部產品。(不支援拓展增加產品;支援增加產品族),就是有很多汽車工廠,但是每個汽車工廠下又有不同發動機、座椅,所以在寶馬工廠裡就實現發動機A和座椅B、在賓士工廠裡就實現發動機B和座椅A
個人理解:
抽象工廠模式和工廠模式的區別是在於不同的維度。抽象工廠是多維的,舉個例子,汽車分為小轎車、越野車、火車這幾種型別,不同的型別下又有不同的規格,比如以小轎車來說:2…0排量的小駕車和2.4排量的小車,這是不同的規格。所以在設計抽象工廠介面時,抽象工廠所抽象是轎車的型別,抽象工廠的實現是提供生產多種轎車型別的例項。這個例項再去提供生產不同規格轎車的例項。而工廠模式是單維對的,只提供一種型別多種規格的轎車。
代理模式
通過代理控制物件的訪問,可以詳細訪問某個物件的方法,在這個方法呼叫處理之前或呼叫後處理。既(AOP的微實現),AOP核心技術面向切面程式設計。
在沒有代理的時候我們想去實現在方法前後加上一些處理時我們最直觀的想法就是使用繼承和聚合。繼承實現要擴充套件方法的類,複寫它的方法;聚合是重新定義一個類,實現介面並且引入要擴充套件方法的類,再對方法內部進行修改,這裡可能講的比較抽象。
靜態代理
靜態代理是需要生成代理物件的
//介面
public interface IUserDao {
public void add();
}
//實現類
public class UserDaoImpl implements IUserDao {
public void add() {
System.out.println("add方法。。。");
}
}
//代理類
public class UserDaoProxy implements IUserDao {
private IUserDao iUserDao;
public UserDaoProxy(IUserDao iUserDao) {
this.iUserDao = iUserDao;
}
public void add() {
System.out.println("開啟事務");
iUserDao.add();
System.out.println("關閉事務");
}
}
//main方法
public class ClientDemo {
public static void main(String[] args) {
//未代理
IUserDao userDao = new UserDaoImpl();
userDao.add();
//靜態代理
UserDaoProxy userDaoProxy = new UserDaoProxy(userDao);
userDaoProxy.add();
}
}
一個介面對應一個代理類,那麼業務量大的時候,可能要寫幾百個代理類,而且每增加一個方法就要修改程式碼,使得程式碼不易擴充套件且很冗餘,一般不使用靜態代理
動態代理
那麼動態代理通俗點說就是用一個類來動態的實現擴充套件,不需要重複修改或建立代理類。
可能我們會先想到反射機制,但是反射只能獲取例項物件的屬性和方法,並對其進行呼叫和賦值,並不能實現在方法前後進行擴充套件。
所以如果我們可以動態生成Proxy類,並且動態編譯。然後,再通過反射建立物件並載入到記憶體中,不就實現了對任意物件進行代理了嗎
JDK動態代理
原理:是根據類載入器和介面建立代理類(此代理類是介面的實現類,所以必須使用介面,面向介面生成代理,位於java.lang.reflect包下)
實現方式:
-
通過實現InvocationHandler介面建立自己的呼叫處理器 IvocationHandler handler = new InvocationHandlerImpl(…);
-
通過為Proxy類指定ClassLoader物件和一組interface建立動態代理類Class clazz = Proxy.getProxyClass(classLoader,new Class[]{…});
-
通過反射機制獲取動態代理類的建構函式,其引數型別是呼叫處理器介面型別Constructor constructor = clazz.getConstructor(new Class[]{InvocationHandler.class});
-
通過建構函式建立代理類例項,此時需將呼叫處理器物件作為引數被傳入Interface Proxy = (Interface)constructor.newInstance(new Object[] (handler));
/**
* 每次生成動態代理物件時,實現InvocationHandler介面的呼叫處理器物件
* @author TiHom
* create at 2018/11/10 0010.
*/
public class InvocationHandlerImpl implements InvocationHandler {
private Object target; //目標代理物件
public InvocationHandlerImpl(Object target) {
this.target = target;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//這裡是呼叫要代理的物件
System.out.println("動態代理-開啟事務");
Object invoke = method.invoke(target,args);
System.out.println("動態代理-提交事務");
return invoke;
}
public static void main(String[] args) {
IUserDao iUserDao = new UserDaoImpl();
//通過實現InvocationHandler介面建立自己的呼叫處理器 IvocationHandler handler = new InvocationHandlerImpl(…);
InvocationHandlerImpl invocationHandler = new InvocationHandlerImpl(iUserDao);
//通過為Proxy類指定ClassLoader物件和一組interface建立動態代理類Class clazz = Proxy.getProxyClass(classLoader,new Class[]{…});
ClassLoader classLoader = iUserDao.getClass().getClassLoader();
Class<?>[] interfaces = iUserDao.getClass().getInterfaces();
// 主要裝載器、一組介面及呼叫處理動態代理例項
IUserDao userDao = (IUserDao) Proxy.newProxyInstance(classLoader,interfaces,invocationHandler);
userDao.add();
}
}
invoke三個引數的介紹
proxy:指代我們所代理的那個真實物件
method:指代的是我們所要呼叫真實物件的某個方法的Method物件
args:指代的是呼叫真實物件某個方法時接受的引數
newProxyInstance三個引數的介紹
classLoader:一個ClassLoader物件,定義了由哪個ClassLoader物件來對生成的代理物件進行載入
interfaces:一個Interface物件的陣列,表示的是我將要給我需要代理的物件提供一組什麼介面,如果我提供了一組介面給它,那麼這個代理物件就宣稱實現了該介面(多型),這樣我就能呼叫這組介面中的方法了
invocationHandler:一個InvocationHandler物件,表示的是當我這個動態代理物件在呼叫方法的時候,會關聯到哪一個InvocationHandler物件上
newProxyInstance就是JVM執行時動態生成一個代理物件,它並不是我們的InvocationHandler型別,也不是我們定義的那組介面的型別,而是在執行時動態生成的一個物件,並且命名方式都是這樣的形式,以$
開頭,proxy為中,最後一個數字表示物件的標號。(如$Proxy0)
增加InvocationHandler介面是實現任意物件的關鍵,且可以根據自己的需求對代理類進行自定義的處理,不過這裡設計的巧妙之處在於,InvocationHandler是一個介面,真正的實現由使用者指定。另外,在每一個方法執行的時候,invoke方法都會被呼叫 ,這個時候如果你需要對某個方法進行自定義邏輯處理,可以根據method的特徵資訊進行判斷分別處理。
CGLIB動態代理
使用cglib[Code Generation Library]實現動態代理,並不要求委託類必須實現介面,底層採用asm位元組碼生成框架生成代理類的位元組碼
public class CglibProxy implements MethodInterceptor {
//目標代理物件
private Object targetObject;
private Object getInstance(Object target) {
this.targetObject = target;
//操作位元組碼 生成虛擬子類,因為要擴充套件,所以虛擬子類複寫父類的方法
Enhancer enhancer = new Enhancer();
//設定超類
enhancer.setSuperclass(target.getClass());
//回撥給當前的cglib代理物件
enhancer.setCallback(this);
return enhancer.create();
}
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("開啟事務");
Object invoke = methodProxy.invoke(targetObject,objects);
System.out.println("提交事務");
return invoke;
}
public static void main(String[] args) {
CglibProxy cglibProxy = new CglibProxy();
UserDaoImpl userDaoImpl = (UserDaoImpl) cglibProxy.getInstance(new UserDaoImpl());
userDaoImpl.add();
}
}
CGLIB動態代理與JDK動態區別
Java動態代理是利用反射機制生成一個實現代理介面的匿名類,在呼叫具體方法前呼叫InvokeHandler來處理。而cglib動態代理是利用asm開源包,對代理物件類的class檔案載入進來,通過修改其位元組碼生成子類來處理。
在Spring中:
1、如果目標物件實現了介面,預設情況下會採用JDK的動態代理實現AOP
2、如果目標物件實現了介面,可以強制使用CGLIB實現AOP
3、如果目標物件沒有實現了介面,必須採用CGLIB庫,spring會自動在JDK動態代理和CGLIB之間轉換
JDK動態代理只能對實現了介面的類生成代理,而不能針對類 。
CGLIB是針對類實現代理,主要是對指定的類生成一個子類,覆蓋其中的方法 。
因為是繼承,所以該類或方法最好不要宣告成final ,final可以阻止繼承和多型。
參考文章: