天天看點

黑馬程式員_高新技術之代理

一,代理的概念和作用

1,概述:

代理的主要類:java.lang.reflect.Proxy

生活中的代理:比如買電腦,直接在家附近的代理商買比直接去北京總部買要友善的多。

程式中的代理:為已存在的多個具有相同接口的目标類的各個方法增加一些系統功能,例如,異常處理、日志、計算方法的運作時間、事務管理、等等

2,編寫一個與目标類具有相同接口的代理類,代理類的每個方法調用目标類的相同方法,并在調用方法時加上系統功能的代碼。比如下面的代碼為X類增加了一個計算方法的系統功能代碼:

class X
{
	void sayHello(){
	System.out.println("hello,itcast");
	}
}
XProxy  /*類X的代理 在方法執行前後增加了時間的計算 計算方法的用時*/
{
	void sayHello()	{
	starttime
	X.sayHello();
	endtime
	}
}      

3,代理架構圖:

黑馬程式員_高新技術之代理

4,AOP:

系統中存在交叉業務,一個交叉業務就是要切入到系統中的一個方面,如下所示:

                              安全       事務         日志

StudentService  ------|----------|------------|-------------

CourseService   ------|----------|------------|-------------

MiscService       ------|----------|------------|-------------

安全,事務,日志等功能要貫穿到好多個子產品中,是以,它們就是交叉業務

用具體的程式代碼描述交叉業務:

method1         method2          method3

{                      {                       {

------------------------------------------------------切面

....            ....              ......

}                       }                       }

交叉業務的程式設計問題即為面向方面的程式設計(Aspect oriented program ,簡稱AOP),AOP的目标就是要使交叉業務子產品化。可以采用将切面代碼移動到原始方法的周圍,這與直接在方法中編寫切面代碼的運作效果是一樣的,如下所示:

func1         func2            func3

{             {                {

}             }                }

使用代理技術正好可以解決這種問題,代理是實作AOP功能的核心和關鍵技術。

二,動态代理技術

1,概述

 要為系統中的各種接口的類增加代理功能,那将需要太多的代理類,全部采用靜态代理方式,将是一件非常麻煩的事情!寫成百上千個代理類,太累!JVM可以在運作期動态生成出類的位元組碼,這種動态生成的類往往被用作代理類,即動态代理類。

 JVM生成的動态類必須實作一個或多個接口,是以,JVM生成的動态類隻能用作具有相同接口的目标類的代理。

2,CGLIB庫

CGLIB庫可以動态生成一個類的子類,一個類的子類也可以用作該類的代理,是以,如果要為一個沒有實作接口的類生成動态代理類,那麼可以使用CGLIB庫。

3,

代理類的各個方法中通常除了要調用目标的相應方法和對外傳回目标傳回的結果外,還可以在代理方法中的如下四個位置加上系統功能代碼:

1.在調用目标方法之前

2.在調用目标方法之後

3.在調用目标方法前後

4.在處理目标方法異常的catch塊中

比如下面的代碼:

Class proxy{
	void sayHello(){
		……….//調用目标方法前
		try{
			target.sayHello();
		}catch(Exception e){
			………..//處理目标方法異常的catch塊中
		}
		………….//調用目标方法後
	}
}
      

三,分析JVM動态生成的類

1,Proxy 的靜态方法

// 方法 1: 該方法用于擷取指定代理對象所關聯的調用處理器
static InvocationHandler getInvocationHandler(Object proxy) 

// 方法 2:該方法用于擷取關聯于指定類裝載器和一組接口的動态代理類的類對象
static Class getProxyClass(ClassLoader loader, Class[] interfaces) 

// 方法 3:該方法用于判斷指定類對象是否是一個動态代理類
static boolean isProxyClass(Class cl) 

// 方法 4:該方法用于為指定類裝載器、一組接口及調用處理器生成動态代理類執行個體
static Object newProxyInstance(ClassLoader loader, Class[] interfaces, 
    InvocationHandler h)       

java.lang.reflect.InvocationHandler:這是調用處理器接口,它自定義了一個 invoke 方法,用于集中處理在動态代理類對象上的方法調用,通常在該方法中實作對委托類的代理通路。

 2,. InvocationHandler 的核心方法

