秦怀杂货店

General Store

  • 首页
  • 文章归档
  • 标签
  • 分类
  • 关于页面

  • 搜索
随便聊聊 数据结构 小游戏 数据库 Docker Springboot 系统设计 雪花算法 分布式 海量ip 最长回文子串 算法 面试题 线程池 多线程 线程 java学习 布隆过滤器 github 架构设计 docsify Git JVM LeetCode 杂货思考 设计模式 Lambda native isAssignableFrom 反射 剑指Offer mybatis SPI JDBC 编程工具 Java基础 集合

设计模式【3.2】-- JDK动态代理源码分析有多香?

发表于 2021-11-06 | 分类于 设计模式 | 0 | 阅读次数 346

前面文章有说到代理模式:http://aphysia.cn/archives/dynamicagentdesignpattern

那么回顾一下,代理模式怎么来的?假设有个需求:

在系统中所有的 controller 类调用方法之前以及之后,打印一下日志。

假设原来的代码:

public class Person{
    public void method(){
        // 表示自己的业务逻辑
        process();
    }
}

如果在所有的类里面都添加打印方法,这样肯定是不现实的,如果我有几百个这样的类,写到崩溃,况且重复代码太多,冗余,还耦合到一块了,要是我下次不打日志了,做其他的,那几百个类又全部改一遍。

public class Person{
    public void method(){
        log();
        // 表示自己的业务逻辑
        process();
        log();
    }
}

静态代理

怎么样写比较优美呢?静态代理 这时候出场了,先把方法抽象成为接口:

public class IProxy(){
    public void method();
}

让具体的类去实现 IProxy,写自己的业务逻辑,比如:

public class Person implements IProxy(){
    public void method(){
        // 表示自己的业务逻辑
        process();
    }
}

然后弄个代理类,对方法进行增强:

public class PersonProxy implements IProxy{
    private IProxy target;
    public PersonProxy(IProxy target){
        this.target = target;
    }
    @Override
    public void method() {
        log();
        target.method();
        log();
    }
}

调用的时候,把真实的对象放到代理类的构造器里面,就可以得到一个代理类,对它的方法进行增强,好处就是,如果下次我要改,不打日志,做其他事情,我改代理类就可以了,不用到处改我的目标类的方法,而坏处还是很明显,要增强哪一个类,就要为它写一个代理类,这样好像不是很合理。

动态代理

怎么样能让他自动生成代理对象呢? 动态代理做的就是这个事情,它可以 动态 的根据我们提供的类,生成代理类的对象。

最主要的,是在运行时,动态生成,只要传入我们要代理增强的类相关的信息,比如类对象本身,类加载器,类的接口等,就可以生成,不用提前知道它是 A 类,B 类还是 C 类。

动态代理主要有三种实现方法,今天我们重点分析 JDK 动态代理:

  • JDK 代理:使用 JDK 提供的官方的 Proxy
  • 第三方 CGlib 代理:使用 CGLib 的 Enhancer 类创建代理对象
  • javassit:Javassist 是一个开源的分析、编辑和创建 Java 字节码的类库。是由东京工业大学的数学和计算机科学系的 Shigeru Chiba (千叶 滋)所创建的。

JDK 动态代理

使用步骤

  1. 新建一个接口
  2. 新建一个类,实现该接口
  3. 创建代理类,实现 java.lang.reflect.InvocationHandler 接口

代码如下:

IPlayDao.java(玩的接口)

public interface IPlayDao {
    void play();
}

StudentDao.java(实现了买东西,玩的接口的学生类)

public class StudentDao implements IPlayDao {
    @Override
    public void play() {
        System.out.println("我是学生,我想出去玩");
    }
}

MyProxy.java 代理类:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class MyProxy {
    private Object target;
    public MyProxy(Object target){
        this.target=target;
    }
    public Object getProxyInstance(){
        return Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                new InvocationHandler() {
                    // 一个接口可能很多方法,要是需要针对某一个方法,那么需要在函数里判断 method
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        System.out.println("开始事务 2");
                        // 执行目标对象方法
                        Object returnValue = method.invoke(target, args);
                        System.out.println("提交事务 2");
                        return returnValue;
                    }
                }
        );
    }
}

测试类(Test.java)

public class Test {
    public static void main(String [] args){
        // 保存生成代理类的字节码文件
        System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");

        IPlayDao target =new StudentDao();
        System.out.println(target.getClass());
        IPlayDao proxy = (IPlayDao) new MyProxy(target).getProxyInstance();
        System.out.println(proxy.getClass());
        // 执行方法   【代理对象】
        proxy.play();

    }
}

由于加了这句代码,我们可以把生成的代理类的字节码文件保存下来, 其实通过输出也可以看到,两个对象不是同一个类,代理类是动态生成的:

```java
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");

源码分析

跟着源码一步步看,先从调用的地方 Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h):

进入方法里面,省略各种异常处理,主要剩下了生成代理类字节码 以及 通过构造函数反射构造新对象:

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

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

上面注释里面说查找或者生成代理对象,为什么有查找?因为并不是每一次都生成,生成的代理对象实际上会缓存起来,如果没有,才会生成,看源码 Class<?> getProxyClass0(ClassLoader loader,Class<?>... interfaces):

    private static Class<?> getProxyClass0(ClassLoader loader,
                                           Class<?>... interfaces) {
        if (interfaces.length> 65535) {
            throw new IllegalArgumentException("interface limit exceeded");
        }
        // 调用缓存代理类的 cache 来获取类加载器
        return proxyClassCache.get(loader, interfaces);
    }

如果由实现给定接口的给定加载器定义的代理类存在,这将简单地返回缓存的副本; 否则,它将通过 ProxyClassFactory 创建代理类,proxyClassCache 其实就是个 weakCache:

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

初始化的时候,proxyClassCache 指定了两个属性,一个是 KeyFactory, 另外一个是 ProxyClassFactory, 从名字就是猜到 ProxyClassFactory 是代理类工厂:

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

记住这里的 subKeyFactory,实际上就是传入的 ProxyClassFactory,那前面 proxyClassCache.get(loader, interfaces); 到底是怎么操作的?

上面调用到了 subKeyFactory.apply(key, parameter),这个 subKeyFactory 实际上是我们传的 ProxyClassFactory, 进入里面去看:

    private static final class ProxyClassFactory
        implements BiFunction<ClassLoader, Class<?>[], Class<?>>
    {
        // 生成的代理类前缀
        private static final String proxyClassNamePrefix = "$Proxy";

        // 下一个生成的代理类的名字的计数器,一般是 $Proxy0,$Proxy1
        private static final AtomicLong nextUniqueNumber = new AtomicLong();

        @Override
        public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {

            Map<Class<?>, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length);
            for (Class<?> intf : interfaces) {
                /*
                 * 检验类加载器是否能通过接口名称加载该类
                 */
                Class<?> interfaceClass = null;
                try {
                    interfaceClass = Class.forName(intf.getName(), false, loader);
                } catch (ClassNotFoundException e) {
                }
                if (interfaceClass != intf) {
                    throw new IllegalArgumentException(
                        intf + "is not visible from class loader");
                }
                /*
                 * 判断接口类型
                 */
                if (!interfaceClass.isInterface()) {
                    throw new IllegalArgumentException(
                        interfaceClass.getName() + "is not an interface");
                }
                /*
                 * 判断是否重复
                 */
                if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null) {
                    throw new IllegalArgumentException(
                        "repeated interface:" + interfaceClass.getName());
                }
            }
            // 代理包名字
            String proxyPkg = null;
            int accessFlags = Modifier.PUBLIC | Modifier.FINAL;

            /*
             * 记录非公共代理接口的包,以便在同一个包中定义代理类。确认所有非公共代理接口都在同一个包中。
             */
            for (Class<?> intf : interfaces) {
                int flags = intf.getModifiers();
                if (!Modifier.isPublic(flags)) {
                    accessFlags = Modifier.FINAL;
                    String name = intf.getName();
                    int n = name.lastIndexOf('.');
                    String pkg = ((n == -1) ? "" : name.substring(0, n + 1));
                    if (proxyPkg == null) {
                        proxyPkg = pkg;
                    } else if (!pkg.equals(proxyPkg)) {
                        throw new IllegalArgumentException(
                            "non-public interfaces from different packages");
                    }
                }
            }

            if (proxyPkg == null) {
                // 如果没有非公共代理接口,请使用 com.sun.proxy 包
                proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";
            }

            /*
             * 为要生成的代理类选择一个名称。
             */
            long num = nextUniqueNumber.getAndIncrement();
            String proxyName = proxyPkg + proxyClassNamePrefix + num;

            /*
             * 生成指定的代理类。(这里是重点!!!)
             */
            byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
                proxyName, interfaces, accessFlags);
            try {
                return defineClass0(loader, proxyName,
                                    proxyClassFile, 0, proxyClassFile.length);
            } catch (ClassFormatError e) {
                /*
                 * 这里的 ClassFormatError 意味着 (禁止代理类生成代码中的错误) 提供给代理类创建的参数有其他一些无效方面 (比如超出了虚拟机限制)。
                 */
                throw new IllegalArgumentException(e.toString());
            }
        }
    }

上面调用一个方法生成代理类,我们看看 IDEA 反编译的代码:

    public static byte[] generateProxyClass(final String var0, Class<?>[] var1, int var2) {
        ProxyGenerator var3 = new ProxyGenerator(var0, var1, var2);
        // 生成文件
        final byte[] var4 = var3.generateClassFile();
        // 判断是否要写入磁盘!!!
        if (saveGeneratedFiles) {
            // 开启高权限写入
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                public Void run() {
                    try {
                        int var1 = var0.lastIndexOf(46);
                        Path var2;
                        if (var1> 0) {
                            Path var3 = Paths.get(var0.substring(0, var1).replace('.', File.separatorChar));
                            Files.createDirectories(var3);
                            var2 = var3.resolve(var0.substring(var1 + 1, var0.length()) + ".class");
                        } else {
                            var2 = Paths.get(var0 + ".class");
                        }

                        Files.write(var2, var4, new OpenOption[0]);
                        return null;
                    } catch (IOException var4x) {
                        throw new InternalError("I/O exception saving generated file:" + var4x);
                    }
                }
            });
        }

        return var4;
    }

生成代理文件实际上和我们想的差不多,就是一些 hashCode(),toString(),equals(), 原方法,代理方法等:

这与我们之前看到的文件一致:

然后之所以我们代码要设置写入磁盘,是因为这个变量, 控制了写磁盘操作:

    private static final boolean saveGeneratedFiles = (Boolean)AccessController.doPrivileged(new GetBooleanAction("sun.misc.ProxyGenerator.saveGeneratedFiles"));

为什么只支持接口实现,不支持继承普通类?
因为代理类继承了 Proxy 类,并且实现了接口,Java 不允许多继承,所以不能代理普通类的方式,并且在静态代码块里面,用反射方式获取了所有的代理方法。

JDK 代理看起来像是个黑盒,实际上,每一句代码,都有其缘由。其实本质上也是动态的为我们的原始类,动态生成代理类。

生成的代理类里面其实对原始类进行增强(比如 play() 方法)的时候, 调用了 super.h.invok() 方法,其实这里的 h 是什么呢?

h 是父类的 h, 生成的代理类的父类是 Proxy,Proxy 的 h,就是我们传入的 InvocationHandler:

    public final void play() throws  {
        try {
            super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }

生成的代码里面通过反射调用到的其实是我们自己重写的那部分逻辑,所以就有了增强的功能,不得不说,这种设计确实巧妙:

动态代理有多香

动态代理是Java语言里面的一个很强大的特性,可以用来做一些切面,比如拦截器,登录验证等等,但是它并不是独立存在的,任何一个知识点都不能独立说明语言的强大,重要的是它们的组合。

动态代理要实现强大的功能,一般需要和反射,注解等一起合作,比如对某些请求进行拦截,拦截后做一些登录验证,或者日志功能。最重要的一点,它能够在减少耦合度的前提下实现增强。

【作者简介】:
秦怀,公众号【秦怀杂货店】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。个人写作方向:Java源码解析,JDBC,Mybatis,Spring,redis,分布式,剑指Offer,LeetCode等,认真写好每一篇文章,不喜欢标题党,不喜欢花里胡哨,大多写系列文章,不能保证我写的都完全正确,但是我保证所写的均经过实践或者查找资料。遗漏或者错误之处,还望指正。

剑指Offer全部题解PDF

2020年我写了什么?

开源编程笔记

  • 本文作者: 秦怀杂货店
  • 本文链接: http://aphysia.cn/archives/she-ji-mo-shi-32--jdk-dong-tai-dai-li-yuan-ma-fen-xi-you-duo-xiang-
  • 版权声明: 本博客所有文章除特别声明外,均采用CC BY-NC-SA 3.0 许可协议。转载请注明出处!
# 随便聊聊 # 数据结构 # 小游戏 # 数据库 # Docker # Springboot # 系统设计 # 雪花算法 # 分布式 # 海量ip # 最长回文子串 # 算法 # 面试题 # 线程池 # 多线程 # 线程 # java学习 # 布隆过滤器 # github # 架构设计 # docsify # Git # JVM # LeetCode # 杂货思考 # 设计模式 # Lambda # native # isAssignableFrom # 反射 # 剑指Offer # mybatis # SPI # JDBC # 编程工具 # Java基础 # 集合
马拉车算法,其实并不难!!!
设计模式【3.3】-- CGLIB动态代理源码解读
  • 文章目录
  • 站点概览
秦怀杂货店

秦怀杂货店

纵然缓慢,驰而不息。

145 日志
19 分类
37 标签
Github E-mail
Creative Commons
0%
© 2022 秦怀杂货店
由 Halo 强力驱动
|
主题 - NexT.Pisces v5.1.4