Java代理模式及動態代理詳解


Java的動態代理在實踐中有着廣泛的使用場景,比如最場景的Spring AOP、Java注解的獲取、日志、用戶鑒權等。本篇文章帶大家了解一下代理模式、靜態代理以及基於JDK原生動態代理。

代理模式

無論學習靜態代理或動態代理,我們都要先了解一下代理模式。

先看百度百科的定義:

代理模式的定義:為其他對象提供一種代理以控制對這個對象的訪問。在某些情況下,一個對象不適合或者不能直接引用另一個對象,而代理對象可以在客戶端和目標對象之間起到中介的作用。

直接看定義可能有些難以理解,我們就以生活中具體的實例來說明一下。

我們都去過超市購買過物品,超市從廠商那里購買貨物之后出售給我們,我們通常並不知道貨物從哪里經過多少流程才到超市。

在這個過程中,等於是廠商“委托”超市出售貨物,對我們來說是廠商(真實對象)是不可見的。而超市(代理對象)呢,作為廠商的“代理者”來與我們進行交互。

同時,超市還可以根據具體的銷售情況來進行折扣等處理,來豐富被代理對象的功能。

通過代理模式,我們可以做到兩點:

1、隱藏委托類的具體實現。

2、實現客戶與委托類的解耦,在不改變委托類代碼的情況下添加一些額外的功能(日志、權限)等。

代理模式角色定義

在上述的過程中在編程的過程中我們可以定義為三類對象:

  • Subject(抽象主題角色):定義代理類和真實主題的公共對外方法,也是代理類代理真實主題的方法。比如:廣告、出售等。
  • RealSubject(真實主題角色):真正實現業務邏輯的類。比如實現了廣告、出售等方法的廠家(Vendor)。
  • Proxy(代理主題角色):用來代理和封裝真實主題。比如,同樣實現了廣告、出售等方法的超時(Shop)。

以上三個角色對應的類圖如下:

Java代理及動態代理詳解

靜態代理實例

靜態代理是指代理類在程序運行前就已經存在,這種情況下的代理類通常都是我們在Java代碼中定義的。

下面我們就以具體的實例來演示一下靜態代理。

首先定義一組接口Sell,用來提供廣告和銷售等功能。然后提供Vendor類(廠商,被代理對象)和Shop(超市,代理類),它們分別實現了Sell接口。

Sell接口定義如下:

/**
 * 委托類和代理類都實現了Sell接口
 * @author sec
 * @version 1.0
 * @date 2020/3/21 9:30 AM
 **/
public interface Sell {

	/**
	 * 出售
	 */
	void sell();

	/**
	 * 廣告
	 */
	void ad();
}

Vendor類定義如下:

/**
 * 供應商
 * @author sec
 * @version 1.0
 * @date 2020/3/21 9:30 AM
 **/
public class Vendor implements Sell{

	@Override
	public void sell() {
		System.out.println("Shop sell goods");
	}

	@Override
	public void ad() {
		System.out.println("Shop advert goods");
	}
}

Shop類定義如下:

/**
 * 超市,代理類
 * @author sec
 * @version 1.0
 * @date 2020/3/21 9:30 AM
 **/
public class Shop implements Sell{

	private Sell sell;

	public Shop(Sell sell){
		this.sell = sell;
	}

	@Override
	public void sell() {
	    System.out.println("代理類Shop,處理sell");
		sell.sell();
	}

	@Override
	public void ad() {
	    System.out.println("代理類Shop,處理ad");
		sell.ad();
	}
}

其中代理類Shop通過聚合的方式持有了被代理類Vendor的引用,並在對應的方法中調用Vendor對應的方法。在Shop類中我們可以新增一些額外的處理,比如篩選購買用戶、記錄日志等操作。

下面看看在客戶端中如何使用代理類。

/**
 * 靜態代理類測試方法
 * @author sec
 * @version 1.0
 * @date 2020/3/21 9:33 AM
 **/
public class StaticProxy {

	public static void main(String[] args) {

		// 供應商---被代理類
		Vendor vendor = new Vendor();

		// 創建供應商的代理類Shop
		Sell sell = new Shop(vendor);

		// 客戶端使用時面向的是代理類Shop。
		sell.ad();
		sell.sell();
	}
}

在上述代碼中,針對客戶看到的是Sell接口提供了功能,而功能又是由Shop提供的。我們可以在Shop中修改或新增一些內容,而不影響被代理類Vendor。

靜態代理的缺點

靜態代理實現簡單且不侵入原代碼,但當場景復雜時,靜態代理會有以下缺點:

1、當需要代理多個類時,代理對象要實現與目標對象一致的接口。要么,只維護一個代理類來實現多個接口,但這樣會導致代理類過於龐大。要么,新建多個代理類,但這樣會產生過多的代理類。

2、當接口需要增加、刪除、修改方法時,目標對象與代理類都要同時修改,不易維護。

於是,動態代理便派上用場了。

動態代理

動態代理是指代理類在程序運行時進行創建的代理方式。這種情況下,代理類並不是在Java代碼中定義的,而是在運行時根據Java代碼中的“指示”動態生成的。

相比於靜態代理,動態代理的優勢在於可以很方便的對代理類的函數進行統一的處理,而不用修改每個代理類的函數。

基於JDK原生動態代理實現

