有點深度的聊聊JDK動態代理

CodeBear發表於2019-01-09

在接觸SpringAOP的時候,大家一定會被這神奇的功能所折服,想知道其中的奧祕,底層到底是如何實現的。於是,大家會通過搜尋引擎,知道了一個陌生的名詞:動態代理,慢慢的又知道了動態代理有多種實現方式,比如 JDK動態代理Cglib 等等。今天我就來簡單說說JDK動態代理

JDK動態代理的簡單應用

我們還是從一個最簡單的例子著手:

首先我們需要定義一個介面:

public interface UserService {
    void query();
}

然後實現這個介面:

public class UserServiceImpl implements UserService {
    public void query() {
        System.out.println("查詢使用者資訊");
    }
}

定義一個類,需要實現InvocationHandler:

public class MyInvocationHandler implements InvocationHandler {

    Object target;

    public MyInvocationHandler(Object target) {
        this.target = target;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("進入了invoke");
        method.invoke(target);
        System.out.println("執行了invoke");
        return null;
    }
}

然後就是Main方法了:

public class Main {
    public static void main(String[] args) {
        MyInvocationHandler myInvocationHandler = new MyInvocationHandler(new UserServiceImpl());
        Object o = Proxy.newProxyInstance(Main.class.getClassLoader(),
                new Class[]{UserService.class}
                , myInvocationHandler);

        ((UserService)o).query();
    }
}

執行:

image.png

可以看到,一切正常,成功的執行了增強的邏輯,也執行了目標方法。

三個疑惑

雖然說這是最簡單的一個例子了,但是在初學的時候,大家肯定和我一樣,有不少疑惑:一是不知道為什麼需要傳入介面,二是不知道為什麼JDK動態代理只能代理介面,三是不知道類載入器的作用。還有,就是程式碼比較複雜。

這三個疑惑困擾我很久,直到我跟著部落格,自己手擼一個閹割版的JDK動態代理,並且簡單的看了下JDK最終生成的程式碼以及原始碼才明白。

寫一個閹割版的JDK動態代理

我們先來分析下MyInvocationHandler類中的invoke方法,方法有三個引數,第一個引數是代理類,第二個引數是方法,第三個引數是 執行方法需要用到的引數。方法內部實現了兩個邏輯,一個是增強邏輯 ,一個是執行目標方法。我們不禁的想,如果我們可以自動生成一個類,去呼叫MyInvocationHandler中的invoke方法是不是就可以實現動態代理了。

人有多大膽,地有多大產,這的確是一個大膽瘋狂的想法,但是這確實可以辦到,主要有如下幾個步驟:

  1. 拼接代理類的程式碼
  2. 輸出.java檔案
  3. 編譯.java檔案成.class檔案
  4. 裝載.class檔案
  5. 建立並返回代理類物件

為了方便,就不考慮返回值和帶參的情況了,我仿照現有的MyInvocationHandler 寫了一個閹割版的MockInvocationHandler類:

public class MockInvocationHandler {

    private Object targetObject;

    public MockInvocationHandler(Object targetObject) {
        this.targetObject = targetObject;

    }

