設計模式之---動態代理
關於Java中的動態代理,我們首先需要了解的是一種常用的設計模式--代理模式,而對於代理,根據建立代理類的時間點,又可以分為靜態代理和動態代理。
一、代理模式
代理模式是常用的java設計模式,他的特徵是代理類與委託類有同樣的介面,代理類主要負責為委託類預處理訊息、過濾訊息、把訊息轉發給委託類,以及事後處理訊息等。代理類與委託類之間通常會存在關聯關係,一個代理類的物件與一個委託類的物件關聯,代理類的物件本身並不真正實現服務,而是通過呼叫委託類的物件的相關方法,來提供特定的服務。簡單的說就是,我們在訪問實際物件時,是通過代理物件來訪問的,代理模式就是在訪問實際物件時引入一定程度的間接性,因為這種間接性,可以附加多種用途。在後面我會
解釋這種間接性帶來的好處。代理模式結構圖(圖片來自《大話設計模式》):
二、靜態代理
1、靜態代理
靜態代理:由程式設計師建立或特定工具自動生成原始碼,也就是在編譯時就已經將介面,被代理類,代理類等確定下來。在程式執行之前,代理類的.class檔案就已經生成。
2、靜態代理簡單實現
根據上面代理模式的類圖,來寫一個簡單的靜態代理的例子。我這兒舉一個比較粗糙的例子,假如一個班的同學要向老師交班費,但是都是通過班長把自己的錢轉交給老師。這裡,班長就是代理學生上交班費,
班長就是學生的代理。
首先,我們建立一個Person介面。這個介面就是學生(被代理類),和班長(代理類)的公共介面,他們都有上交班費的行為。這樣,學生上交班費就可以讓班長來代理執行。
/**
* 建立Person介面
* @author Gonjan
*/
public interface Person {
//上交班費
void giveMoney();
}
Student類實現Person介面。Student可以具體實施上交班費的動作。
public class Student implements Person { private String name; public Student(String name) { this.name = name; } @Override public void giveMoney() { System.out.println(name + "上交班費50元"); } }
StudentsProxy類,這個類也實現了Person介面,但是還另外持有一個學生類物件,由於實現了Peson介面,同時持有一個學生物件,那麼他可以代理學生類物件執行上交班費(執行giveMoney()方法)行為。
/**
* 學生代理類,也實現了Person介面,儲存一個學生實體,這樣既可以代理學生產生行為
* @author Gonjan
*
*/
public class StudentsProxy implements Person{
//被代理的學生
Student stu;
public StudentsProxy(Person stu) {
// 只代理學生物件
if(stu.getClass() == Student.class) {
this.stu = (Student)stu;
}
}
//代理上交班費,呼叫被代理學生的上交班費行為
public void giveMoney() {
stu.giveMoney();
}
}
下面測試一下,看如何使用代理模式:
public class StaticProxyTest {
public static void main(String[] args) {
//被代理的學生張三,他的班費上交有代理物件monitor(班長)完成
Person zhangsan = new Student("張三");
//生成代理物件,並將張三傳給代理物件
Person monitor = new StudentsProxy(zhangsan);
//班長代理上交班費
monitor.giveMoney();
}
}
執行結果:
這裡並沒有直接通過張三(被代理物件)來執行上交班費的行為,而是通過班長(代理物件)來代理執行了。這就是代理模式。
代理模式最主要的就是有一個公共介面(Person),一個具體的類(Student),一個代理類(StudentsProxy),代理類持有具體類的例項,代為執行具體類例項方法。上面說到,代理模式就是在訪問實際物件時引入一定程度的間接性,因為這種間接性,可以附加多種用途。這裡的間接性就是指不直接呼叫實際物件的方法,那麼我們在代理過程中就可以加上一些其他用途。就這個例子來說,加入班長在幫張三上交班費之前想要先反映一下張三最近學習有很大進步,通過代理模式很輕鬆就能辦到:
public class StudentsProxy implements Person{
//被代理的學生
Student stu;
public StudentsProxy(Person stu) {
// 只代理學生物件
if(stu.getClass() == Student.class) {
this.stu = (Student)stu;
}
}
//代理上交班費,呼叫被代理學生的上交班費行為
public void giveMoney() {
System.out.println("張三最近學習有進步!");
stu.giveMoney();
}
}
執行結果:
可以看到,只需要在代理類中幫張三上交班費之前,執行其他操作就可以了。這種操作,也是使用代理模式的一個很大的優點。最直白的就是在Spring中的面向切面程式設計(AOP),我們能在一個切點之前執行一些操作,在一個切點之後執行一些操作,這個切點就是一個個方法。這些方法所在類肯定就是被代理了,在代理過程中切入了一些其他操作。
三、動態代理
1.動態代理
代理類在程式執行時建立的代理方式被成為動態代理。 我們上面靜態代理的例子中,代理類(studentProxy)是自己定義好的,在程式執行之前就已經編譯完成。然而動態代理,代理類並不是在Java程式碼中定義的,而是在執行時根據我們在Java程式碼中的“指示”動態生成的。相比於靜態代理, 動態代理的優勢在於可以很方便的對代理類的函式進行統一的處理,而不用修改每個代理類中的方法。 比如說,想要在每個代理的方法前都加上一個處理方法:
public void giveMoney() {
//呼叫被代理方法前加入處理方法
beforeMethod();
stu.giveMoney();
}
這裡只有一個giveMoney方法,就寫一次beforeMethod方法,但是如果出了giveMonney還有很多其他的方法,那就需要寫很多次beforeMethod方法,麻煩。那看看下面動態代理如何實現。
2、動態代理簡單實現
在java的java.lang.reflect包下提供了一個Proxy類和一個InvocationHandler介面,通過這個類和這個介面可以生成JDK動態代理類和動態代理物件。
建立一個動態代理物件步驟,具體程式碼見後面:
- 建立一個InvocationHandler物件
//建立一個與代理物件相關聯的InvocationHandler InvocationHandler stuHandler = new MyInvocationHandler<Person>(stu);
- 使用Proxy類的getProxyClass靜態方法生成一個動態代理類stuProxyClass
Class<?> stuProxyClass = Proxy.getProxyClass(Person.class.getClassLoader(), new Class<?>[] {Person.class});
- 獲得stuProxyClass 中一個帶InvocationHandler引數的構造器constructor
Constructor<?> constructor = PersonProxy.getConstructor(InvocationHandler.class);
- 通過構造器constructor來建立一個動態例項stuProxy
Person stuProxy = (Person) cons.newInstance(stuHandler);
就此,一個動態代理物件就建立完畢,當然,上面四個步驟可以通過Proxy類的newProxyInstances方法來簡化:
//建立一個與代理物件相關聯的InvocationHandler InvocationHandler stuHandler = new MyInvocationHandler<Person>(stu); //建立一個代理物件stuProxy,代理物件的每個執行方法都會替換執行Invocation中的invoke方法 Person stuProxy= (Person) Proxy.newProxyInstance(Person.class.getClassLoader(), new Class<?>[]{Person.class}, stuHandler);
到這裡肯定都會很疑惑,這動態代理到底是如何執行的,是如何通過代理物件來執行被代理物件的方法的,先不急,我們先看看一個簡單的完整的動態代理的例子。還是上面靜態代理的例子,班長需要幫學生代交班費。 首先是定義一個Person介面:
/**
* 建立Person介面
* @author Gonjan
*/
public interface Person {
//上交班費
void giveMoney();
}
建立需要被代理的實際類:
public class Student implements Person {
private String name;
public Student(String name) {
this.name = name;
}
@Override
public void giveMoney() {
try {
//假設數錢花了一秒時間
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name + "上交班費50元");
}
}
再定義一個檢測方法執行時間的工具類,在任何方法執行前先呼叫start方法,執行後呼叫finsh方法,就可以計算出該方法的執行時間,這也是一個最簡單的方法執行時間檢測工具。
public class MonitorUtil {
private static ThreadLocal<Long> tl = new ThreadLocal<>();
public static void start() {
tl.set(System.currentTimeMillis());
}
//結束時列印耗時
public static void finish(String methodName) {
long finishTime = System.currentTimeMillis();
System.out.println(methodName + "方法耗時" + (finishTime - tl.get()) + "ms");
}
}
建立StuInvocationHandler類,實現InvocationHandler介面,這個類中持有一個被代理物件的例項target。InvocationHandler中有一個invoke方法,所有執行代理物件的方法都會被替換成執行invoke方法。
再再invoke方法中執行被代理物件target的相應方法。當然,在代理過程中,我們在真正執行被代理物件的方法前加入自己其他處理。這也是Spring中的AOP實現的主要原理,這裡還涉及到一個很重要的關於java反射方面的基礎知識。
public class StuInvocationHandler<T> implements InvocationHandler {
//invocationHandler持有的被代理物件
T target;
public StuInvocationHandler(T target) {
this.target = target;
}
/**
* proxy:代表動態代理物件
* method:代表正在執行的方法
* args:代表呼叫目標方法時傳入的實參
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("代理執行" +method.getName() + "方法");
*/
//代理過程中插入監測方法,計算該方法耗時
MonitorUtil.start();
Object result = method.invoke(target, args);
MonitorUtil.finish(method.getName());
return result;
}
}
做完上面的工作後,我們就可以具體來建立動態代理物件了,上面簡單介紹瞭如何建立動態代理物件,我們使用簡化的方式建立動態代理物件:
public class ProxyTest {
public static void main(String[] args) {
//建立一個例項物件,這個物件是被代理的物件
Person zhangsan = new Student("張三");
//建立一個與代理物件相關聯的InvocationHandler
InvocationHandler stuHandler = new StuInvocationHandler<Person>(zhangsan);
//建立一個代理物件stuProxy來代理zhangsan,代理物件的每個執行方法都會替換執行Invocation中的invoke方法
Person stuProxy = (Person) Proxy.newProxyInstance(Person.class.getClassLoader(), new Class<?>[]{Person.class}, stuHandler);
//代理執行上交班費的方法
stuProxy.giveMoney();
}
}
我們執行這個ProxyTest類,先想一下,我們建立了一個需要被代理的學生張三,將zhangsan物件傳給了stuHandler中,我們在建立代理物件stuProxy時,將stuHandler作為引數了的,上面也有說到所有執行代理物件的方法都會被替換成執行invoke方法,也就是說,最後執行的是StuInvocationHandler中的invoke方法。所以在看到下面的執行結果也就理所當然了。
執行結果:
上面說到,動態代理的優勢在於可以很方便的對代理類的函式進行統一的處理,而不用修改每個代理類中的方法。是因為所有被代理執行的方法,都是通過在InvocationHandler中的invoke方法呼叫的,所以我們只要在invoke方法中統一處理,就可以對所有被代理的方法進行相同的操作了。例如,這裡的方法計時,所有的被代理物件執行的方法都會被計時,然而我只做了很少的程式碼量。
動態代理的過程,代理物件和被代理物件的關係不像靜態代理那樣一目瞭然,清晰明瞭。因為動態代理的過程中,我們並沒有實際看到代理類,也沒有很清晰地的看到代理類的具體樣子,而且動態代理中被代理物件和代理物件是通過InvocationHandler來完成的代理過程的,其中具體是怎樣操作的,為什麼代理物件執行的方法都會通過InvocationHandler中的invoke方法來執行。帶著這些問題,我們就需要對java動態代理的原始碼進行簡要的分析,弄清楚其中緣由。
四、動態代理原理分析
1、Java動態代理創建出來的動態代理類
上面我們利用Proxy類的newProxyInstance方法建立了一個動態代理物件,檢視該方法的原始碼,發現它只是封裝了建立動態代理類的步驟(紅色標準部分):
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
throws IllegalArgumentException
{
Objects.requireNonNull(h);
final Class<?>[] intfs = interfaces.clone();
final SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
}
/*
* Look up or generate the designated proxy class.
*/
Class<?> cl = getProxyClass0(loader, intfs);
/*
* Invoke its constructor with the designated invocation handler.
*/
try {
if (sm != null) {
checkNewProxyPermission(Reflection.getCallerClass(), cl);
}
final Constructor<?> cons = cl.getConstructor(constructorParams);
final InvocationHandler ih = h;
if (!Modifier.isPublic(cl.getModifiers())) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
cons.setAccessible(true);
return null;
}
});
}
return cons.newInstance(new Object[]{h});
} catch (IllegalAccessException|InstantiationException e) {
throw new InternalError(e.toString(), e);
} catch (InvocationTargetException e) {
Throwable t = e.getCause();
if (t instanceof RuntimeException) {
throw (RuntimeException) t;
} else {
throw new InternalError(t.toString(), t);
}
} catch (NoSuchMethodException e) {
throw new InternalError(e.toString(), e);
}
}
其實,我們最應該關注的是 Class<?> cl = getProxyClass0(loader, intfs);這句,這裡產生了代理類,後面程式碼中的構造器也是通過這裡產生的類來獲得,可以看出,這個類的產生就是整個動態代理的關鍵,由於是動態生成的類檔案,我這裡不具體進入分析如何產生的這個類檔案,只需要知道這個類檔案時快取在java虛擬機器中的,我們可以通過下面的方法將其列印到檔案裡面,一睹真容:
byte[] classFile = ProxyGenerator.generateProxyClass("$Proxy0", Student.class.getInterfaces());
String path = "G:/javacode/javase/Test/bin/proxy/StuProxy.class";
try(FileOutputStream fos = new FileOutputStream(path)) {
fos.write(classFile);
fos.flush();
System.out.println("代理類class檔案寫入成功");
} catch (Exception e) {
System.out.println("寫檔案錯誤");
}
對這個class檔案進行反編譯,我們看看jdk為我們生成了什麼樣的內容:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
import proxy.Person;
public final class $Proxy0 extends Proxy implements Person
{
private static Method m1;
private static Method m2;
private static Method m3;
private static Method m0;
/**
*注意這裡是生成代理類的構造方法,方法引數為InvocationHandler型別,看到這,是不是就有點明白
*為何代理物件呼叫方法都是執行InvocationHandler中的invoke方法,而InvocationHandler又持有一個
*被代理物件的例項,不禁會想難道是....? 沒錯,就是你想的那樣。
*
*super(paramInvocationHandler),是呼叫父類Proxy的構造方法。
*父類持有:protected InvocationHandler h;
*Proxy構造方法:
* protected Proxy(InvocationHandler h) {
* Objects.requireNonNull(h);
* this.h = h;
* }
*
*/
public $Proxy0(InvocationHandler paramInvocationHandler)
throws
{
super(paramInvocationHandler);
}
//這個靜態塊本來是在最後的,我把它拿到前面來,方便描述
static
{
try
{
//看看這兒靜態塊兒裡面有什麼,是不是找到了giveMoney方法。請記住giveMoney通過反射得到的名字m3,其他的先不管
m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] { Class.forName("java.lang.Object") });
m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
m3 = Class.forName("proxy.Person").getMethod("giveMoney", new Class[0]);
m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
return;
}
catch (NoSuchMethodException localNoSuchMethodException)
{
throw new NoSuchMethodError(localNoSuchMethodException.getMessage());
}
catch (ClassNotFoundException localClassNotFoundException)
{
throw new NoClassDefFoundError(localClassNotFoundException.getMessage());
}
}
/**
*
*這裡呼叫代理物件的giveMoney方法,直接就呼叫了InvocationHandler中的invoke方法,並把m3傳了進去。
*this.h.invoke(this, m3, null);這裡簡單,明瞭。
*來,再想想,代理物件持有一個InvocationHandler物件,InvocationHandler物件持有一個被代理的物件,
*再聯絡到InvacationHandler中的invoke方法。嗯,就是這樣。
*/
public final void giveMoney()
throws
{
try
{
this.h.invoke(this, m3, null);
return;
}
catch (Error|RuntimeException localError)
{
throw localError;
}
catch (Throwable localThrowable)
{
throw new UndeclaredThrowableException(localThrowable);
}
}
//注意,這裡為了節省篇幅,省去了toString,hashCode、equals方法的內容。原理和giveMoney方法一毛一樣。
}
jdk為我們的生成了一個叫$Proxy0(這個名字後面的0是編號,有多個代理類會一次遞增)的代理類,這個類檔案時放在記憶體中的,我們在建立代理物件時,就是通過反射獲得這個類的構造方法,然後建立的代理例項。通過對這個生成的代理類原始碼的檢視,我們很容易能看出,動態代理實現的具體過程。
我們可以對InvocationHandler看做一箇中介類,中介類持有一個被代理物件,在invoke方法中呼叫了被代理物件的相應方法。通過聚合方式持有被代理物件的引用,把外部對invoke的呼叫最終都轉為對被代理物件的呼叫。
代理類呼叫自己方法時,通過自身持有的中介類物件來呼叫中介類物件的invoke方法,從而達到代理執行被代理物件的方法。也就是說,動態代理通過中介類實現了具體的代理功能。
五、總結
生成的代理類:$Proxy0 extends Proxy implements Person,我們看到代理類繼承了Proxy類,所以也就決定了java動態代理只能對介面進行代理,Java的繼承機制註定了這些動態代理類們無法實現對class的動態代理。 上面的動態代理的例子,其實就是AOP的一個簡單實現了,在目標物件的方法執行之前和執行之後進行了處理,對方法耗時統計。Spring的AOP實現其實也是用了Proxy和InvocationHandler這兩個東西的。