動態代理

1. 什麼是代理

 簡單來講就是:不直接訪問目標,而是通過一箇中間層來訪問
在這裏插入圖片描述

2. 動態代理產生的原因

  當某些代碼塊需要很多地方執行,但又不想與特定方法耦合,即不想使用硬編碼的方法將公共代碼塊與特定方法耦合,這時動態代理就可以實現上面的需求

3. 動態代理的分類

3.1 JDK的動態代理

  只能實現接口代理,並且是包裝的被代理對象(類的實例),也就是說,在代理的過程中,有2個對象,一個代理對象,一個目標對象,目標對象被包裝在代理對象裏面。

3.1.1 實現

  在Java中,有一個Proxy類,可以直接使用反射方式,代理攔截。 最常用的是一個靜態方法Proxt.newProxyInstance()來創建動態代理對象

  1. 定義接口

      interface Dog{
     	void info();
     	void run(String name);
     }
    
  2. 接口實現

     public class GunDog implements Dog{
     	public void info(){
    		System.out.println("我是一隻獵狗");
    	}
    	
    	public void run(){
    		System.out.println("我奔跑速度");
    	}
    }
    
  3. 代理對象生成
       要實現InvocationHandler,並實現其中的invoke方法,在調用目標對象的時候,會先調用到invoke方法,再主動調用被調用者方法

    public class MyInvokationHandler implements InvocationHandler{
    	//需要代理的對象
    	private Object target;
    	public void setTarget(Object target){
    		this.target = target;
    	}
    	// 重寫invoke(),執行動態代理對象的所有方法時,都會被替換成執行如下的invoke()方法
    	pubic void invoke(Object proxy,Method method,Object[] args) throws Exception{
    		System.out.println("======模擬第一個通用方法");
    		Object result = method.invoke(target,args);
    		System.out.println("======模擬第二個通用方法");
    		return result;
    	}
    }.
    
  4. 使用

     public class Test{
     //創建MyInvokationHandler對象
     MyInvokationHandler handler = new MyInvokationHandler ();
     
     //創建原始的GunDog對象,作爲target
     Dog target = newGunDog();
     //爲MyInvokationHandler 指定target對象,
     handler.setTarget(target); 
     //以指定的target來創建動態代理對象,接口類型來接收
     Dog dog = (Dog) Proxy.newProxyInstance(target.getClass().getClassLoader(),target.getClass().getInterfaces(),handler);
    dog.info();
    dog.run();
    }
    
  5. 運行結果

     =======模擬第一個通用方法
     我是一隻獵狗
     ======模擬第二個通用方法
     =======模擬第一個通用方法
     我奔跑迅速
     ======模擬第二個通用方法
    

3.1.2 相關知識

    public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h)
        throws IllegalArgumentException
    {
        final Class<?>[] intfs = interfaces.clone();
		//獲取代理類的class對象
        Class<?> cl = getProxyClass0(loader, intfs);
        //根據參數獲取class對象的構造函數
 	     final Constructor<?> cons = cl.getConstructor(constructorParams);
		//生成代理類實例
		return cons.newInstance(new Object[]{h});
    }

  先查看緩存中查到是否該類加載器已經加載過該代理類,如果找到則直接返回,否則使用ProxyClassFactory來生成代理類
根據類加載器和目標接口類獲取代理類Class對象:該方法裏面包含了代理類的動態創建過程。會生成一個形如$Proxy0…之類的動態代理類,然後JVM會加載這些字節類,得到對應的Class對象,進行緩存

private static Class<?> getProxyClass0(ClassLoader loader,
                                           Class<?>... interfaces) {
		if (interfaces.length > 65535) {
            throw new IllegalArgumentException("interface limit exceeded");
        }
 
        if (interfaces.length > 65535) {
            throw new IllegalArgumentException("interface limit exceeded");
        }
        return proxyClassCache.get(loader, interfaces);
    }
 public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {
	 long num = nextUniqueNumber.getAndIncrement();
      //拼接代理類名
     String proxyName = proxyPkg + proxyClassNamePrefix + num;
     //生成字節碼
     byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
                proxyName, interfaces, accessFlags);
     //通過類加載器生成代理類的Class對象
      return defineClass0(loader, proxyName,
                                    proxyClassFile, 0, proxyClassFile.length);
    }

