JDK動態代理可以不基于接口嗎?
答案是:不行!
舉個簡單的例子
在分析原因之前,我們先完整的看一下實現jdk動態代理需要幾個步驟,首先需要定義一個接口:
public interface Worker {
void work();
}
再寫一個基于這個接口的實現類:
public class Programmer implements Worker {
@Override
public void work() {
System.out.println("coding...");
}
}
自定義一個Handler,實現InvocationHandler接口,通過重寫內部的invoke方法實現邏輯增強。其實這個InvocationHandler可以使用匿名內部類的形式定義,這里為了結構清晰拿出來單獨聲明。
public class WorkHandler implements InvocationHandler {
private Object target;
WorkHandler(Object target){
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getName().equals("work")) {
System.out.println("before work...");
Object result = method.invoke(target, args);
System.out.println("after work...");
return result;
}
return method.invoke(target, args);
}
}
在main方法中進行測試,使用Proxy類的靜態方法newProxyInstance生成一個代理對象并調用方法:
public static void main(String[] args) {
Programmer programmer = new Programmer();
Worker worker = (Worker) Proxy.newProxyInstance(
programmer.getClass().getClassLoader(),
programmer.getClass().getInterfaces(),
new WorkHandler(programmer));
worker.work();
}
執行上面的代碼,輸出:
before work...
coding...
after work...
可以看到,執行了方法邏輯的增強,到這,一個簡單的動態代理過程就實現了,下面我們分析一下源碼。
Proxy源碼解析
既然是一個代理的過程,那么肯定存在原生對象和代理對象之分,下面我們查看源碼中是如何動態的創建代理對象的過程。上面例子中,創建代理對象調用的是Proxy類的靜態方法newProxyInstance,查看一下源碼:
@CallerSensitive
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
}
概括一下上面代碼中重點部分:
- 在checkProxyAccess方法中,進行參數驗證
- 在getProxyClass0方法中,生成一個代理類Class或者尋找已生成過的代理類的緩存
- 通過getConstructor方法,獲取生成的代理類的構造方法
- 通過newInstance方法,生成實例對象,也就是最終的代理對象
上面這個過程中,獲取構造方法和生成對象都是直接利用的反射,而需要重點看看的是生成代理類的方法getProxyClass0。
private static Class<?> getProxyClass0(ClassLoader loader,
Class<?>... interfaces) {
if (interfaces.length > 65535) {
throw new IllegalArgumentException("interface limit exceeded");
}
// If the proxy class defined by the given loader implementing
// the given interfaces exists, this will simply return the cached copy;
// otherwise, it will create the proxy class via the ProxyClassFactory
return proxyClassCache.get(loader, interfaces);
}
注釋寫的非常清晰,如果緩存中已經存在了就直接從緩存中取,這里的proxyClassCache是一個WeakCache類型,如果緩存中目標classLoader和接口數組對應的類已經存在,那么返回緩存的副本。如果沒有就使用ProxyClassFactory去生成Class對象。中間的調用流程可以省略,最終實際調用了ProxyClassFactory的Apply方法生成Class。在apply方法中,主要做了下面3件事。
- 首先,根據規則生成文件名:
if (proxyPkg == null) {
// if no non-public proxy interfaces, use com.sun.proxy package
proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";
}
/*
* Choose a name for the proxy class to generate.
*/
long num = nextUniqueNumber.getAndIncrement();
String proxyName = proxyPkg + proxyClassNamePrefix + num;
如果接口被定義為public公有,那么默認會使用com.sun.proxy作為包名,類名是$Proxy加上一個自增的整數值,初始時是0,因此生成的文件名是$Proxy0。
如果是非公有接口,那么會使用和被代理類一樣的包名,可以寫一個private接口的例子進行一下測試。
package com.hydra.test.face;
public class InnerTest {
private interface InnerInterface {
void run();
}
class InnerClazz implements InnerInterface {
@Override
public void run() {
System.out.println("go");
}
}
}
這時生成的代理類的包名為com.hydra.test.face,與被代理類相同:
- 然后,利用ProxyGenerator.generateProxyClass方法生成代理的字節碼數組:
byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
proxyName, interfaces, accessFlags);
在generateProxyClass方法中,有一個重要的參數會發揮作用:
private static final boolean saveGeneratedFiles = (Boolean)AccessController.doPrivileged(new GetBooleanAction("sun.misc.ProxyGenerator.saveGeneratedFiles"));
如果這個屬性被配置為true,那么會把字節碼存儲到硬盤上的class文件中,否則不會保存臨時的字節碼文件。
- 最后,調用本地方法defineClass0生成Class對象:
return defineClass0(loader, proxyName,
proxyClassFile, 0, proxyClassFile.length);
返回代理類的Class后的流程我們在前面就已經介紹過了,先獲得構造方法,再使用構造方法反射的方式創建代理對象。
神秘的代理對象
創建代理對象流程的源碼分析完了,我們可以先通過debug來看看上面生成的這個代理對象究竟是個什么:
和源碼中看到的規則一樣,是一個Class為$Proxy0的神秘對象,再看一下代理對象的Class的詳細信息:
類的全限定名是com.sun.proxy.$Proxy0,在上面我們提到過,這個類是在運行過程中動態生成的,并且程序執行完成后,會自動刪除掉class文件。如果想要保留這個臨時文件不被刪除,就要修改我們上面提到的參數,具體操作起來有兩種方式,第一種是在啟動VM參數中加入:
-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true
第二種是在代碼中加入下面這一句,注意要加在生成動態代理對象之前:
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
使用了上面兩種方式中的任意一種后,就可以保存下來臨時的字節碼文件了,需要注意這個文件生成的位置,并不是在target目錄下,而是生成在項目目錄下的comsunproxy中,正好和默認生成的包名對應。
拿到字節碼文件后,就可以使用反編譯工具來反編譯它了,這里使用jad在cmd下一條命令直接搞定:
jad -s JAVA $Proxy0.class
看一下反編譯后$Proxy0.java文件的內容,下面的代碼中,我只保留了核心部分,省略了無關緊要的equals、toString、hashCode方法的定義。
public final class $Proxy0 extends Proxy implements Worker{
public $Proxy0(InvocationHandler invocationhandler){
super(invocationhandler);
}
public final void work(){
try{
super.h.invoke(this, m3, null);
return;
}catch(Error _ex) { }
catch(Throwable throwable){
throw new UndeclaredThrowableException(throwable);
}
}
private static Method m3;
static {
try{
m3 = Class.forName("com.hydra.test.Worker").getMethod("work", new Class[0]);
//省略其他Method
}//省略catch
}
}
這個臨時生成的代理類$Proxy0中主要做了下面的幾件事:
- 在這個類的靜態代碼塊中,通過反射初始化了多個靜態方法Method變量,除了接口中的方法還有equals、toString、hashCode這三個方法
- 繼承父類Proxy,實例化的過程中會調用父類的構造方法,構造方法中傳入的invocationHandler對象實際上就是我們自定義的WorkHandler的實例
- 實現了自定義的接口Worker,并重寫了work方法,方法內調用了InvocationHandler的invoke方法,也就是實際上調用了WorkHandler的invoke方法
- 省略的equals、toString、hashCode方法實現也一樣,都是調用super.h.invoke()方法
到這里,整體的流程就分析完了,我們可以用一張圖來簡要總結上面的過程:
為什么要有接口?
通過上面的分析,我們已經知道了代理對象是如何生成的了,那么回到開頭的問題,為什么jdk的動態代理一定要基于接口呢?
其實如果不看上面的分析,我們也應該知道,要擴展一個類有常見的兩種方式,繼承父類或實現接口。這兩種方式都允許我們對方法的邏輯進行增強,但現在不是由我們自己來重寫方法,而是要想辦法讓jvm去調用InvocationHandler中的invoke方法,也就是說代理類需要和兩個東西關聯在一起:
- 被代理類
- InvocationHandler
而jdk處理這個問題的方式是選擇繼承父類Proxy,并把InvocationHandler存在父類的對象中:
public class Proxy implements java.io.Serializable {
protected InvocationHandler h;
protected Proxy(InvocationHandler h) {
Objects.requireNonNull(h);
this.h = h;
}
//...
}
通過父類Proxy的構造方法,保存了創建代理對象過程中傳進來的InvocationHandler的實例,使用protected修飾保證了它可以在子類中被訪問和使用。但是同時,因為java是單繼承的,因此在繼承了Proxy后,只能通過實現目標接口的方式來實現方法的擴展,達到我們增強目標方法邏輯的目的。
扯點別的
其實看完源碼、弄明白代理對象生成的流程后,我們還可以用另一種方法實現動態代理:
public static void main(String[] args) throws Exception {
Class<?> proxyClass = Proxy.getProxyClass(Test3.class.getClassLoader(), Worker.class);
Constructor<?> constructor = proxyClass.getConstructor(InvocationHandler.class);
InvocationHandler workHandler = new WorkHandler(new Programmer());
Worker worker = (Worker) constructor.newInstance(workHandler);
worker.work();
}
運行結果與之前相同,這種寫法其實就是抽出了我們前面介紹的幾個核心方法,中間省略了一些參數的校驗過程,這種方式可以幫助大家熟悉jdk動態代理原理,但是在使用過程中還是建議大家使用標準方式,相對更加安全規范。
總結
本文從源碼以及實驗的角度,分析了jdk動態代理生成代理對象的流程,通過代理類的實現原理分析了為什么jdk動態代理一定要基于接口實現。總的來說,jdk動態代理的應用還是非常廣泛的,例如在Spring、Mybatis以及Feign等很多框架中動態代理都被大量的使用,可以說學好jdk動態代理,對于我們閱讀這些框架的底層源碼還是很有幫助的。
以上文章來源于碼農參上 ,作者Dr Hydra