設計模式~代理模式

一樂樂發表於2022-03-01

學習代理模式內容:

★ 靜態代理、

★ 動態代理(JDK動態代理、CGLIB動態代理)、

★ 攔截器的原理和日誌記錄

★ 代理總結




一、職責分離的例子---房屋租賃

1、重複

2、職責不分離

●【陪著看房、陪著談價格、交鑰匙】----不應該交個房東來重複做,不是他關心的重點,作為房東他只需要關心【籤合同、收房租

----解決:把這部分重複非業務重點的程式碼重構抽離給第三者----中介


● 抽離之後,中介也會重複了呀,那怎麼辦呢?

----沒事,這是人家的責任,人家就是靠這個賺錢的。


客戶端Client----- 第三方(目的:增強Service的功能)-----服務端Service




二、代理模式 [設計模式] 中介作用

✿ 1、代理模式:客戶端直接使用的都是代理物件,並不知道真實物件是誰,此時代理物件可以在客戶端和真實物件之間起到中介作用


(1)代理物件完全包含真實物件,客戶端使用的都是代理物件的方法,和真實物件沒有直接關係;

(2)代理模式的職責:把不是真實物件該做的事情從真實物件上撇開——職責清晰




三、靜態代理

1、概念/原理:

在程式執行前就已經存在代理類的位元組碼檔案,代理物件和真實物件的關係在執行前就確定了。


2、靜態代理的實現過程:

■ 將功能封裝成一個介面,代理類和真實類/委託類 都需要實現該介面:

  • 真實類/委託類 需要實現該介面,很好理解,才具有特定的功能。
  • 代理類 需要實現該介面,是因為這樣子才知道需要為哪些功能做代理、做增強。

image



3、靜態代理的優缺點:

● 優點:職責分離、安全

1,業務類只需要關注業務邏輯本身,保證了業務類的重用性。

2,把真實物件隱藏起來了,保護真實物件。


● 缺點:程式碼臃腫(一個真實物件需要對應一個代理物件)、不符合開閉原則(不方便擴充套件和維護)。

----一個代理類只能服務某一個業務介面

1,代理物件的某個介面只服務於某一種型別的物件也就是說每一個真實物件都得建立一個代理物件

2,如果需要代理的方法很多,則要為每一種方法都進行代理處理。

3,如果介面增加一個方法,除了所有實現類需要實現這個方法外,代理類也需要實現此方法



4、總結靜態代理:

1、靜態代理需要實現某個介面(才知道要代理、增強什麼功能)

2、靜態代理要包含真實物件(真實物件是靜態代理類的物件屬性)----內部bean,不暴露給外界

3、測試,通過介面物件進行測試,直接呼叫的是靜態代理物件(間接呼叫真實物件)

動態代理在測試時,是通過動態代理類,先獲取到代理物件,直接呼叫的是動態代理物件(間接呼叫真實物件)】

  • 獲取到代理物件:jdk提供的Proxy的方法newProxyInstance

● 詳細的程式碼:

/* 靜態代理類【需要實現介面,才知道需要為哪些功能做代理、做增強】*/
public class EmployeeServiceProxy implements IEmployeService{
	@Setter
	private IEmployeService target;//真實物件/委託物件
	@Setter
	private TransactionManager txManager;//事務管理器
	
	@Override
	public void save(Employee e) {
		txManager.open();
		try {
			target.save(e);
			txManager.commit();
		} catch (Exception e2) {
			txManager.rollback();
			e2.printStackTrace();
		}
	}
	
	@Override
	public void update(Employee e) {
		txManager.open();
		try {
			target.update(e);
			txManager.commit();
		} catch (Exception e2) {
			txManager.rollback();
			e2.printStackTrace();
		}
	}
}


/* 測試類 (先獲取代理物件)*/
@SpringJUnitConfig
public class App {
    
	@Autowired
	private IEmployeService service;

	@Test
	void testSave() throws Exception {
		Employee e = new Employee();
		e.setName("shangke");
		e.setAge(28);
        
		service.save(e);//呼叫介面物件【根據bean配置,實際呼叫的是靜態代理物件】
//         System.out.println(service);//介面物件的真實型別【根據bean配置,實際呼叫的是靜態代理物件】
//		  System.out.println(service.getClass());
	}
}

● 靜態代理bean物件的配置:

image




四、動態代理

1、學習動態動態代理之前的準備工作:位元組碼的動態載入

(1) 先了解一下java的編譯執行原理【java載入位元組碼原理】:

  • 編譯:將原始檔 .java 檔案,通過編譯器(javac 命令) 編譯成 位元組碼檔案 .class 檔案。【編譯得到位元組碼檔案

  • 執行:通過類載入器(以二進位制流形式)把位元組碼載入進JVM,通過java解析器(java 命令) 進行執行程式。【jvm解析位元組碼檔案


(2) 如何動態建立一份位元組碼?(實現了在程式碼中動態建立一個類的能力):

  • 通過java的編譯和執行原理,可以看到:在執行時期,是jvm通過位元組碼的二進位制資訊來載入類的

​ 所以,當我們在執行時期,通過java編譯系統組織.class檔案的格式和結構,生成相應的二進位制資料,然後再把這個二進位制資料載入轉換成對應的類



2、靜態代理和動態代理的(原理)區別:

■ 靜態代理:(經歷了編譯和執行)

在程式執行前就已經存在代理類的位元組碼檔案(因為通過了編譯階段),代理物件和真實物件的關係在執行前就確定了(因為通過了編譯階段)。

■ 動態代理:(只經歷了執行,我們通過某種手段得到的位元組碼【遵循位元組碼格式和結構】)

動態代理類是在程式執行期間由jvm通過反射等機制動態生成的,所以不存在代理類的位元組碼檔案(因為沒有經歷編譯階段),代理物件和真實物件的關係是在程式執行期間才確定的

□ 兩個原理相同點:

客戶端直接使用的都是代理物件,並不知道真實物件是誰,此時代理物件可以在客戶端和真實物件之間起到中介作用



3、動態代理分類:JDK動態代理、CGLIB動態代理

(1)JDK動態代理:

① 使用JDK的 Proxy的newProxyInstance 建立動態代理物件
	//動態代理---獲取代理物件
	@SuppressWarnings("unchecked")
	public <T> T getProxyObject() {
		return (T) Proxy.newProxyInstance(target.getClass().getClassLoader(),//真實物件的類載入器
				target.getClass().getInterfaces(), //真實物件實現的介面
				 this);//如何做事務增強的物件【增強器】
	}	//動態代理---獲取代理物件
	@SuppressWarnings("unchecked")
	public <T> T getProxyObject() {
		return (T) Proxy.newProxyInstance(target.getClass().getClassLoader(),//真實物件的類載入器
				target.getClass().getInterfaces(), //真實物件實現的介面
				 this);//如何做事務增強的物件【增強器】
	}

● 詳細的程式碼:

/* 動態代理:事務的增強操作 */
public class TransactionMangagerAdvice implements InvocationHandler{
	@Setter
	private Object target;//真實物件/委託物件
	@Setter
	private TransactionManager txManager;//事務增強器
	
	//動態代理---獲取代理物件
	@SuppressWarnings("unchecked")
	public <T> T getProxyObject() {
		return (T) Proxy.newProxyInstance(target.getClass().getClassLoader(),//真實物件的類載入器
				target.getClass().getInterfaces(), //真實物件實現的介面
				 this);//如何做事務增強的物件【增強器】
	}

    
	//如何為真實物件的方法做增強具體操作
	@Override
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		Object ret = null;
		txManager.open();
		try {
            //======================================================
			ret = method.invoke(target, args);//呼叫真實物件的方法
            //======================================================
			txManager.commit();
		} catch (Exception e) {
			e.printStackTrace();
			txManager.rollback();
		}
		return ret;
	}
}


/* 測試類 (先獲取代理物件)*/
@SpringJUnitConfig
public class App {
    
	@Autowired
	private TransactionMangagerAdvice advice;

	@Test
	void testSave() throws Exception {
		Employee e = new Employee();
		e.setName("shang");
		e.setAge(10);	
		e.setId(2L);
		
		//獲取代理物件
		IEmployeService proxy = advice.getProxyObject();
        //呼叫代理物件的儲存操作
		proxy.save(e);
        
//		System.out.println(proxy);//TransactionMangagerAdvice物件的真實型別是代理物件
//		System.out.println(proxy.getClass());//物件的真實型別
	}
}



☺(2) 動態代理原理:

  • 原理和靜態代理差不多【客戶端直接使用的都是代理物件,並不知道真實物件是誰,此時代理物件可以在客戶端和真實物件之間起到中介作用通過代理物件間接的呼叫真實物件的方法

  • 只不過,動態代理的代理類,不是由我們所建立,是我們生成位元組碼對應格式和結構的二進位制資料載入進虛擬機器,動態生成的