    public void invoke(Method targetMethod) {
        try {
            System.out.println("進入了invoke");
            targetMethod.invoke(targetObject, null);
            System.out.println("結束了invoke");
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}

要呼叫到MockInvocationHandler 中的invoke方法,生成的代理類大概可能也許長這個樣子:

public class $Proxy implements 需要代理的介面{
     MockInvocationHandler h;
     public $Proxy (MockInvocationHandler h ) {this.h = h; }
     public void query(){
      try{ 
        //method=需要的執行方法
         this.h.invoke(method);
        }catch(Exception ex){}
    }
}

好了,接下來就是體力活了,直接貼上程式碼:

public class MockProxy {

    final static String ENTER = "
";
    final static String TAB = "	";

    public static Object newProxyInstance(Class interfaceClass,MockInvocationHandler h) {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("package com.codebear;");
        stringBuilder.append(ENTER);
        stringBuilder.append("import java.lang.reflect.*;");
        stringBuilder.append(ENTER);
        stringBuilder.append("public class $Proxy implements " + interfaceClass.getName() + "{");
        stringBuilder.append(ENTER);
        stringBuilder.append(TAB);
        stringBuilder.append(" MockInvocationHandler h;");
        stringBuilder.append(ENTER);
        stringBuilder.append(TAB);
        stringBuilder.append(" public $Proxy (MockInvocationHandler h ) {this.h = h; }");
        stringBuilder.append(ENTER);
        stringBuilder.append(TAB);
        for (Method method : interfaceClass.getMethods()) {
            stringBuilder.append(" public void " + method.getName() + "(){");
            stringBuilder.append(ENTER);
            stringBuilder.append(TAB);
            stringBuilder.append("  try{ ");
            stringBuilder.append(ENTER);
            stringBuilder.append(TAB);
            stringBuilder.append(TAB);
            stringBuilder.append(" Method method = " + interfaceClass.getName() + ".class.getMethod("" + method.getName() + "");");
            stringBuilder.append(ENTER);
            stringBuilder.append(TAB);
            stringBuilder.append(TAB);
            stringBuilder.append(" this.h.invoke(method);");
            stringBuilder.append(ENTER);
            stringBuilder.append(TAB);
            stringBuilder.append(TAB);
            stringBuilder.append("}catch(Exception ex){}");
            stringBuilder.append(ENTER);
            stringBuilder.append(TAB);
            stringBuilder.append("}");
            stringBuilder.append(ENTER);
            stringBuilder.append("}");
        }
        String content = stringBuilder.toString();

        try {
            String filePath = "D:\com\codebear\$Proxy.java";
            File file = new File(filePath);

            File fileParent = file.getParentFile();
            if (!fileParent.exists()) {
                fileParent.mkdirs();
            }

            FileWriter fileWriter = new FileWriter(file);
            fileWriter.write(content);
            fileWriter.flush();
            fileWriter.close();

            JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
            StandardJavaFileManager fileManager = compiler.getStandardFileManager
                    (null, null, null);
            Iterable iterable = fileManager.getJavaFileObjects(filePath);
            JavaCompiler.CompilationTask task = compiler.getTask
                    (null, fileManager, null, null, null, iterable);
            task.call();
            fileManager.close();

            URLClassLoader classLoader = new URLClassLoader(new URL[]{new URL("file:D:\\")});
            Class<?> clazz = classLoader.loadClass("com.codebear.$Proxy");
            Constructor<?> constructor = clazz.getConstructor(MockInvocationHandler.class);
            return constructor.newInstance(h);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return null;
    }
}

然後測試一下:

public class Main {
    public static void main(String[] args) {
        MockInvocationHandler mockInvocationHandler=new MockInvocationHandler(new UserServiceImpl());
        UserService userService = (UserService)MockProxy.
                newProxyInstance(UserService.class, mockInvocationHandler);
        userService.query();
    }
}

執行結果:
image.png

好了,在不考慮效能,可維護性,安全性的情況下,我們閹割版的動態代理就完成了。程式碼難度不是很大,就是比較考驗反射和耐心。

簡單分析下JDK原始碼

原始碼基於JDK1.8

 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);
        }

        /*
         * 得到代理類
         */
        Class<?> cl = getProxyClass0(loader, intfs);

        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});//通過構造方法,建立物件,傳入InvocationHandler 物件
        } catch (IllegalAccessException|InstantiationException e) {
            throw new InternalError(e.toString(), e);
        } catch (InvocationTargetException e) {
            Throwable t = e.getCause();
            if (t instanceof RuntimeException) {
                throw (RuntimeException) t;
            } else {
                throw new InternalError(t.toString(), t);
            }
        } catch (NoSuchMethodException e) {
            throw new InternalError(e.toString(), e);
        }
    }

簡單的看下原始碼,我們一下子就能把目光移動到getProxyClass0方法了,這才是我們需要關心的,我們點進去:

  private static Class<?> getProxyClass0(ClassLoader loader,
                                           Class<?>... interfaces) {
        //當介面大於65535報錯
        if (interfaces.length > 65535) {
            throw new IllegalArgumentException("interface limit exceeded");
        }

        return proxyClassCache.get(loader, interfaces);
    }

這方法可以說什麼事情也沒幹,但是通過最後的proxyClassCache.get可以很容易的知道JDK的動態代理是用了快取的,我們需要關注的方法在get裡面,繼續點進去:

public V get(K key, P parameter) {
        Objects.requireNonNull(parameter);

        expungeStaleEntries();
        //通過上游方法,可以知道key是類載入器,這裡是通過類載入器可以獲得第一層key
       Object cacheKey = CacheKey.valueOf(key, refQueue);
        
       //我們檢視map的定義,可以看到map變數是一個兩層的ConcurrentMap
       ConcurrentMap<Object, Supplier<V>> valuesMap = map.get(cacheKey);//通過第一層key嘗試獲取資料
       //如果valuesMap 為空,就新建一個ConcurrentHashMap,
       //key就是生成出來的cacheKey,並把這個新建的ConcurrentHashMap推到map
       if (valuesMap == null) {
            ConcurrentMap<Object, Supplier<V>> oldValuesMap
                = map.putIfAbsent(cacheKey,
                                  valuesMap = new ConcurrentHashMap<>());
            if (oldValuesMap != null) {
                valuesMap = oldValuesMap;
            }
        }

        //通過上游方法可以知道key是類載入器,parameter是類本身,這裡是通過類載入器和類本身獲得第二層key
        Object subKey = Objects.requireNonNull(subKeyFactory.apply(key, parameter));
        Supplier<V> supplier = valuesMap.get(subKey);
        Factory factory = null;

        while (true) {
            if (supplier != null) {
                //如果有快取,直接呼叫get方法後返回,當沒有快取,會繼續執行後面的程式碼,
                //由於while (true),會第二次跑到這裡,再get返回出去,
                //其中get方法呼叫的是WeakCahce中的靜態內部類Factory的get方法
                V value = supplier.get();
                if (value != null) {
                    return value;
                }
            }
            //當factory為空,會建立Factory物件
            if (factory == null) {
                factory = new Factory(key, parameter, subKey, valuesMap);
            }

            if (supplier == null) {
                supplier = valuesMap.putIfAbsent(subKey, factory);
                if (supplier == null) {
                    //當沒有代理類快取的時候,會執行到這裡,把Factory的物件賦值給supplier ,
                    //進行下一次迴圈,supplier就不為空了,可以呼叫get方法返回出去了,
                    //這個Factory位於WeakCahce類中,是一個靜態內部類
                    supplier = factory;
                }
            } else {
                if (valuesMap.replace(subKey, supplier, factory)) {
                    supplier = factory;
                } else {
                    supplier = valuesMap.get(subKey);
                }
            }
        }
    }