=====================================================
java.lang.ClassLoader
這是類裝載器類,負責將類的字節碼裝載到 Java 虛擬機(JVM)中併爲其定義類對象,然後該類才能被使用。Proxy 靜態方法生成動態代理類同樣需要通過類裝載器來進行裝載才能使用,它與普通類的唯一區別就是其字節碼是由 JVM 在運行時動態生成的而非預存在於任何一個 .class 文件中。
每次生成動態代理類對象時都需要指定一個類裝載器對象

3.2 CGLIB的代理

  是繼承目標對象,生成了一個新的類,然後來實現代理,這樣,在內存中只有代理對象,沒有目標對象了,使用的是直接繼承的方式

3.2.1 什麼是CGLIB

 Cglib與一些框架和語言的關係:
在這裏插入圖片描述

  • 最底層的是字節碼Bytecode,字節碼是Java爲了保證“一次編譯、到處運行”而產生的一種虛擬指令格式,例如iload_0、iconst_1、if_icmpne、dup等

  • 位於字節碼之上的是ASM,這是一種直接操作字節碼的框架,應用ASM需要對Java字節碼、Class結構比較熟悉

  • 位於ASM之上的是CGLIB、Groovy、BeanShell,後兩種並不是Java體系中的內容而是腳本語言,它們通過ASM框架生成字節碼變相執行Java代碼,這說明在JVM中執行程序並不一定非要寫Java代碼----只要你能生成Java字節碼,JVM並不關心字節碼的來源,當然通過Java代碼生成的JVM字節碼是通過編譯器直接生成的,算是最“正統”的JVM字節碼

  • 位於CGLIB、Groovy、BeanShell之上的就是Hibernate、Spring AOP這些框架了,這一層大家都比較熟悉

  • 最上層的是Applications,即具體應用,一般都是一個Web項目或者本地跑一個程序

  Cglib是一個強大的,高性能,高質量的代碼生成類庫。它可以在運行期擴展JAVA類與實現JAVA接口。其底層實現是通過ASM字節碼處理框架來轉換字節碼並生成新的類。大部分功能實際上是ASM所提供的,Cglib只是封裝了ASM,簡化了ASM操作,實現了運行期生成新的class。

  運行時動態的生成一個被代理類的子類(通過ASM字節碼處理框架實現),子類重寫了被代理類中所有非final的方法。在子類中採用方法攔截的技術攔截所有父類方法的調用,順勢植入橫切邏輯。

