JDK动态代理为什么不能代理类--详解动态代理

java进阶系列-CLassLoader详解
java进阶系列-反射详解
java进阶系列-动态代理

动态代理的应用十分广泛,很多有名的框架都用到了动态代理,比如spring aop,mybatis,Hibernate,rpc等等,甚至我们日常开发中一些非功能性需求--监控、 统计、鉴权、限流、事务、幂等、日志--也是基于动态代理实现的。由此可见,掌握动态代理对我们的开发工作或阅读框架源码是非常有帮助的。

本文主要介绍Java中两种常见的动态代理方式:JDK动态代理CGLIB动态代理

Java动态代理与java反射关系紧密,若读者对Java反射机制有些疑问,可参考上一篇文章《java进阶系列-反射详解》。

本文概要:

  1. 简单介绍代理模式
  2. 比较静态代理与动态代理
  3. JDK动态代理
  4. CGLIB动态代理

引入需求

UserServiceImpl代码如下:

public class UserServiceImpl implements UserService {

    @Override
    public void login(String username, String password) {
        System.out.println("欢迎" + username + "登录!");
    }

}

可以看到UserServiceImpl实现了UserService,有一个login方法。

请你设想一下,现在我们面临这样一个新需求:收集接口请求的原始数据,比如记录方法的访问时间,及处理时长。

首先分析一下需求,很明显这样的需求与业务代码本身并没有什么关系,任何业务代码都可能面临这种需求,所以直接在login方法中添加代码的方法,破坏了业务类的单一原则,也增加了代码的冗余程度。

为了将框架代码和业务代码解耦代理模式就派上用场了。

代理模式

代理模式(Proxy Design Pattern):在不改变原始类 (或叫被代理类)代码的情况下,通过引入代理类来给原始类附加功能。
如果根据代理类字节码的创建时机来分类,可以分为静态代理动态代理

  • 静态:在程序运行前就已经存在代理类的字节码文件,代理类和原始类的关系在运行前就确定了。
  • 动态:代理类源码是在程序运行期间由JVM根据反射等机制动态的生成,所以在运行前并不存在代理类的字节码文件

静态代理

我们首先实现静态代理,添加代理类UserServiceProxy,同样实现UserService接口,代理类 UserServiceProxy 负责在业务代码执行前后附加其他逻辑代码,并通过委托的方式调用原始类来执行业务代码。具体的代码实现如下所示:

public class UserServiceProxy implements UserService {
    private UserServiceImpl userService;

    public UserServiceProxy(UserServiceImpl userService) {
        this.userService = userService;
    }

    @Override
    public void login(String username, String password) {
        long startTimestamp = System.currentTimeMillis();

        userService.login(username, password);

        long endTimeStamp = System.currentTimeMillis();

        long responseTime = endTimeStamp - startTimestamp;

        System.out.printf("method:%s, startTime:%s, responseTime:%s", "login", startTimestamp, responseTime);
    }

}

下面调用一下试试:

public class DynamicProxyDemo {
    public static void main(String[] args) {
        staticProxy();
    }

    private static void staticProxy() {
        UserServiceImpl userService = new UserServiceImpl();
        UserServiceProxy userServiceProxy = new UserServiceProxy(userService);
        userServiceProxy.login("rex", "123");
    }
}

执行结果如下:

欢迎rex登录!
method:login, startTime:1605756985465, responseTime:1

虽然静态代理实现简单,且不侵入原代码,但缺点也是很明显的,试想一下,随着我们业务系统的逐渐发展,代码越来越多,难道我们要为每个原始类都添加一个代理类吗?

优缺点

  • 优点:代码结构简单,较容易实现
  • 缺点:无法适配所有代理场景,如果有新的需求,需要修改代理类,「不符合软件工程的开闭原则」

我们可以使用动态代理(Dynamic Proxy)来解决这个问题。

动态代理

JDK动态代理,有两个关键类:java.lang.reflect.InvocationHandlerjava.lang.reflect.Proxy
具体的代码如下所示。

package dynamicproxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class DynamicProxyHandler implements InvocationHandler {
    // 原始类
    private Object proxied;

    public DynamicProxyHandler(Object proxied) {
        this.proxied = proxied;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        long startTimestamp = System.currentTimeMillis();

        Object obj = method.invoke(proxied, args);

        long endTimeStamp = System.currentTimeMillis();

        long responseTime = endTimeStamp - startTimestamp;

        System.out.printf("method:%s, startTime:%s, responseTime:%s", method.getName(), startTimestamp, responseTime);

        return obj;
    }
}

invoke() 方法有3个参数:

  • Object proxy:代理对象
  • Method method:真正执行的方法
  • Object[] agrs:调用第二个参数 method 时传入的参数列表值

生成代理对象需要用到Proxy类,它可以帮助我们生成任意一个代理对象,里面提供一个静态方法newProxyInstance。

Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h);

实例化代理对象时,需要传入3个参数:

  • ClassLoader loader:加载动态代理类的类加载器
  • Class<?>[] interfaces:代理类实现的接口,可以传入多个接口
  • InvocationHandler h:指定代理类的「调用处理程序」,即调用接口中的方法时,会找到该代理工厂h,执行invoke()方法
public class DynamicProxyDemo {
    public static void main(String[] args) {
        dynamicProxy();
    }
    
    private static void dynamicProxy() {
        // 设置变量可以保存动态代理类,默认名称以 $Proxy0 格式命名
        System.setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");

        UserServiceImpl userServiceImpl = new UserServiceImpl();

        UserService proxyInstance = (UserService) Proxy.newProxyInstance(UserServiceImpl.class.getClassLoader(),
                userServiceImpl.getClass().getInterfaces(), new DynamicProxyHandler(userServiceImpl));

        proxyInstance.login("jack", "456");
    }
}

执行结果如下:

欢迎jack登录!
method:login, startTime:1605759941043, responseTime:0

以上就是我们实现的动态代理了,相比静态代理,我们不需要重复实现代理类,jdk会帮助我们动态的实现代理类。
看下上述代码的调用栈信息:

在这里插入图片描述

其中,Proxy0 就是自动生成的代理类,如果我们想看下自动生成proxy class到底长什么样该如何办呢?可以通过参数-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true(或者执行System.setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");)来保存成对应的class文件。Proxy0 代码:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.sun.proxy;

import dynamicproxy.UserService;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;

public final class $Proxy0 extends Proxy implements UserService {
    private static Method m1;
    private static Method m2;
    private static Method m3;
    private static Method m0;

    public $Proxy0(InvocationHandler var1) throws  {
        super(var1);
    }

    public final boolean equals(Object var1) throws  {
        try {
            return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

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

    public final void login(String var1, String var2) throws  {
        try {
            super.h.invoke(this, m3, new Object[]{var1, var2});
        } catch (RuntimeException | Error var4) {
            throw var4;
        } catch (Throwable var5) {
            throw new UndeclaredThrowableException(var5);
        }
    }

    public final int hashCode() throws  {
        try {
            return (Integer)super.h.invoke(this, m0, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m3 = Class.forName("dynamicproxy.UserService").getMethod("login", Class.forName("java.lang.String"), Class.forName("java.lang.String"));
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

从 $Proxy0 的代码中我们可以发现:

  • $Proxy0 继承了 Proxy 类(这也是JDK动态代理为什么不能代理类的直接原因),并且实现了被代理的所有接口,以及equals、hashCode、toString等方法
  • $Proxy0 继承了 Proxy 类, 每个代理类都有一个参数为 InvocationHandler的构造方法
  • 类和所有方法都被 public final 修饰,所以代理类只可被使用,不可以再被继承
  • 每个方法都有一个 Method 对象来描述,Method 对象在static静态代码块中创建,以 m + 数字 的格式命名
  • 调用方法的时候通过 super.h.invoke(this, m1, (Object[])null); 调用,其中的 super.h.invoke 实际上是在创建代理的时候传递给 Proxy.newProxyInstance 的 DynamicProxyHandler 对象,它继承 InvocationHandler 类,负责实际的调用处理逻辑

而 DynamicProxyHandler 的 invoke 方法接收到 method、args 等参数后,进行一些处理,然后通过反射让被代理的对象 proxied 执行方法。

Proxy源码

下面开始分析JDK动态代理生成proxy class流程源码,Proxy.newProxyInstance() 是生成动态代理对象的关键,我们可来看看它里面到底干了些什么,以下代码分析只贴出主要流程代码。

 /** parameter types of a proxy class constructor */
private static final Class<?>[] constructorParams =
        { InvocationHandler.class };
public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h)
        throws IllegalArgumentException
    {
        // 省略检验部分代码

        /*
         * 这里是为了得到动态生成的代理类,具体过程稍后分析
         * Look up or generate the designated proxy class.
         */
        Class<?> cl = getProxyClass0(loader, intfs);

        /*
         * Invoke its constructor with the designated invocation handler.
         */
        try {
            /*
             * 因为动态生成的代理类都继承了Proxy,所以都有一个参数为InvocationHandler的构造器
             * 通过这个构造器生成代理类的实例
             */
            final Constructor<?> cons = cl.getConstructor(constructorParams);
            return cons.newInstance(new Object[]{h});
        } 
        // 省略catch部分代码
    }

可以看到代理类是getProxyClass0方法获取的:

    /**
     * Generate a proxy class.  Must call the checkProxyAccess method
     * to perform permission checks before calling this.
     */
    private static Class<?> getProxyClass0(ClassLoader loader,
                                           Class<?>... interfaces) {
        if (interfaces.length > 65535) {
            throw new IllegalArgumentException("interface limit exceeded");
        }

        // If the proxy class defined by the given loader implementing
        // the given interfaces exists, this will simply return the cached copy;
        // otherwise, it will create the proxy class via the ProxyClassFactory
        return proxyClassCache.get(loader, interfaces);
    }

发现里面用到一个缓存 「proxyClassCache」,从结构来看类似于是一个 map 结构,根据类加载器loader和真实对象实现的接口interfaces查找是否有对应的 Class 对象,我们接着往下看 get() 方法。

public V get(K key, P parameter) {
    ...
    Object subKey = Objects.requireNonNull(subKeyFactory.apply(key, parameter));
    ...
}

在 get() 方法中,如果没有从缓存中获取到 Class 对象,则需要利用 「subKeyFactory」 去实例化一个动态代理对象,subKeyFactory 在这里是「Proxy」 类中「ProxyClassFactory」 内部类,由它来创建一个动态代理类,所以我们接着去看 ProxyClassFactory 中的 apply() 方法。

public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {
     //自动生成一个proxy class,序号+1
    long num = nextUniqueNumber.getAndIncrement();
    // 动态代理对象名拼接!包名 + "$Proxy" + 数字
    String proxyName = proxyPkg + proxyClassNamePrefix + num;
    // 生成字节码文件,返回一个字节数组
    byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
        proxyName, interfaces, accessFlags);
    try {
    // 利用字节码文件创建该字节码的 Class 类对象
        return defineClass0(loader, proxyName,
                    proxyClassFile, 0, proxyClassFile.length);
    } catch (ClassFormatError e) {
        throw new IllegalArgumentException(e.toString());
    }
}

apply() 方法中注意有两个非常重要的方法:

  • ProxyGenerator.generateProxyClass():它是生成字节码文件的方法,它返回了一个字节数组,字节码文件本质上就是一个字节数组,所以 proxyClassFile数组就是一个字节码文件
  • defineClass0():生成字节码文件的 Class 对象,它是一个 native 本地方法,调用操作系统底层的方法创建类对象

CGLIB动态代理

CGLIB(Code generation Library) 不是 JDK 自带的动态代理,它需要导入第三方依赖,它是一个字节码生成类库,能够在运行时动态生成代理类。

与 JDK 动态代理不同的是,CGLIB不仅能够为 Java接口 做代理,而且能够为普通的 Java类 做代理。

添加maven依赖:

<dependency>
  <groupId>cglib</groupId>
  <artifactId>cglib-nodep</artifactId>
  <version>3.3.0</version>
</dependency>

CGLIB 代理,有两个核心的类:MethodInterceptor接口和Enhancer类,MethodInterceptor类似于上述的InvocationHandler,Enhancer类似于Proxy。
下面是具体实现:

package dynamicproxy;

import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class DynamicInterceptor implements MethodInterceptor {

    /**
     * @param o 表示原始类
     * @param method 表示被拦截的方法
     * @param objects 数组表示参数列表,基本数据类型需要传入其包装类型,如int-->Integer、long-Long、double-->Double
     * @param methodProxy 表示对方法的代理,invokeSuper方法表示对原始对象方法的调用
     * @return 执行结果
     * @throws Throwable
     */
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        long startTimestamp = System.currentTimeMillis();

        // 注意这里是调用 invokeSuper 而不是 invoke,否则死循环,
        // methodProxy.invokeSuper执行的是原始类的方法,
        // method.invoke执行的是子类的方法
        Object result = methodProxy.invokeSuper(o, objects);

        long endTimeStamp = System.currentTimeMillis();

        long responseTime = endTimeStamp - startTimestamp;

        System.out.printf("method:%s, startTime:%s, responseTime:%s", method.getName(), startTimestamp, responseTime);

        return result;
    }

}

在演示Enhancer前,我们先添加UserServiceWithoutImpl(不再实现UserService接口):

package dynamicproxy;

public class UserServiceWithoutImpl {

    public void login(String username, String password) {
        System.out.println("欢迎" + username + "登录!");
    }

}

测试

public class DynamicProxyDemo {
    public static void main(String[] args) {
        cglibProxy();
    }
    private static void cglibProxy() {
        DynamicInterceptor interceptor = new DynamicInterceptor();

        Enhancer enhancer = new Enhancer();
        // 设置超类,cglib是通过继承来实现的
        enhancer.setSuperclass(UserServiceWithoutImpl.class);  
        enhancer.setCallback(interceptor);

        UserServiceWithoutImpl userService = (UserServiceWithoutImpl)enhancer.create();
        userService.login("mary", "789");
    }
}

结果:

欢迎mary登录!
method:login, startTime:1605775483020, responseTime:10

CGLIB 创建动态代理类的模式是:

  • 查找目标类上的所有非final 的public类型的方法定义;
  • 将这些方法的定义转换成字节码;
  • 将组成的字节码转换成相应的代理的class对象;
  • 实现 MethodInterceptor接口,用来处理对代理类上所有方法的请求

JDK 动态代理 和 CGLIB动态代理 的对比

JDK Proxy CGLIB
代理工厂实现接口 InvocationHandler MethodInterceptor
动态生成代理类 Proxy Enhancer

JDK动态代理:基于Java反射机制实现,必须要实现了接口的业务类才能用这种办法生成代理对象。
cglib动态代理:基于ASM机制实现,通过生成业务类的子类作为代理类。

JDK Proxy 的优势:

  • 最小化依赖关系,减少依赖意味着简化开发和维护,JDK 本身的支持,可能比 cglib 更加可靠。
  • 平滑进行 JDK 版本升级,而字节码类库通常需要进行更新以保证在新版 Java 上能够使用。
  • 代码实现简单。

基于 cglib 框架的优势:

  • 无需实现接口,达到代理类无侵入
  • 只操作我们关心的类,而不必为其他相关类增加工作量。
  • 高性能

以上代码已上传至:github

参考:
给女同事讲完代理后,女同事说:你好棒哦
Java 动态代理详解

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 197,814评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,124评论 2 375
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 144,814评论 0 327
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,924评论 1 268
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,815评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,562评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,944评论 3 388
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,582评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,859评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,881评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,700评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,493评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,943评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,115评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,413评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,978评论 2 343
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,182评论 2 339

推荐阅读更多精彩内容