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

在接觸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();
    }
}

運行:

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

三個疑惑

雖然說這是最簡單的一個例子了,但是在初學的時候,大家肯定和我一樣,有不少疑惑:一是不知道爲什麼需要傳入接口,二是不知道爲什麼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 = "\n";
    final static String TAB = "\t";

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

運行結果:


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

簡單分析下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動態代理有了一個新的認識。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章