// 該方法負責集中處理動态代理類上的所有方法調用。第一個參數既是代理類執行個體,第二個參數是被調用的方法對象
// 第三個方法是調用參數。調用處理器根據這三個參數進行預處理或分派到委托類執行個體上發射執行
Object invoke(Object proxy, Method method, Object[] args) 
      

每次生成動态代理類對象時都需要指定一個實作了該接口的調用處理器對象。

java.lang.ClassLoader:這是類裝載器類,負責将類的位元組碼裝載到 Java 虛拟機(JVM)中并為其定義類對象,然後該類才能被使用。Proxy 靜态方法生成動态代理類同樣需要通過類裝載器來進行裝載才能使用,它與普通類的唯一差別就是其位元組碼是由 JVM 在運作時動态生成的而非預存在于任何一個 .class 檔案中。

3,代理機制及其特點:

如何使用 Java 動态代理。具體有如下四步驟:

通過實作 InvocationHandler 接口建立自己的調用處理器;

通過為 Proxy 類指定 ClassLoader 對象和一組 interface 來建立動态代理類;

通過反射機制獲得動态代理類的構造函數,其唯一參數類型是調用處理器接口類型;

通過構造函數建立動态代理類執行個體,構造時調用處理器對象作為參數被傳入。

4,動态代理對象建立過程

// MyInvocationHandler 實作了 InvocationHandler 接口,并能實作方法調用從代理類到委托類的分派轉發
// 其内部通常包含指向委托類執行個體的引用,用于真正執行分派轉發過來的方法調用
InvocationHandler handler = new MyInvocationHandler(..); 

// 通過 Proxy 為包括 Interface 接口在内的一組接口動态建立代理類的類對象
Class clazz = Proxy.getProxyClass(classLoader, new Class[] { Interface.class, ... }); 

// 通過反射從生成的類對象獲得構造函數對象
Constructor constructor = clazz.getConstructor(new Class[] { InvocationHandler.class }); 

// 通過構造函數對象建立動态代理類執行個體
Interface Proxy = (Interface)constructor.newInstance(new Object[] { handler });       

實際使用過程更加簡單,因為 Proxy 的靜态方法 newProxyInstance 已經為我們封裝了步驟 2 到步驟 4 的過程,是以簡化後的過程如下

簡化的動态代理對象建立過程

// MyInvocationHandler 實作了 InvocationHandler 接口,并能實作方法調用從代理類到委托類的分派轉發
InvocationHandler handler = new MyInvocationHandler(..); 

// 通過 Proxy 直接建立動态代理類執行個體
Interface proxy = (Interface)Proxy.newProxyInstance( classLoader, 
	 new Class[] { Interface.class }, 
	 handler ); 
	      

四, Java 動态代理機制的一些特點。

首先是動态生成的代理類本身的一些特點。1)包:如果所代理的接口都是 public 的,那麼它将被定義在頂層包(即包路徑為空),如果所代理的接口中有非 public 的接口(因為接口不能被定義為 protect 或 private,是以除 public 之外就是預設的 package 通路級别),那麼它将被定義在該接口所在包(假設代理了 com.itcast.developerworks 包中的某非 public 接口 A,那麼新生成的代理類所在的包就是 com.itcast.developerworks),這樣設計的目的是為了最大程度的保證動态代理類不會因為包管理的問題而無法被成功定義并通路;2)類修飾符:該代理類具有 final 和 public 修飾符,意味着它可以被所有的類通路,但是不能被再度繼承;3)類名:格式是“$ProxyN”,其中 N 是一個逐一遞增的阿拉伯數字,代表 Proxy 類第 N 次生成的動态代理類,值得注意的一點是,并不是每次調用 Proxy 的靜态方法建立動态代理類都會使得 N 值增加,原因是如果對同一組接口(包括接口排列的順序相同)試圖重複建立動态代理類,它會很聰明地傳回先前已經建立好的代理類的類對象,而不會再嘗試去建立一個全新的代理類,這樣可以節省不必要的代碼重複生成,提高了代理類的建立效率。4)類繼承關系:該類的繼承關系如圖:

黑馬程式員_高新技術之代理

由圖可見,Proxy 類是它的父類,這個規則适用于所有由 Proxy 建立的動态代理類。而且該類還實作了其所代理的一組接口,這就是為什麼它能夠被安全地類型轉換到其所代理的某接口的根本原因。

