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 總結

動態代理框架選型:

FAQ

如果沒有動態代理幫我們完成方法調用攔截,用戶該怎麼完成 RPC 調用?

就需要使用靜態代理來實現,就需要用戶對原始類中所有的方法都重新實現一遍,並且爲每個方法附加相似的代碼邏輯,如果在 RPC 中,這種需要代理的類有很多個,就需要針對每個類都創建一個代理類。

調用雙方可以通過定義一套消息 id 和消息結構(纔有 protobuf 定義),也可完成遠程調用。

參考:


本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/-jNgAjA-JanPpeR1xbL_RA