● 詳細的程式碼[通過 DynamicProxyClassGenerator 生成動態代理的位元組碼,再通過反編譯工具檢視。]:

public class DynamicProxyClassGenerator {
	public static void main(String[] args) throws Exception {
		generateClassFile(EmployeeServiceImpl.class, "EmployeeServiceProxy");
	}

	public static void generateClassFile(Class targetClass, String proxyName)throws Exception {
		//根據類資訊和提供的代理類名稱,生成位元組碼
		byte[] classFile =
			ProxyGenerator.generateProxyClass(proxyName, targetClass.getInterfaces());
		String path = targetClass.getResource(".").getPath();
		System.out.println(path);
		FileOutputStream out = null;
		//保留到硬碟中
		out = new FileOutputStream(path + proxyName + ".class");
		out.write(classFile);
		out.close();
	}
}



(3) CGLIB動態代理:

  • 第三方,需要拷貝jar包【spring-frame框架已經整合了cglib動態代理】

  • 原理:繼承

image

● 詳細的程式碼:

/* cglib動態代理:事務的增強操作 [和jdk的區別在建立代理物件方式上] */
//動態代理---獲取代理物件
	@SuppressWarnings("unchecked")
	public <T> T getProxyObject() {
        Enhancer enhancer = new Enhancer();
        return (T)enhancer.create(target.getClass(), this);//建立代理物件
		//enhancer.setSuperclass(target.getClass());//將繼承哪一個類,去做增強
		//enhancer.setCallback(this);//設定增強物件【增強器】	
		//return (T)enhancer.create();//建立代理物件
	}



(4)JDK動態代理和CGLIB動態代理的區別:

JDK動態代理要求 真實物件 必須要實現介面
■ CGLIB動態代理:可以針對沒有介面.

image





☺ 五、代理的總結:

1、代理原理圖:

image



☺ 2、代理的目標/作用:為了給目標物件(真實物件)的方法做功能的增強



☺ 3、動態代理原理:

  • 原理和靜態代理差不多【客戶端直接使用的都是代理物件,並不知道真實物件是誰,此時代理物件可以在客戶端和真實物件之間起到中介作用通過代理物件間接的呼叫真實物件的方法

  • 只不過,動態代理的代理類,不是由我們所建立,是我們生成位元組碼對應格式和結構的二進位制資料載入進虛擬機器,動態生成的



4、JDK 動態代理 和 CGLIB 動態代理的總結:有介面-使用jdk,沒有介面-使用cglib

(1) JDK 動態代理:

① JAVA 動態代理是使用 java.lang.reflect 包中的 Proxy 類與 InvocationHandler 介面這兩個來完成的。

② 要使用 JDK 動態代理,委託類(真實類)必須要定義介面

JDK 動態代理將會攔截所有 pubic 的方法(因為只能呼叫介面中定義的方法),這樣即使在介面中增加 了新的方法,不用修改程式碼也會被攔截。

④ 動態代理的最小單位是類(所有類中的方法都會被處理),如果只想攔截一部分方法,可以在 invoke 方法 中對要執行的方法名進行判斷 [判斷內容可以放到配置檔案,方便後續修改和維護~]


(2) CGLIB 動態代理:

CGLIB 可以生成委託類的子類,並重寫父類非 final 修飾符的方法

② 要求類不能是 final 的,要攔截的方法要是非 final、非 static、非 private 的。

③ 動態代理的最小單位是類(所有類中的方法都會被處理);



5、效能和選擇 [有介面-使用jdk,沒有介面-使用cglib + 效能要求有要求-Javassit]

  • JDK 動態代理是基於實現介面的,CGLIB 和 Javassit 是基於繼承委託類的。

  • 從效能上考慮:Javassit > CGLIB > JDK Struts2 的攔截器和 Hibernate 延遲載入物件,採用的是 Javassit 的方式.

  • 對介面建立代理優於對類建立代理,因為會產生更加鬆耦合的系統,也更符合面向介面程式設計規範

  • 若委託物件實現了幹介面,優先選用 JDK 動態代理。 若委託物件沒有實現任何介面,使用 Javassit 和 CGLIB 動態代



☺ 6、動態代理的應用:過濾器、攔截器、日誌記錄

1、過濾器Filter

2、攔截器Interceptor

  • 過濾器和攔截器差不多,只是過濾器是針對與web領域的概念,只能針對與請求和響應做增強,離不開servlet-api.jar;而攔截器是對於整個java領域的概念,不僅可以應用到web層,還可以應用到service層。

3、日誌記錄log


相關文章