接下來讓我們了解一下代理類執行個體的一些特點。每個執行個體都會關聯一個調用處理器對象,可以通過 Proxy 提供的靜态方法 getInvocationHandler 去獲得代理類執行個體的調用處理器對象。在代理類執行個體上調用其代理的接口中所聲明的方法時,這些方法最終都會由調用處理器的 invoke 方法執行,此外,值得注意的是,代理類的根類 java.lang.Object 中有三個方法也同樣會被分派到調用處理器的 invoke 方法執行,它們是 hashCode,equals 和 toString,可能的原因有:一是因為這些方法為 public 且非 final 類型,能夠被代理類覆寫;二是因為這些方法往往呈現出一個類的某種特征屬性,具有一定的區分度,是以為了保證代理類與委托類對外的一緻性,這三個方法也應該被分派到委托類執行。當代理的一組接口有重複聲明的方法且該方法被調用時,代理類總是從排在最前面的接口中擷取方法對象并分派給調用處理器,而無論代理類執行個體是否正在以該接口(或繼承于該接口的某子接口)的形式被外部引用,因為在代理類内部無法區分其目前的被引用類型。

接着來了解一下被代理的一組接口有哪些特點。首先,要注意不能有重複的接口,以避免動态代理類代碼生成時的編譯錯誤。其次,這些接口對于類裝載器必須可見,否則類裝載器将無法連結它們,将會導緻類定義失敗。再次,需被代理的所有非 public 的接口必須在同一個包中,否則代理類生成也會失敗。最後,接口的數目不能超過 65535,這是 JVM 設定的限制。

最後再來了解一下異常處理方面的特點。從調用處理器接口聲明的方法中可以看到理論上它能夠抛出任何類型的異常,因為所有的異常都繼承于 Throwable 接口,但事實是否如此呢?答案是否定的,原因是我們必須遵守一個繼承原則:即子類覆寫父類或實作父接口的方法時,抛出的異常必須在原方法支援的異常清單之内。是以雖然調用處理器理論上講能夠,但實際上往往受限制,除非父接口中的方法支援抛 Throwable 異常。那麼如果在 invoke 方法中的确産生了接口方法聲明中不支援的異常,那将如何呢?放心,Java 動态代理類已經為我們設計好了解決方法:它将會抛出 UndeclaredThrowableException 異常。這個異常是一個 RuntimeException 類型,是以不會引起編譯錯誤。通過該異常的 getCause 方法,還可以獲得原來那個不受支援的異常對象,以便于錯誤診斷。

五,動态代理的工作原理圖

黑馬程式員_高新技術之代理

六,讓動态生成的類成為目标類的代理

怎樣将目标類傳進去?

直接在InvocationHandler實作類中建立目标類的執行個體對象,可以看運作效果和加入日志代碼,但沒有實際意義。

為InvocationHandler實作類注入目标類的執行個體對象,不能采用匿名内部類的形式了。

讓匿名的InvocationHandler實作類通路外面方法中的目标類執行個體對象的final類型的引用變量。

将建立代理的過程改為一種更優雅的方式,eclipse重構出一個getProxy方法綁定接收目标同時傳回代理對象,讓調用者更懶惰,更友善,調用者甚至不用接觸任何代理的API。

将系統功能代碼子產品化,即将切面代碼也改為通過參數形式提供,怎樣把要執行的系統功能代碼以參數形式提供?

把要執行的代碼裝到一個對象的某個方法裡,然後把這個對象作為參數傳遞,接收者隻要調用這個對象的方法,即等于執行了外界提供的代碼!

為bind方法增加一個Advice參數。

七。代碼示範:

動态代理測試類:

public class ProxyTest {