3.2.2 優點

  1. JDK實現動態代理需要實現類通過接口定義業務方法,對於沒有接口的類,如何實現動態代理呢,這就需要CGLib了
  2. CGLib採用了非常底層的字節碼技術,其原理是通過字節碼技術爲一個類創建子類,並在子類中採用方法攔截的技術攔截所有父類方法的調用,順勢織入橫切邏輯。JDK動態代理與CGLib動態代理均是實現Spring AOP的基礎
  3. Cglib動態代理比使用java反射的JDK動態代理要(Cglib的FastClass機制,解析參考http://www.cnblogs.com/cruze/category/593899.html )

3.2.3 缺點

  1. CGLib由於是採用動態創建子類的方法,對於被代理類中的final方法,無法進行代理,因爲子類中無法重寫final函數
  2. CGLib創建的動態代理對象性能比JDK創建的動態代理對象的性能高不少,但是CGLib在創建代理對象時所花費的時間卻比JDK多得多,所以對於單例的對象,因爲無需頻繁創建對象,用CGLib合適,反之,使用JDK方式要更爲合適一些

3.2.4 實現

 將上面用java動態代理實現改爲使用CGLIB代理實現

  1. 定義被代理類

    public class Dog {
    
        public void info(){
            System.out.println("我是一隻獵狗");
        };
    
        public void run(){
            System.out.println("我奔跑速度");
        };
    }
    
  2. 代理對象生成

    import org.springframework.cglib.proxy.MethodProxy;
    import java.lang.reflect.Method;
     public class MyMethodInterceptor implements MethodInterceptor {
    
        //Object表示要進行增強的對象;
        //Method表示攔截的方法;
        //Object[]數組表示參數列表,基本數據類型需要傳入其包裝類型,如int-->Integer、long-Long、double-->Double;
        //MethodProxy表示對方法的代理,invokeSuper方法表示對被代理對象方法的調用
        @Override
        public Object intercept(Object obj, Method method, Object[] arg, MethodProxy proxy) throws Throwable {
            System.out.println("======模擬第一個通用方法");
            Object object = proxy.invokeSuper(obj, arg);
            System.out.println("======模擬第二個通用方法" );
            return object;
        };
    }
    
  3. 使用

    public class Test {
    
        public static void main(String[] args){
            MyMethodInterceptor interceptor = new MyMethodInterceptor();
    
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(Dog.class);
            enhancer.setCallback(interceptor);
    
            Dog dog = (Dog)enhancer.create();
            dog.info();
            dog.run();
        }
    
  4. 運行結果

     ======模擬第一個通用方法
    我是一隻獵狗
    ======模擬第二個通用方法
    ======模擬第一個通用方法
    我奔跑速度
    ======模擬第二個通用方法
    

 運行結果和java實現的動態代理運行效果一樣,但是可以看出少了一個步驟,就是不用定義接口了:
 注:可能使用CGLIB報錯:

Exception in thread "main" java.lang.NoClassDefFoundError: org/objectweb/asm/Type
	at net.sf.cglib.core.TypeUtils.parseType(TypeUtils.java:184)
	at net.sf.cglib.core.KeyFactory.<clinit>(KeyFactory.java:66)
	at net.sf.cglib.proxy.Enhancer.<clinit>(Enhancer.java:69)

 如果是maven了項目,引入如下依賴即可:

		<dependency>
			<groupId>asm</groupId>
			<artifactId>asm</artifactId>
			<version>3.3.1</version>
		</dependency>


以上完成了動態代理的講解,下面說一下靜態代理

4. 靜態代理

4.1 實現

  1. 定義接口

      interface Dog{
     	void info();
     	void run();
     }
    
  2. 接口實現

     public class GunDog implements Dog{
     	public void info(){
    		System.out.println("我是一隻獵狗");
    	}
    
    	public void run(){
    		System.out.println("我奔跑速度");
    	}
    }
    
  3. 代理類

    public class DogProxy implements Dog{  
      
        // 目標對象  
        private Dog  dog;
    
    	// 通過構造方法傳入目標對象
    	public DogProxy (Dog dog){
    		this.dog=dog;
        }
        
         @Override  
         public void info() {  
             System.out.println("======模擬第一個通用方法");
         	 dog.info(); 
             System.out.println("======模擬第一個通用方法");	              
        }  
    
    	@Override  
         public void run() {  
             System.out.println("======模擬第一個通用方法");
         	 dog.info(); 
             System.out.println("======模擬第一個通用方法");	              
        } 
    }
    
  4. 使用
    public class Test {

    public static void main(String[] args){
        DogProxy dog = new DogProxy(new GunDog());
        dog.info();
        dog.run();
    }
    
  5. 運行結果

    ======模擬第一個通用方法
    我是一隻獵狗
    ======模擬第二個通用方法
    ======模擬第一個通用方法
    我奔跑速度
    ======模擬第二個通用方法
    

4.2 優點

  1. 代理使客戶端不需要知道實現類是什麼,怎麼做的,而客戶端只需知道代理即可(解耦合)

4.3 缺點

  1. 代理類和委託類實現了相同的接口,代理類通過委託類實現了相同的方法。這樣就出現了大量的代碼重複。如果接口增加一個方法,除了所有實現類需要實現這個方法外,所有代理類也需要實現此方法。增加了代碼維護的複雜度
  2. 代理對象只服務於一種類型的對象,如果要服務多類型的對象。勢必要爲每一種對象都進行代理,靜態代理在程序規模稍大時就無法勝任了。如上的代碼是隻爲UserManager類的訪問提供了代理,但是如果還要爲其他類如Department類提供代理的話,就需要我們再次添加代理Department的代理類

  靜態代理類只能爲特定的接口(Service)服務。如想要爲多個接口服務則需要建立很多個代理類。

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