這裡面的程式碼比較複雜,簡單的來說:

  • JDK動態代理是用了兩層的map去快取,第一個層是類載入器,第二層是 類載入器+本身
  • 當有快取,直接呼叫get並且返回,反之繼續執行下面的程式碼,為supplier進行賦值,由於while (true),會第二次跑到這裡,再呼叫get()返回出去。核心在於supplier.get(),它呼叫的是WeakCahce中的靜態內部類Factory的get(),裡面就是 獲取代理類的方法了。

讓我們看下supplier.get()方法:

 value = Objects.requireNonNull(valueFactory.apply(key, parameter));

核心在於這一句話,但是valueFactory是什麼?我們可以檢視它的定義:

 private final BiFunction<K, P, V> valueFactory;

我們再看下它的WeakCahce構造方法:

 public WeakCache(BiFunction<K, P, ?> subKeyFactory,
                     BiFunction<K, P, V> valueFactory) {
        this.subKeyFactory = Objects.requireNonNull(subKeyFactory);
        this.valueFactory = Objects.requireNonNull(valueFactory);
    }

我們肯定在哪邊呼叫過這個構造方法了,在Proxy類中有這樣的定義:

 private static final WeakCache<ClassLoader, Class<?>[], Class<?>>
        proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory());

這個proxyClassCache有沒有很熟悉, 是的,它就在getProxyClass0方法中用到了,這裡建立了WeakCache物件,並且呼叫了帶兩個引數的構造方法,第二個引數是ProxyClassFactory物件,也就對應了WeakCache中第二個引數BiFunction<K, P, V> valueFactory,然後把值賦值給了final valueFactory,valueFactory.apply所以最終會呼叫ProxyClassFactory中的apply方法。關鍵在於:

 byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
                proxyName, interfaces, accessFlags);//生成代理類的二進位制陣列
            try {
                 //內部是native標記的方法,是用C或者C++實現的,這裡不深究
                //方法內部就是通過類載入器和上面生成的代理類的二進位制陣列等資料,經過處理,成為Class
                return defineClass0(loader, proxyName,
                                    proxyClassFile, 0, proxyClassFile.length);
            } catch (ClassFormatError e) {
                throw new IllegalArgumentException(e.toString());
            }

generateProxyClass方法內部生成了代理類的二進位制陣列,具體是怎麼生成的,大家可以點進去自己看看,這裡就不再繼續往下了,因為我們的目標就是找到generateProxyClass方法,然後自己寫一個方法,去執行generateProxyClass,把返回的byte[]輸出到.class檔案,利用idea的反編譯功能,看看最終生成出來的代理類是什麼樣子的:

 byte[] $proxies = ProxyGenerator.generateProxyClass("$Proxy", new Class[]{UserService.class});
        File file=new File("D:\$Proxy.class");
        FileOutputStream outputStream = null;
        try {
            outputStream = new FileOutputStream(file);
            try {
                outputStream.write($proxies);
            } catch (IOException e) {
                e.printStackTrace();
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }

執行,發現D盤出現了$Proxy.class檔案,我們把它拖到idea裡面,看看它的真面目,因為生成的程式碼還是比較長的,我這裡只把核心程式碼貼出來:

//繼承了Proxy類
public final class $Proxy extends Proxy implements UserService {
    public $Proxy(InvocationHandler var1) throws  {
        super(var1);
    }
    public final void query() throws  {
        try {
            super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }
}

這程式碼有沒有很熟悉,很接近我們自己手寫動態代理生成的代理類。

解開疑惑

好了,先是自己手寫了一個閹割版的動態代理,然後簡單的看了下JDK動態代理原始碼,也看了下JDK動態代理生成的代理類。這樣,就可以解開上面的三個疑惑了:

  1. 類載入器是幹嘛的:其一:JDK內部需要通過類載入作為快取的key 其二:需要類載入器生成class
  2. 為什麼需要介面:因為生成的代理類需要實現這個介面
  3. 為什麼JDK動態代理只能代理介面:因為生成的代理類已經繼承了Proxy類,Java是單繼承的,所以沒法再繼承另外一個類了。

有一些部落格上可能會說cglib和JDK動態代理的區別,cglib是通過操作位元組碼去完成代理的,其實JDK動態代理也操作了位元組碼

經過這麼一分析,相信大家對JDK動態代理有了一個新的認識。

相關文章