	/**
	 * @param args
	 */
	public static void main(String[] args) throws Exception{
		// TODO Auto-generated method stub
		//建立實作了Collection接口的動态類和檢視其名稱
		Class clazzProxy1 = Proxy.getProxyClass(Collection.class.getClassLoader(), Collection.class);
		System.out.println(clazzProxy1.getName());
		
		//編碼列出動态類中的所有構造方法和參數簽名
		System.out.println("----------begin constructors list----------");
		/*$Proxy0()
		$Proxy0(InvocationHandler,int)*/
		Constructor[] constructors = clazzProxy1.getConstructors();
		for(Constructor constructor : constructors){
			String name = constructor.getName();//擷取到某個構造方法的名字
			//StringBuilder在單線程下效率更高 StringBuffer在多線程下效率高
			StringBuilder sBuilder = new StringBuilder(name);
			sBuilder.append('(');
			//擷取構造方法裡的參數類型
			Class[] clazzParams = constructor.getParameterTypes();
			for(Class clazzParam : clazzParams){
				sBuilder.append(clazzParam.getName()).append(',');
			}
			if(clazzParams!=null && clazzParams.length != 0)
				//出去參數後面的最後一個多餘的逗号
				sBuilder.deleteCharAt(sBuilder.length()-1);
			sBuilder.append(')');
			System.out.println(sBuilder.toString());			
		}
		//編碼列出動态類中的所有方法和參數簽名
		System.out.println("----------begin methods list----------");
		/*$Proxy0()
		$Proxy0(InvocationHandler,int)*/
		//擷取Collection對象上的方法
		Method[] methods = clazzProxy1.getMethods();
		for(Method method : methods){
			String name = method.getName();
			StringBuilder sBuilder = new StringBuilder(name);
			sBuilder.append('(');
			Class[] clazzParams = method.getParameterTypes();
			for(Class clazzParam : clazzParams){
				sBuilder.append(clazzParam.getName()).append(',');
			}
			if(clazzParams!=null && clazzParams.length != 0)
				sBuilder.deleteCharAt(sBuilder.length()-1);
			sBuilder.append(')');
			System.out.println(sBuilder.toString());			
		}
		//建立動态類的執行個體對象
		System.out.println("----------begin create instance object----------");
		//Object obj = clazzProxy1.newInstance();
		//該對象的構造函數是有參的 是以先通過反射擷取有參的構造函數
		Constructor constructor = clazzProxy1.getConstructor(InvocationHandler.class);
		//自己建立一個類實作InvocationHandler
		class MyInvocationHander1 implements InvocationHandler{

			public Object invoke(Object proxy, Method method, Object[] args)
					throws Throwable {
				// TODO Auto-generated method stub
				return null;
			}
		
		}
		//執行個體化該對象傳入的參數必須是InvocationHandler類型
		Collection proxy1 = (Collection)constructor.newInstance(new MyInvocationHander1());
		
		System.out.println(proxy1);//列印結果為null 因為調用了toString invoke傳回的為null 是以為null
		proxy1.clear();//清除集合
		//proxy1.size();//會報告異常 因為調用size方法 回去調用invoke方法  傳回一個null 與size傳回的一個整數相沖突
		
		//将建立動态類的執行個體對象的代理改成匿名内部類的形式編寫
		Collection proxy2 = (Collection)constructor.newInstance(new InvocationHandler(){

			public Object invoke(Object proxy, Method method, Object[] args)
					throws Throwable {
				return null;
			}
			
		});
		
		final ArrayList target = new ArrayList();			
		Collection proxy3 = (Collection)getProxy(target,new MyAdvice());
		proxy3.add("zxx");//調用add方法會去找Handler的incoke方法
		proxy3.add("lhm");
		proxy3.add("bxd");
		System.out.println(proxy3.size());
		System.out.println(proxy3.getClass().getName());
	}
	
	//把目标和系統功能作為一個參數傳進來 用這個方法去實作目标的代理類
	private static Object getProxy(final Object target,final Advice advice) {
		Object proxy3 = Proxy.newProxyInstance(
				target.getClass().getClassLoader(),//獲得目标類的類加載器
				/*new Class[]{Collection.class},*/
				target.getClass().getInterfaces(),//目标類的接口
				
				new InvocationHandler(){
				
					public Object invoke(Object proxy, Method method, Object[] args)
							throws Throwable {

						/*long beginTime = System.currentTimeMillis();
						Object retVal = method.invoke(target, args);
						long endTime = System.currentTimeMillis();
						System.out.println(method.getName() + " running time of " + (endTime - beginTime));
						return retVal;*/
						

						advice.beforeMethod(method);
						Object retVal = method.invoke(target, args);
						advice.afterMethod(method);
						return retVal;						
						
					}
				}
				);
		return proxy3;
	}

}

      

傳入的參數Advice接口:

import java.lang.reflect.Method;

//要使用的系統功能代碼接口
public interface Advice {
	void beforeMethod(Method method);
	void afterMethod(Method method);
}      

實作Advice接口的MyAdvice類:

import java.lang.reflect.Method;
//實作了系統功能接口的類:MyAdvice  也就是我們要切入的代碼
public class MyAdvice implements Advice {
	long beginTime = 0;
	public void afterMethod(Method method) {
		// TODO Auto-generated method stub
		System.out.println("從傳智播客畢業上班啦!");		
		long endTime = System.currentTimeMillis();
		System.out.println(method.getName() + " running time of " + (endTime - beginTime));

	}

	public void beforeMethod(Method method) {
		// TODO Auto-generated method stub
		System.out.println("到傳智播客來學習啦!");
		beginTime = System.currentTimeMillis();
	}

}      

八,實作AOP功能的封裝與配置

工廠類BeanFactory:

import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

import cn.itcast.day3.Advice;
//負責建立目标類或代理類的執行個體對象
public class BeanFactory {
	Properties props = new Properties();
	//構造方法要接受一個配置檔案
	public BeanFactory(InputStream ips){
		try {
			//從輸入流中讀取屬性清單(鍵和元素對)。
			props.load(ips);
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	/*根據參數字元串傳回一個相應的執行個體對象,如果參數字元串在配置檔案中對應的類名不是ProxyFactoryBean,
	則直接傳回該類的執行個體對象,否則,傳回該類執行個體對象的getProxy方法傳回的對象。*/
	public Object getBean(String name){
		//用指定的鍵name在此屬性清單中搜尋屬性
		String className = props.getProperty(name);
		Object bean = null;
		try {
			//建立這個類的執行個體對象
			Class clazz = Class.forName(className);
			bean = clazz.newInstance();
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} 
		//如果是特殊的類ProxyFactoryBean就建立代理 并傳回代理
		if(bean instanceof ProxyFactoryBean){
			Object proxy = null;
			ProxyFactoryBean proxyFactoryBean = (ProxyFactoryBean)bean;
			try {
				//擷取目标和通告
				Advice advice = (Advice)Class.forName(props.getProperty(name + ".advice")).newInstance();
				Object target = Class.forName(props.getProperty(name + ".target")).newInstance();
				//設定目标和通告
				proxyFactoryBean.setAdvice(advice);
				proxyFactoryBean.setTarget(target);
				//通過proxyFactoryBean擷取proxy對象
				proxy = proxyFactoryBean.getProxy();
			} catch (Exception e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			return proxy;
		}
		return bean;
	}
}      

配置檔案config.properties

:

#xxx=java.util.ArrayList

 xxx=cn.itcast.ProxyFactoryBean

 xxx.target=java.util.ArrayList

 xxx.advice=cn.itcast.MyAdvice

生成動态代理的工廠ProxyFactoryBean

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

import cn.itcast.day3.Advice;

//充當封裝生成動态代理的工廠 需要為工廠類提供目标 通知配置參數資訊
public class ProxyFactoryBean {

	private Advice advice;
	private Object target;
	
	public Advice getAdvice() {
		return advice;
	}

	public void setAdvice(Advice advice) {
		this.advice = advice;
	}

	public Object getTarget() {
		return target;
	}

	public void setTarget(Object target) {
		this.target = target;
	}

	public Object getProxy() {
		// TODO Auto-generated method stub
		Object proxy3 = Proxy.newProxyInstance(
				target.getClass().getClassLoader(),
				/*new Class[]{Collection.class},*/
				target.getClass().getInterfaces(),
				new InvocationHandler(){
				
					public Object invoke(Object proxy, Method method, Object[] args)
							throws Throwable {

						/*long beginTime = System.currentTimeMillis();
						Object retVal = method.invoke(target, args);
						long endTime = System.currentTimeMillis();
						System.out.println(method.getName() + " running time of " + (endTime - beginTime));
						return retVal;*/
						

						advice.beforeMethod(method);
						Object retVal = method.invoke(target, args);
						advice.afterMethod(method);
						return retVal;						
						
					}
				}
				);
		return proxy3;
	}

}      

驗證類:

import java.io.InputStream;
import java.util.Collection;

//驗證是代理還是目标
public class AopFrameworkTest {

	/**
	 * @param args
	 */
	public static void main(String[] args) throws Exception {
		// TODO Auto-generated method stub
		//将給定名稱的資源用一個讀取流關聯 加在配置檔案
		InputStream ips = AopFrameworkTest.class.getResourceAsStream("config.properties");
		Object bean = new BeanFactory(ips).getBean("xxx");
		System.out.println(bean.getClass().getName());
		((Collection)bean).clear();
	}

}