手寫一個面向接口的動態代理

如題,手寫一個面向接口的動態代理。我們需要先了解jdk中的動態代理是怎么實現的。

理解生成的代碼和調用過程

設置vm參數,-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true,可以使jdk動態生成的class文件輸出到磁盤中。

設置vm options

使用下面代碼進行調試

public interface IService {
    public void service(String name )throws Exception;
}

public class ServiceImplA implements IService {
    @Override
    public void service(String name) throws Exception {
        System.out.println("ServiceImplA name" + name);
    }
}

public class DynaProxyServiceA implements InvocationHandler {
    private Object object;
    /**
     *   將目標對象關聯到InvocationHandler接口,返回代理對象obj
     *   調用代理對象的方法時,都會自動調用invoke方法
     */

    public Object bind(Object object){
        this.object = object;
        return Proxy.newProxyInstance(
                this.object.getClass().getClassLoader(),
                this.object.getClass().getInterfaces(),
                this);
    }

    @SuppressWarnings("unchecked")
    public <T> T bindInterface(Class<T> proxyInterface){
        object = proxyInterface;
        return (T)Proxy.newProxyInstance(
                proxyInterface.getClassLoader(),
                new Class[]{proxyInterface},
                this);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object result = null;
        System.out.println("log start");
        if(object instanceof Class<?>){
            Class<?> clazz = (Class<?>) object;
            for (Method clazzMethod : clazz.getDeclaredMethods()) {
                System.out.println(clazzMethod.getName());
            }
        }
        try{
            result = method.invoke(this.object,args);
            Class<?> returnType = method.getReturnType();
            System.out.println(returnType);
        }catch (Exception e){
            throw e;
        }
        System.out.println("log end");
        return result;
    }
    public static void main(String [] args) throws Exception {
        IService service = (IService)new DynaProxyServiceA()
                        .bind(new ServiceImplA());
        service.service("zhjl");
    }
}

輸出結果
log start
ServiceImplA namezhjl
void
log end

運行完程序后,會在項目的根目錄生成一個文件夾com.sun.proxy,里面會生成一個$Proxy0.class的代理類文件。

打開文件可以看到以下生成的源代碼。