實現動態代理通常有兩種方式:JDK原生動態代理和CGLIB動態代理。這里,我們以JDK原生動態代理為例來進行講解。

JDK動態代理主要涉及兩個類:java.lang.reflect.Proxy和java.lang.reflect.InvocationHandler。

InvocationHandler接口定義了如下方法:

/**
 * 調用處理程序
 */
public interface InvocationHandler { 
    Object invoke(Object proxy, Method method, Object[] args); 
}

顧名思義,實現了該接口的中介類用做“調用處理器”。當調用代理類對象的方法時,這個“調用”會轉送到invoke方法中,代理類對象作為proxy參數傳入,參數method標識了具體調用的是代理類的哪個方法,args為該方法的參數。這樣對代理類中的所有方法的調用都會變為對invoke的調用,可以在invoke方法中添加統一的處理邏輯(也可以根據method參數對不同的代理類方法做不同的處理)。

Proxy類用於獲取指定代理對象所關聯的調用處理器。

下面以添加日志為例來演示一下動態代理。

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Date;

public class LogHandler implements InvocationHandler {
    Object target;  // 被代理的對象,實際的方法執行者

    public LogHandler(Object target) {
        this.target = target;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        before();
        Object result = method.invoke(target, args);  // 調用 target 的 method 方法
        after();
        return result;  // 返回方法的執行結果
    }
    // 調用invoke方法之前執行
    private void before() {
        System.out.println(String.format("log start time [%s] ", new Date()));
    }
    // 調用invoke方法之后執行
    private void after() {
        System.out.println(String.format("log end time [%s] ", new Date()));
    }
}

客戶端編寫程序使用動態代理代碼如下:

import java.lang.reflect.Proxy;

/**
 * 動態代理測試
 *
 * @author sec
 * @version 1.0
 * @date 2020/3/21 10:40 AM
 **/
public class DynamicProxyMain {

	public static void main(String[] args) {
		// 創建中介類實例
		LogHandler logHandler = new LogHandler(new Vendor());
		// 設置該變量可以保存動態代理類,默認名稱$Proxy0.class
		System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");

		// 獲取代理類實例Sell
		Sell sell = (Sell) (Proxy.newProxyInstance(Sell.class.getClassLoader(), new Class[]{Sell.class}, logHandler));

		// 通過代理類對象調用代理類方法,實際上會轉到invoke方法調用
		sell.sell();
		sell.ad();
	}
}

執行之后,打印日志如下:

調用方法sell之【前】的日志處理
Shop sell goods
調用方法sell之【后】的日志處理
調用方法ad之【前】的日志處理
Shop advert goods
調用方法ad之【后】的日志處理

經過上述驗證,我們發現已經成功為我們的被代理類統一添加了執行方法之前和執行方法之后的日志。

在上述實例中為了看一下生成的動態代理類的代碼,我們添加了下面的屬性設置(在生產環境中需要去掉該屬性)。

// 設置該變量可以保存動態代理類,默認名稱$Proxy0.class
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");

那么,我們可以執行main方法之后,還生成了一個名字為$Proxy0.class類文件。通過反編譯可看到如下的代碼:

package com.sun.proxy;

import com.choupangxia.proxy.Sell;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;

public final class $Proxy0 extends Proxy implements Sell {
    private static Method m1;
    private static Method m2;
    private static Method m4;
    private static Method m3;
    private static Method m0;

    public $Proxy0(InvocationHandler var1) throws  {
        super(var1);
    }

    public final boolean equals(Object var1) throws  {
        try {
            return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

    public final String toString() throws  {
        try {
            return (String)super.h.invoke(this, m2, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final void ad() throws  {
        try {
            super.h.invoke(this, m4, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final void sell() throws  {
        try {
            super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final int hashCode() throws  {
        try {
            return (Integer)super.h.invoke(this, m0, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m4 = Class.forName("com.choupangxia.proxy.Sell").getMethod("ad");
            m3 = Class.forName("com.choupangxia.proxy.Sell").getMethod("sell");
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

可以看到$Proxy0(代理類)繼承了Proxy類,並且實現了被代理的所有接口,以及equals、hashCode、toString等方法。

由於動態代理類繼承了Proxy類,所以每個代理類都會關聯一個InvocationHandler方法調用處理器。

類和所有方法都被public final修飾,所以代理類只可被使用,不可以再被繼承。

每個方法都有一個Method對象來描述,Method對象在static靜態代碼塊中創建,以“m+數字”的格式命名。

調用方法的時候通過super.h.invoke(this,m1,(Object[])null);調用。其中的super.h.invoke實際上是在創建代理的時候傳遞給Proxy.newProxyInstance的LogHandler對象,它繼承InvocationHandler類,負責實際的調用處理邏輯。

小結

關於代理和動態代理相關的內容,我們就講這么多。了解了代理模式可以讓我們的系統設計的更加具有可擴展性。而動態代理的應用就更廣了,各類框架及業務場景都在使用。有了兩個基礎,就能夠更好的學習其他框架。

關於CGLIB動態代理的內容,我們下篇文章再來聊一聊。


程序新視界:精彩和成長都不容錯過
![程序新視界-微信公眾號](https://img2020.cnblogs.com/other/1742867/202003/1742867-20200321112843411-428493370.png)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM