RPC 核心實現原理 - 動態代理
實現過統一攔截嗎?如授權認證、性能統計,可以用 Spring AOP,不需要改動原有代碼前提下,還能實現非業務邏輯跟業務邏輯的解耦。核心就是動態代理,通過對字節碼進行增強,在方法調用時進行攔截,以便於在方法調用前後,增加處理邏輯。
1 遠程調用的魔法
使用 RPC,一般先找服務提供方要接口,通過 Maven 或其他工具把接口依賴到我們項目。
編寫業務邏輯時,若要調用提供方的接口,只需通過依賴注入把接口注入到項目,然後在代碼裏面直接調用接口的方法。
接口裏並不包含真實業務邏輯,業務邏輯都在服務提供方應用,但通過調用接口方法,確實拿到了想要結果,RPC 怎麼完成這魔術的?核心就是動態代理。
RPC 會自動給接口生成一個代理類,當我們在項目中注入接口時,運行過程中實際綁定的是這個接口生成的代理類。這樣在接口方法被調用時,它實際上是被生成代理類攔截,就可在生成的代理類裏,加入遠程調用邏輯。
“偷樑換柱”,幫用戶屏蔽遠程調用細節,實現像調用本地一樣地調用遠程的體驗。
調用流程:
2 實現原理
package com.javaedge.design.pattern.structural.proxy.dynamicproxy.jdkdynamicproxy.v1;
/**
* 要代理的接口
*
* @author JavaEdge
* @date 2023/2/4
*/
public interface Hello {
String say();
}
package com.javaedge.design.pattern.structural.proxy.dynamicproxy.jdkdynamicproxy.v1;
/**
* 真實調用對象
*
* @author JavaEdge
* @date 2023/2/4
*/
public class RealHello {
public String invoke(){
return "i'm proxy";
}
}
package com.javaedge.design.pattern.structural.proxy.dynamicproxy.jdkdynamicproxy.v1;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
/**
* JDK代理類生成
*
* @author JavaEdge
* @date 2023/2/4
*/
public class JDKProxy implements InvocationHandler {
private Object target;
JDKProxy(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] paramValues) {
return ((RealHello)target).invoke();
}
}
package com.javaedge.design.pattern.structural.proxy.dynamicproxy.jdkdynamicproxy.v1;
import org.azeckoski.reflectutils.ClassLoaderUtils;
import java.lang.reflect.Proxy;
/**
* 測試例子
*
* @author JavaEdge
* @date 2023/2/4
*/
public class TestProxy {
public static void main(String[] args) {
// 構建代理器
JDKProxy proxy = new JDKProxy(new RealHello());
ClassLoader classLoader = ClassLoaderUtils.getCurrentClassLoader();
// 把生成的代理類保存到文件
System.setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
// 生成代理類
Hello test = (Hello) Proxy.newProxyInstance(classLoader, new Class[]{Hello.class}, proxy);
// 方法調用
System.out.println(test.say());
}
}
給 Hello 接口生成一個動態代理類,並調用接口 say(),但真實返回值來自 RealHello#invoke() 的返回值。
Proxy.newProxyInstance
生成字節碼節點,即 ProxyGenerator.generateProxyClass() 用參數 saveGeneratedFiles 控制是否把生成的字節碼保存本地。把參數 saveGeneratedFiles 設置成 true,但這個參數的值是由 key 爲 “sun.misc.ProxyGenerator.saveGeneratedFiles” 的 Property 來控制的,動態生成的類會保存在工程根目錄下的 com/sun/proxy 目錄裏面。現在我們找到剛纔生成的 $Proxy0.class,通過反編譯工具打開 class 文件:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package com.sun.proxy;
import com.javaedge.design.pattern.structural.proxy.dynamicproxy.jdkdynamicproxy.v1.Hello;
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 Hello {
private static Method m1;
private static Method m2;
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 String say() throws {
try {
return (String)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");
m3 = Class.forName("com.javaedge.design.pattern.structural.proxy.dynamicproxy.jdkdynamicproxy.v1.Hello").getMethod("say");
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 類有跟 Hello 一樣簽名的 say() 方法,其中 this.h 綁定的是剛纔傳入的 JDKProxy 對象,所以當我們調用 Hello.say(),其實它是被轉發到 JDKProxy.invoke()。
3 實現方案
3.1 JDK 默認代理
要求被代理的類只能是接口,因爲生成的代理類會繼承 Proxy 類,但 Java 不支持多繼承。
對服務調用方,在使用 RPC 時正好本就是面向接口編程。使用 JDK 默認代理,最大問題就是性能。它生成後的代理類是使用反射完成方法調用。
3.2 Javassist
能操縱底層字節碼,要生成動態代理類有點複雜,但無需反射,所以性能更好。通過 Javassist 生成一個代理類後,此 CtClass 對象會被凍結,不允許再修改;否則,再次生成時會報錯。
3.3 Byte Buddy
後起之秀,Spring、Jackson 都用 Byte Buddy 完成底層代理,其提供更易操作的 API,代碼可讀性更高,生成的代理類執行速度比 Javassist 更快。
區別就只是如何生成代理類、生成的代理類裏怎麼完成方法調用。正因爲這些細小差異,才導致不同代理框架性能不同。
4 總結
動態代理框架選型:
-
• 因爲代理類是在運行中生成的,那麼代理框架生成代理類的速度、生成代理類的字節碼大小等等,都會影響到其性能——生成的字節碼越小,運行所佔資源就越小。
-
• 還有就是我們生成的代理類,是用於接口方法請求攔截的,所以每次調用接口方法的時候,都會執行生成的代理類,這時生成的代理類的執行效率就需要很高效。
-
• 最後一個是從我們的使用角度出發的,我們肯定希望選擇一個使用起來很方便的代理類框架,比如我們可以考慮:API 設計是否好理解、社區活躍度、還有就是依賴複雜度等。
FAQ
如果沒有動態代理幫我們完成方法調用攔截,用戶該怎麼完成 RPC 調用?
就需要使用靜態代理來實現,就需要用戶對原始類中所有的方法都重新實現一遍,並且爲每個方法附加相似的代碼邏輯,如果在 RPC 中,這種需要代理的類有很多個,就需要針對每個類都創建一個代理類。
調用雙方可以通過定義一套消息 id 和消息結構(纔有 protobuf 定義),也可完成遠程調用。
參考:
-
• https://www.baeldung.com/jdk-com-sun-proxy
-
• https://github.com/wangzheng0822/codedesign/tree/master/com/xzg/cd/rpc
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/-jNgAjA-JanPpeR1xbL_RA