public final class $Proxy0 extends Proxy implements IService {
    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 void service(String var1) throws Exception {
        try {
            super.h.invoke(this, m3, new Object[]{var1});
        } catch (Exception | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

    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("org.example.aop.IService").getMethod("service", Class.forName("java.lang.String"));
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

很容易可以發現,生成的源代碼是有規律可尋的。

  • 固定會重寫java.lang.Object中的equalstoStringhashCode三個方法
  • 對比靜態變量Method的聲明對應方法的位置與static代碼塊這三塊地方,可以發現,聲明順序與靜態代碼塊中反射獲取的順序一致,并且在各方法中執行反射出來的Method也是相等的。這里做到了提前加載被代理類的方法,然后使用到該代理類的方法時將被代理類的方法當參數傳到h.invoke中執行。

進入到被繼承的Proxy中,發現在生成的類中使用的h就是InvocationHandler這個類。即我們在使用動態代理時所要實現才那個類!

image
image.png

所以這個地方是生成一個回調代理類的invoke方法的類,來調用invoke時決定什么時候執行被代理類的service方法就能達到切面增強這個方法的效果

執行代理類的流程圖

理解完了生成代碼的意義和處理流程,剩下的就是怎么構造這些代碼并將他編譯成.class文件和被類加載器加載并被創建實例被我們所使用了。

如何構造和加載

參考Proxy.newProxyInstance的代碼,看到getProxyClass0,前面的代碼忽略,看注釋就知道這里是生成指定代理類的方法。直接點進去就好了。

生成代碼的方法
image
image
image

記住這兩個變量分別是KeyFactoryProxyClassFactory。然后回到proxyClassCache.get(loader, interfaces)

image
image

根據實現的接口數量來返回Key

往下走,最后指向的類都是Factory,并在最后執行get方法。

image

最后回到ProxyClassFactory這個類的apply方法。

image

最后在方法的底部發現ProxyGenerator.generateProxyClass對應的作用就是構造相當于構造.java源文件。
defineClass0相當于javac編譯成.class文件并loadClass返回對應的類

image

這個生成方法較復雜,經過簡單的查看源碼,已經知道步驟如下:

  • 構造.java源文件
  • 編譯成.class后加載類

筆者的方法比較簡單,直接使用freemaker來構造源文件,需要傳入以下四個參數。

  • package->生成類所在的包
  • className->生成的代理類名稱
  • interface->實現的接口類全類名
  • methodList->需要重寫的方法列表

freemaker模板如下

package ${package};
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
/**
* @Author: Jdragon
* @email: 1061917196@qq.com
* @Description: jdk動態代理實質
*/
public class ${className} extends Proxy implements ${interface} {
    public ${className}(InvocationHandler h) {
        super(h);
    }
<#list 0..(methodList!?size-1) as i>
    @Override
    public final ${methodList[i].retType} ${methodList[i].methodName}(
        <#list methodList[i].paramList as param>
                ${param} var${param_index}<#if param_has_next>,</#if>
        </#list>) {
        try {
            <#if (methodList[i].retType!="void")>return (${methodList[i].retType})</#if>
            <#if (methodList[i].paramList?size==0)>
            super.h.invoke(this, m${i}, (Object[])null);
            <#else>
            super.h.invoke(this, m${i}, new Object[]{
                <#list 0..(methodList[i].paramList!?size-1) as k>var${k}
                    <#if k_has_next>,</#if>
                </#list>});
            </#if>
        } catch (RuntimeException | Error e) {
            throw e;
        }catch (Throwable throwable) {
            throw new UndeclaredThrowableException(throwable);
        }
    }
</#list>
<#list 0..(methodList!?size-1) as i>
    private static Method m${i};
</#list>
    static{
        try{
            <#list 0..(methodList!?size-1) as i>
                m${i} = Class.forName("${methodList[i].className}").getMethod("${methodList[i].methodName}"
                <#list methodList[i].paramList as param>
                    ,Class.forName("${param}")
                </#list>);
            </#list>
        }  catch (NoSuchMethodException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

主要代碼。以下代碼不完整,可到gitee獲取源碼

public class JdkProxyFactory {

    private final static String LN = System.lineSeparator();

    private final static AtomicInteger PROXY_INDEX = new AtomicInteger(0);

    private final static Boolean SAVE_GENERATED_FILES = Boolean.valueOf(System.getProperty("sun.misc.ProxyGenerator.saveGeneratedFiles"));

    private final static String USER_DIR = System.getProperty("user.dir") + "/com/jdragon/proxy/";

    private final static String PACKAGE_NAME = "com.jdragon.proxy";

    public static Object newProxyInstance(ClassLoader classLoader,
                                          @NotNull Class<?>[] interfaces,
                                          @NotNull InvocationHandler h) {
        try {
            if (interfaces.length == 0) {
                throw new Exception("至少要實現一個接口");
            }
            //使用被代理類的類名和自增數定義代理類的名字
            String proxyClass = interfaces[0].getSimpleName() + "$Proxy" + PROXY_INDEX.incrementAndGet();
            //加載代理類
            Class<?> loadClass = loadClass(interfaces[0], proxyClass);
            Constructor<?> constructor = loadClass.getDeclaredConstructor(InvocationHandler.class);
            return constructor.newInstance(h);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * @Description: 加載類
    **/
    private static Class<?> loadClass(Class<?> interfaces, String proxyClassName) throws Exception {
        String classPath = PACKAGE_NAME + "." + proxyClassName;
        //構建源代碼
        String sourceCode = generateSourceCode(interfaces, proxyClassName);
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        try (JavaFileManager manager = new MemoryFileManager(compiler.getStandardFileManager(null, null, null))) {
            List<JavaFileObject> files = Collections.singletonList(new MemoryJavaFileObject(proxyClassName, sourceCode));
            JavaCompiler.CompilationTask task = compiler.getTask(null, manager, null, null, null, files);
            if (!task.call()) {
                throw new Exception("任務調用異常");
            }
            ClassLoader classLoader = manager.getClassLoader(null);
            Class<?> aClass = manager.getClassLoader(null).loadClass(classPath);
            if (SAVE_GENERATED_FILES) {
                save(proxyClassName, classLoader);
            }
            return aClass;
        }
    }

    /**
     * @Description: 構造源代碼
    **/
    private static String generateSourceCode(Class<?> interfaces, String proxyClassName) {
        String interfaceName = interfaces.getName();
        List<MethodEntity> methodEntities = new ArrayList<>();
        methodEntities.add(new MethodEntity(Object.class, "toString", null,
                String.class));
        methodEntities.add(new MethodEntity(Object.class, "hashCode", null,
                int.class));
        methodEntities.add(new MethodEntity(Object.class, "equals", Collections.singletonList(Object.class.getName()),
                boolean.class));

        for (Method declaredMethod : interfaces.getDeclaredMethods()) {
            MethodEntity methodEntity = new MethodEntity();
            methodEntity.setClassName(interfaces);
            methodEntity.setMethodName(declaredMethod.getName());
            List<String> params = new ArrayList<>();
            for (Parameter parameter : declaredMethod.getParameters()) {
                String paramTypeName = parameter.getType().getName();
                params.add(paramTypeName);
            }
            methodEntity.setParamList(params);
            methodEntity.setRetType(declaredMethod.getReturnType());
            methodEntity.setTransferType(declaredMethod.getReturnType());
            methodEntities.add(methodEntity);
        }

        //利用定義好的模板傳入參數到freemaker進行遍歷填充,最后獲得源代碼
        Map<String, Object> map = new HashMap<>(8);
        map.put("package", PACKAGE_NAME);
        map.put("className", proxyClassName);
        map.put("interface", interfaceName);
        map.put("methodList", methodEntities);
        FreeMakerUtil freeMakerUtil = new FreeMakerUtil("/template/freemaker/", "ftl");
        return freeMakerUtil.printString("proxy", map);
    }
}


最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,572評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,071評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,409評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,569評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,360評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,895評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,979評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,123評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,643評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,559評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,742評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,250評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,981評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,363評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,622評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,354評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,707評論 2 370

推薦閱讀更多精彩內容