为了账号安全,请及时绑定邮箱和手机立即绑定

有点深度的聊聊JDK动态代理

标签:
Java

在接触SpringAOP的时候,大家一定会被这神奇的功能所折服,想知道其中的奥秘,底层到底是如何实现的。于是,大家会通过搜索引擎,知道了一个陌生的名词:动态代理,慢慢的又知道了动态代理有多种实现方式,比如 JDK动态代理Cglib 等等。今天我就来简单说说JDK动态代理

JDK动态代理的简单应用

我们还是从一个最简单的例子着手:

首先我们需要定义一个接口:

public interface UserService {    void query();
}

然后实现这个接口:

blic 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 = "\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();
    }
}

运行结果:
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动态代理有了一个新的认识。

原文出处: https://www.cnblogs.com/CodeBear/p/10245442.html  

点击查看更多内容
2人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消