徹底搞懂JDK動態代理核心原理

2020-10-13 01:01:17
通過前面的學習,我們知道 JDK 動態代理是代理模式的一種實現方式,那麼它是如何實現的呢?俗話說:不僅知其然,還得知其所以然。下面主要探究一下 JDK 動態代理的原理,並模仿 JDK 動態代理編寫一個屬於自己的動態代理。

JDK 動態代理採用位元組重組,重新生成物件來替代原始物件,以達到動態代理的目的。JDK 動態代理生成物件的步驟大致如下。
  1. 獲取被代理物件的參照,並且獲取它的所有介面。
  2. JDK 動態代理類重新生成一個新的類,同時新的類要實現被代理類實現的所有介面。
  3. 動態生成 Java 程式碼,新加的業務邏輯方法由一定的邏輯程式碼呼叫(在程式碼中體現),拿到被代理物件的參照。
  4. 編譯新生成的 Java 程式碼 .class 位元組碼檔案。
  5. 重新載入到 JVM 中執行。

以上過程就叫作位元組碼重組。

JDK 中有一個規範,在 ClassPath 目錄下只要是 $ 開頭的 .class 檔案,一般都是自動生成的。

下面我們檢視以 $ 開頭的 .class 檔案的內容。方法為:首先將記憶體中的物件位元組碼通過檔案流輸出到一個新的 .class 檔案,然後使用反編譯工具檢視原始碼。
public static void main(String[] args) {
    try {
        IPerson obj = (IPerson) new JdkFuDao().getInstance(new ZhangSan());
        obj.findTeacher();

        //通過反編譯工具檢視原始碼
        byte bytes[] = ProxyGenerator.generateProxyClass("$Proxy0", new Class[]{IPerson.class
        });
        FileOutputStream os = new FileOutputStream("D://$Proxy0.class");
        os.write(bytes);
        os.close();
    } catch (Exception e) {
        e.printStackTrace();
    }
}
執行以上程式碼,成功後就可以在 D 盤找到 $Proxy0.class 檔案了。

這裡我們使用 Jad 工具進行反編譯,當然您也可以使用其他反編譯工具。反編譯後得到 $Proxy0.jad 檔案,檔案內容如下:
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)

import java.lang.reflect.*;
import proxy.IPerson;

public final class $Proxy0 extends Proxy
    implements IPerson
{

    public $Proxy0(InvocationHandler invocationhandler)
    {
        super(invocationhandler);
    }

    public final boolean equals(Object obj)
    {
        try
        {
            return ((Boolean)super.h.invoke(this, m1, new Object[] {
                obj
            })).booleanValue();
        }
        catch(Error _ex) { }
        catch(Throwable throwable)
        {
            throw new UndeclaredThrowableException(throwable);
        }
    }

    public final int hashCode()
    {
        try
        {
            return ((Integer)super.h.invoke(this, m0, null)).intValue();
        }
        catch(Error _ex) { }
        catch(Throwable throwable)
        {
            throw new UndeclaredThrowableException(throwable);
        }
    }

    public final void findTeacher()
    {
        try
        {
            super.h.invoke(this, m3, null);
            return;
        }
        catch(Error _ex) { }
        catch(Throwable throwable)
        {
            throw new UndeclaredThrowableException(throwable);
        }
    }

    public final String toString()
    {
        try
        {
            return (String)super.h.invoke(this, m2, null);
        }
        catch(Error _ex) { }
        catch(Throwable throwable)
        {
            throw new UndeclaredThrowableException(throwable);
        }
    }

    private static Method m1;
    private static Method m0;
    private static Method m3;
    private static Method m2;

    static
    {
        try
        {
            m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] {
                Class.forName("java.lang.Object")
            });
            m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
            m3 = Class.forName("proxy.IPerson").getMethod("findTeacher", new Class[0]);
            m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
        }
        catch(NoSuchMethodException nosuchmethodexception)
        {
            throw new NoSuchMethodError(nosuchmethodexception.getMessage());
        }
        catch(ClassNotFoundException classnotfoundexception)
        {
            throw new NoClassDefFoundError(classnotfoundexception.getMessage());
        }
    }
}
我們發現,$Proxy0 繼承了 Proxy 類,並且實現了 IPerson 的介面,而且重寫了 equals、hashCode、toString、findTeacher() 等方法。其中,在靜態程式碼塊中,通過反射獲取了代理類的所有方法,而且儲存了所有方法的參照,重寫的方法用反射呼叫目標物件的方法。通過 invoke 執行代理類中的目標方法 findTeacher。

學到這裡,大家一定會好奇,$Proxy0.jad 中的程式碼都是從哪裡來的?這些都是 JDK 自動生成的。

手動模擬實現動態代理

下面我們不依賴 JDK,自己來動態生成原始碼、動態完成編譯,然後替代目標物件並執行。

JDK 代理需要實現 java.lang.reflect.InvocationHandler 介面,並使用 java.lang.reflect.Proxy.newProxyInstance() 方法生成代理物件。

下面我們使用 JDK 代理的類名和方法名定義,由於篇幅原因,沒有展示其原始碼,大家可以自行檢視。

仿照 InvocationHandler 介面,建立 MyInvocationHandler 介面並定義 invoke 方法,程式碼如下:
public interface MyInvocationHandler {
    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable;
}
仿照 Proxy 類,建立 MyProxy 類,程式碼如下:
/**
* 自己實現的代理類,用來生成位元組碼檔案,並動態載入到JVM中
*/
public class MyProxy {

    public static final String ln = "\r\n";

    public static Object newProxyInstance(MyClassLoader  classLoader, Class<?>[] interfaces, MyInvocationHandler h) {
        try {
            //1、動態生成原始碼.java檔案
            String src = generateSrc(interfaces);
            //2、Java檔案輸出磁碟
            String filePath = MyProxy.class.getResource("").getPath();

            File f = new File(filePath + "$Proxy0.java");
            FileWriter fw = new FileWriter(f);
            fw.write(src);
            fw.flush();
            fw.close();

            //3、把生成的.java檔案編譯成.class檔案
            JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
            StandardJavaFileManager manage = compiler.getStandardFileManager(null, null, null);
            Iterable iterable = manage.getJavaFileObjects(f);

            JavaCompiler.CompilationTask task = compiler.getTask(null, manage, null, null, null, iterable);
            task.call();
            manage.close();

            //4、編譯生成的.class檔案載入到JVM中來
            Class proxyClass = classLoader.findClass("$Proxy0");
            Constructor c = proxyClass.getConstructor(MyInvocationHandler.class);
            f.delete();

            //5、返回位元組碼重組以後的新的代理物件
            return c.newInstance(h);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    private static String generateSrc(Class<?>[] interfaces) {
        StringBuffer sb = new StringBuffer();
        sb.append(MyProxy.class.getPackage() + ";" + ln);
        sb.append("import " + interfaces[0].getName() + ";" + ln);
        sb.append("import java.lang.reflect.*;" + ln);
        sb.append("public class $Proxy0 implements " + interfaces[0].getName() + "{" + ln);
        sb.append("MyInvocationHandler h;" + ln);
        sb.append("public $Proxy0(MyInvocationHandler h) { " + ln);
        sb.append("this.h = h;");
        sb.append("}" + ln);
        for (Method m : interfaces[0].getMethods()) {
            Class<?>[] params = m.getParameterTypes();

            StringBuffer paramNames = new StringBuffer();
            StringBuffer paramValues = new StringBuffer();
            StringBuffer paramClasses = new StringBuffer();

            for (int i = 0; i < params.length; i++) {
                Class clazz = params[i];
                String type = clazz.getName();
                String paramName = toLowerFirstCase(clazz.getSimpleName());
                paramNames.append(type + " " + paramName);
                paramValues.append(paramName);
                paramClasses.append(clazz.getName() + ".class");
                if (i > 0 && i < params.length - 1) {
                    paramNames.append(",");
                    paramClasses.append(",");
                    paramValues.append(",");
                }
            }

            sb.append("public " + m.getReturnType().getName() + " " + m.getName() + "(" + paramNames.toString() + ") {" + ln);
            sb.append("try{" + ln);
            sb.append("Method m = " + interfaces[0].getName() + ".class.getMethod(\"" + m.getName() + "\",new Class[]{" + paramClasses.toString() + "});" + ln);
            sb.append((hasReturnValue(m.getReturnType()) ? "return " : "") + getCaseCode("this.h.invoke(this,m,new Object[]{" + paramValues + "})", m.getReturnType()) + ";" + ln);
            sb.append("}catch(Error _ex) { }");
            sb.append("catch(Throwable e){" + ln);
            sb.append("throw new UndeclaredThrowableException(e);" + ln);
            sb.append("}");
            sb.append(getReturnEmptyCode(m.getReturnType()));
            sb.append("}");
        }
        sb.append("}" + ln);
        return sb.toString();
    }


    private static Map<Class, Class> mappings = new HashMap<Class, Class>();

    static {
        mappings.put(int.class, Integer.class);
    }

    private static String getReturnEmptyCode(Class<?> returnClass) {
        if (mappings.containsKey(returnClass)) {
            return "return 0;";
        } else if (returnClass == void.class) {
            return "";
        } else {
            return "return null;";
        }
    }

    private static String getCaseCode(String code, Class<?> returnClass) {
        if (mappings.containsKey(returnClass)) {
            return "((" + mappings.get(returnClass).getName() + ")" + code + ")." + returnClass.getSimpleName() + "Value()";
        }
        return code;
    }

    private static boolean hasReturnValue(Class<?> clazz) {
        return clazz != void.class;
    }

    private static String toLowerFirstCase(String src) {
        char[] chars = src.toCharArray();
        chars[0] += 32;
        return String.valueOf(chars);
    }

}
建立 MyClassLoader 類,程式碼如下:
public class MyClassLoader  extends ClassLoader {

    private File classPathFile;

    public MyClassLoader () {
        String classPath = MyClassLoader .class.getResource("").getPath();
        this.classPathFile = new File(classPath);
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {

        String className = MyClassLoader .class.getPackage().getName() + "." + name;
        if (classPathFile != null) {
            File classFile = new File(classPathFile, name.replaceAll("\\.", "/") + ".class");
            if (classFile.exists()) {
                FileInputStream in = null;
                ByteArrayOutputStream out = null;
                try {
                    in = new FileInputStream(classFile);
                    out = new ByteArrayOutputStream();
                    byte[] buff = new byte[1024];
                    int len;
                    while ((len = in.read(buff)) != -1) {
                        out.write(buff, 0, len);
                    }
                    return defineClass(className, out.toByteArray(), 0, out.size());
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
        return null;
    }
}
建立 MyFuDao 類,程式碼如下:
public class MyFuDao implements MyInvocationHandler {
    private IPerson target;

    public IPerson getInstance(IPerson target) {
        this.target = target;
        Class<?> clazz = target.getClass();
        return (IPerson) MyProxy.newProxyInstance(new MyClassLoader (), clazz.getInterfaces(), this);
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        before();
        Object result = method.invoke(this.target, args);
        after();
        return result;
    }

    private void after() {
        System.out.println("雙方同意,開始輔導");
    }

    private void before() {
        System.out.println("這裡是C語言中文網,已經收集到你的需求,開始挑選");
    }
}
使用者端測試程式碼如下:
public class Test {
    public static void main(String[] args) {
        MyFuDao MyFuDao = new MyFuDao();
        IPerson zhangsan = MyFuDao.getInstance(new ZhangSan());
        zhangsan.findTeacher();

    }
}
執行結果如下:

這裡是C語言中文網,已經收集到你的需求,開始挑選
兒子張三提出要求
雙方同意,開始輔導

到這裡,手寫JDK動態代理就完成了。