SE高階(17):動態代理的實現機制與應用實例

代理模式介紹

        代理模式是23種常用的設計模式之一,其作用是爲其他對象提供一種代理來控制對這個對象的訪問。主要作用就是加以控制。該模式的好處:在目標對象實現的基礎上擴展目標對象的功能。

應用實例理解:

       現實中,例如房子出售,買家與賣家無需接觸,買房和賣房的所有細節操作都交給中介,其他不用管,這裏的中介就是一個代理對象;買火車票不一定非要去火車站才能買,通過火車票代售點也可以買;還有國內是無法直接訪問一些外網的,這時我們就可以使用VPN等軟件來幫助我們去連接外網,至於連接細節不用關心,只需要上網就行。這裏的VPN就起到一箇中間層的作用,即代理對象。還有許多例子就不一一說明了。

代理模式特徵:

        代理類與委託類必須實現同樣的接口,代理類主要負責爲委託類預處理消息、過濾消息、轉發消息給委託類以及事後處理消息等。兩者之間通常會存在關聯關係,一個代理類的對象與一個委託類的對象關聯,代理類對象本身並不真正實現服務,而是通過調用委託類對象的相關方法來提供特定的服務。

借用網上一張簡潔明瞭的圖來說明代理起到什麼作用(注:該圖是靜態代理模式):

                     


代理種類:

代理分爲靜態代理動態代理兩大類。實現動態代理可以用JDK自帶的API來實現,即JDK代理,還有一種就是cglib代理。

  • 靜態代理:由程序員創建或由特定工具自動生成源代碼,再對其編譯。程序運行前,代理類的class文件就已存在。
  • 動態代理:在程序運行時,運用反射機制動態創建而成。(JDK代理方式)
  • 上面僅是對兩者作一個簡單的區別介紹,下面分別介紹兩種代理的使用方式及優缺點。

靜態代理

靜態代理實例:

/** 代理類和委託類實現的統一接口 */
public interface ImpUserDao {
	void saveUser(); //保存用戶
}
/** 實現接口的委託類 */
public class UserDao implements ImpUserDao {
	@Override
	public void saveUser() { //省略User對象,理解就好
		System.out.println("保存User對象數據");
	}
}
/** 代理類 */
public class ProxyUserDao implements ImpUserDao{
	//聲明與之關聯的委託類對象
	private ImpUserDao ud;
	//傳入實現ImpUserDao接口的類對象,即委託類對象
	public ProxyUserDao(ImpUserDao ud) {
		this.ud = ud;
	}
	//攔截UserDao,對其進行控制
	@Override
	public void saveUser() {
		System.out.println("開啓事務...");
		ud.saveUser(); //執行的是委託類的方法,代理類本身不提供真正實現
		System.out.println("提交事務...");
	}
}
/** 測試 */
public class Client {
	public static void main(String[] args) {
		UserDao ud = new UserDao();
		ProxyUserDao proxy = new ProxyUserDao(ud);
		proxy.saveUser();//執行代理類方法
	}
}

靜態代理解析:

        通過代理類,可以在不影響委託類的原有功能基礎上進行擴展,例如開啓事務和提交事務功能是委託類所沒有的,委託類只需要關心怎麼去提交數據,其他事情交給代理類來處理。這樣的好處是可以把核心代碼和輔助代碼分開,而且客戶端無需關注實現類(委託類),只需選擇代理就行。


靜態代理不足之處:

       代理類需要和委託類實現同一接口,實現接口方法時還需要把委託類的實現方法放進來,這意味着會出現許多的重複代碼,導致代碼冗餘;每個代理類只能服務於一個類型對象,當每增加一種類型對象時,就需要創建一個對應代理類,隨着業務增長,類會越來越多,上層結構一改動,維護會越來越複雜。簡要來說就是:靜態代理類只能爲特定的接口(Service)服務。如想要爲多個接口服務則需要建立很多個代理類。

        正因爲靜態代理有以上缺陷,所以需要採用動態代理來解決以上問題。動態代理最簡潔一點就是可以代理所有類型對象,這避免創建大量代理類,意味着只需一個代理類就行。


動態代理

        Java動態代理的實現需要使用java.lang.reflect包下的InvocationHandler接口和Proxy類。InvocationHandler接口是代理實例調用方法時的調用處理程序,簡單來講就是代理實例做的任何事情都由它來包辦,代理類執行任何方法都是在調用該接口的invoke()方法。Proxy類的作用就是提供用於創建動態代理類和實例的靜態方法。


invoke()方法參數說明:

	/** 參數說明
	 *  proxy  :表示動態代理對象
	 *  method : 委託類要執行的方法
	 *  args   : 執行方法的參數             */
	@Override
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {	
		return null;
	}

Proxy類主要方法:

//獲取一個代理類Class對象,需要提供關聯的類加載器和接口。 形參...表示可以傳入多個。
getProxyClass(ClassLoader loader,Class<?>... interfaces)  

//獲取一個代理類實例,需要提供指定接口的類加載器、一組接口數組和將方法調用指派到指定的調用處理程序。   
newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h) 

//返回指定動態代理實例的調用處理程序。   
getInvocationHandler(Object proxy)

//判斷指定的Class對象是否爲一個動態代理類 	
isProxyClass(Class<?> cl)  
	
//   參數說明 
//   loader     : 定義代理類的類加載器
//   interfaces : 代理類要實現的接口列表
//   h          : 指派方法調用的調用處理程序

動態代理實例:

/** 接口 */
interface UserDaoImp{
	void saveUser();
}
/** 實現接口的委託類,即目標類 */
class UserDao implements UserDaoImp{
	@Override
	public void saveUser() {
		System.out.println("---執行savaUser()---");
	}
}

/** 自定義InvocationHandler 調用處理程序*/
class MyHandler implements InvocationHandler{
	private Object tar; //保存目標類對象
	public MyHandler(Object tar) {
		this.tar = tar;
	}
	//代理類調用方法時,統一在此處進行處理
	@Override
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		System.out.println("---開啓事務---");
		method.invoke(tar, args); //調用目標類對象的方法
		System.out.println("---關閉事務---");
		return null;
	}
}
//測試
public class Client {
	public static void main(String[] args) throws Exception{
		//爲指定接口生成動態代理類Class字節碼對象
		Class proClass = Proxy.getProxyClass(UserDaoImp.class.getClassLoader(), UserDaoImp.class);
		//獲得構造器,參數類型是InvocationHandler.class
		Constructor constr = proClass.getConstructor(InvocationHandler.class);
		//傳入自定義InvocationHandler的實例對象,生成對應的代理類對象
		UserDaoImp iud = (UserDaoImp)constr.newInstance( new MyHandler(new UserDao()) );
		iud.saveUser();
	}
}

使用getProxyClass()生成代理實例比較繁瑣,可以簡化創建代理實例流程,採用newProxyInstance()來生成代理實例,代碼很簡潔,示例如下:
		//委託類對象
		UserDao ud = new UserDao();
		UserDaoImp udi  = (UserDaoImp) Proxy.newProxyInstance(UserDaoImp.class.getClassLoader(), //加載接口的類加載器
						   ud.getClass().getInterfaces(), //要代理的一組接口 
						   new MyHandler(ud)); //代理實例的調用處理程序
		udi.saveUser();
		
		//輸出結果
		---開啓事務---
		---執行savaUser()---
		---關閉事務---	
 
雖然簡化了創建流程,但每次使用手動創建代理實例,所以將創建代理實例封裝起來,以後只需傳入指定類,即能對其進行代理,邏輯上更直觀。
interface UserDaoImp {
	void saveUser(); 
}
interface TestImp{
	void test();
}
//實現類,實現了以上兩個接口
public class UserDao implements UserDaoImp, TestImp{
	@Override
	public void saveUser() {
		System.out.println("執行saveUser()...");
	}
	@Override
	public void test() {
		System.out.println("執行test()...");
	}
}

//所有代理的統一調用處理程序類
public class MyHandler implements InvocationHandler{
	private Object target; //實現類對象
	public MyHandler() {}

	//傳入要代理對象,生成對應代理實例
	public Object createProxy(Object target) {
		this.target = target;
		return Proxy.newProxyInstance(target.getClass().getClassLoader(),
				target.getClass().getInterfaces(),
				this); //this:表示該代理實例調用的是當前的調用處理器
	}
	
	/** 代理類無論執行什麼方法,都會統一調用invoke() */
	@Override
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		System.out.println("打開事務...");
		method.invoke(target, args);
		System.out.println("關閉事務...");
		return null;
	}
}
//測試
public class Client {
	public static void main(String[] args) {
		//委託類對象
		UserDao ud = new UserDao();
		//獲取相應的代理實例
		UserDaoImp udi = (UserDaoImp)new MyHandler().createProxy(ud);
		TestImp tt = (TestImp)new MyHandler().createProxy(ud);
		udi.saveUser();
		tt.test();
	}
}

//輸出結果:
打開事務...
執行saveUser()...
關閉事務...
打開事務...
執行test()...
關閉事務...

動態代理解析:

        通過上面的結果來看,如果是採用靜態代理來完成,則需要兩個代理類,還要保證和類實現的接口是一致的。而通過動態代理,連代理類都不用創建,需要代理時再生成對應的代理實例,不僅邏輯上簡潔明瞭,而且代碼量還變得很少;接口中方法都被放在invoke()進行統一處理。但動態代理的缺點也很明顯, 要代理的某個類必須實現接口,而生成的代理類也只能代理接口定義的方法,如果是類本身獨有的方法就沒辦法代理。所以可以去使用cglib代理,cglib可以對類進行代理,而不僅限於接口。有興趣的可以去查找相關資料。
注意點:
        容易出現的錯誤就是接口類型轉換異常,根據參數Class<?>[] interfaces  可以知道需要傳入的是一組被代理的接口列表,所以在使用getInterfaces()獲取接口列表時,一定要保證有接口,不然會導致錯誤。還有另一種傳入接口列表方式:new Class[]{接口.class }   這樣也能達到同樣效果。

以上,就是關於靜態代理和動態代理是如何使用的,以及它們的優缺點。


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