JDK、CGLIB动态代理使用以及源码分析

作者: JavaJVMCoder

前言介绍

在Java中动态代理是非常重要也是非常有用的一个技术点,如果没有动态代理技术几乎也就不会有各种优秀框架的出现,包括Spring。
其实在动态代理的使用中,除了我们平时用的Spring还有很多中间件和服务都用了动态代理,例如;

  1. RPC通信框架Dubbo,在通信的时候由服务端提供一个接口描述信息的Jar,调用端进行引用,之后在调用端引用后生成了对应的代理类,当执行方法调用的时候,实际需要走到代理类向服务提供端发送请求信息,直至内容回传。
  2. 另外在使用Mybatis时候可以知道只需要定义一个接口,不需要实现具体方法就可以调用到Mapper中定义的数据库操作信息了。这样极大的简化了代码的开发,又增强了效率。
  3. 最后不知道你自己是否尝试过开发一些基于代理类的框架,以此来优化业务代码。也就是将业务代码中非业务逻辑又通用性的功能抽离出来,开发为独立的组件。推荐个案例,方便知道代理类的应用:手写RPC框架第三章《RPC中间件》

代理方式

动态代理可以使用Jdk方式也可以使用CGLB,他们的区别,如下;

类型 机制 回调方式 适用场景 效率
JDK 委托机制,代理类和目标类都实现了同样的接口,InvocationHandler持有目标类,代理类委托InvocationHandler去调用目标类的原始方法 反射 目标类是接口类 效率瓶颈在反射调用稍慢
CGLIB 继承机制,代理类继承了目标类并重写了目标方法,通过回调函数MethodInterceptor调用父类方法执行原始逻辑 通过FastClass方法索引调用 非接口类,非final类,非final方法 第一次调用因为要生成多个Class对象较JDK方式慢,多次调用因为有方法索引较反射方式快,如果方法过多switch case过多其效率还需测试

案例工程

itstack-demo-test
└── src
    ├── main
    │   └── java
    │       └── org.itstack.demo
    │           ├── proxy
    │           │   └── cglib
    │           │       └── CglibProxy.java
    │           ├── jdk 
    │           │   ├── reflect
    │           │   │   ├── JDKInvocationHandler.java
    │           │   │   └── JDKProxy.java        
    │           │   └── util
    │           │       └── ClassLoaderUtils.java    
    │           └── service
    │               ├── IUserService.java
    │               └── UserService.java 
    └── test
        └── java
            └── org.itstack.demo.test
                └── ApiTest.java

基础接口和方法便于验证

service/IUserService.java

public interface IUserService {

    String queryUserNameById(String userId);

}

service/UserService.java

public class UserService implements IUserService {

    public String queryUserNameById(String userId) {
        return "hi user " + userId;
    }

}

JDK动态代理

reflect/JDKInvocationHandler.java & 代理类反射调用

  • 实现InvocationHandler.invoke,用于方法增强{监控、执行其他业务逻辑、远程调用等}
  • 如果有需要额外的参数可以提供构造方法
public class JDKInvocationHandler implements InvocationHandler {

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println(method.getName());
        return "我被JDKProxy代理了";
    }

}

reflect/JDKProxy.java & 定义一个代理类获取的服务

  • Proxy.newProxyInstance 来实际生成代理类,过程如下;
    1. Class<?> cl = getProxyClass0(loader, intfs); 查找或生成指定的代理类
    2. proxyClassCache.get(loader, interfaces); 代理类的缓存中获取
    3. subKeyFactory.apply(key, parameter) 继续下一层
    4. byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces, accessFlags); 生成代理类的字节码
public class JDKProxy {

    public static <T> T getProxy(Class<T> interfaceClass) throws Exception {
        InvocationHandler handler = new JDKInvocationHandler();
        ClassLoader classLoader = ClassLoaderUtils.getCurrentClassLoader();
        T result = (T) Proxy.newProxyInstance(classLoader, new Class[]{interfaceClass}, handler);
        return result;
    }

}

ApiTest.test_proxy_jdk() & 执行调用并输出反射类的字节码

  • 代理后调用方法验证
  • 通过使用ProxyGenerator.generateProxyClass获取实际的字节码,查看代理类的内容
@Test
public void test_proxy_jdk() throws Exception {

    IUserService proxy = (IUserService) JDKProxy.getProxy(ClassLoaderUtils.forName("org.itstack.demo.service.IUserService"));
    String userName = proxy.queryUserNameById("10001");
    System.out.println(userName);

    String name = "ProxyUserService";
    byte[] data = ProxyGenerator.generateProxyClass(name, new Class[]{IUserService.class});

    // 输出类字节码
    FileOutputStream out = null;
    try {
        out = new FileOutputStream(name + ".class");
        System.out.println((new File("")).getAbsolutePath());
        out.write(data);
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (null != out) try {
            out.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

输出结果

queryUserNameById
我被JDKProxy代理了

将生成的代理类进行反编译jd-gui

部分内容抽取,可以看到比较核心的方法,也就是我们在调用的时候走到了这里

public final String queryUserNameById(String paramString)
    throws 
{
try
{
  return (String)this.h.invoke(this, m3, new Object[] { paramString });
}
catch (Error|RuntimeException localError)
{
  throw localError;
}
catch (Throwable localThrowable)
{
  throw new UndeclaredThrowableException(localThrowable);
}
}


static
{
try
{
  m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] { Class.forName("java.lang.Object") });
  m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
  m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
  m3 = Class.forName("org.itstack.demo.service.IUserService").getMethod("queryUserNameById", new Class[] { Class.forName("java.lang.String") });
  return;
}
catch (NoSuchMethodException localNoSuchMethodException)
{
  throw new NoSuchMethodError(localNoSuchMethodException.getMessage());
}
catch (ClassNotFoundException localClassNotFoundException)
{
  throw new NoClassDefFoundError(localClassNotFoundException.getMessage());
}
}

CGLIB动态代理

cglib/CglibProxy.java

  • 提供构造方法,生成CGLIB的代理类,回调this
  • intercept可以进行方法的增强,处理相关业务逻辑
  • CGLIB是通过ASM来操作字节码生成类
public class CglibProxy implements MethodInterceptor {

    public Object newInstall(Object object) {
        return Enhancer.create(object.getClass(), this);
    }

    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("我被CglibProxy代理了");
        return methodProxy.invokeSuper(o, objects);
    }

}

ApiTest.test_proxy_cglib() & 调用代理类

@Test
public void test_proxy_cglib() {
    CglibProxy cglibProxy = new CglibProxy();
    UserService userService = (UserService) cglibProxy.newInstall(new UserService());
    String userName = userService.queryUserNameById("10001");
    System.out.println(userName);
}

输出结果

我被CglibProxy代理了
hi user 10001

综上总结

  • 在我们实际使用中两种方式都用所有使用,也可以依照不同的诉求进行选择
  • 往往动态代理会和注解共同使用,代理类拿到以后获取方法的注解,并做相应的业务操作
  • 有时候你是否会遇到增加AOP不生效,因为有时候有些类是被代理操作的,并没有执行你的自定义注解也就是切面

文章列表

更多推荐

更多
  • Azure上Linux管理-十、使用 Azure Kubernetes 服务 技术要求,开始使用 AKS,与 Helm 一起工作,使用草稿,管理 Kubernetes,问题,进一步,使用 WSL 和 VS Code,安装依赖,kubectl 安装,使用 Azure CLI 创建集群,AKS 首次部署,创建服务,多
    Apache CN

  • Azure上Linux管理-十一、故障排除和监控您的工作负载 module(load="imuxsock"),技术要求,访问您的系统,Azure 日志分析,性能监控,问题,进一步,不允许远程访问,正在端口上工作,使用 nftables,引导诊断,Linux 登录,配置日志分析服务,安装 Azure
    Apache CN

  • Azure上Linux管理-十二、附录 第一章:探索微软 Azure 云,第二章:Azure 云入门,第三章:Linux 基础管理,第 4 章:管理 Azure,第五章:高级 Linux 管理,第七章:部署虚拟机,第八章:探索连续配置自动化,第 9 章:Azure 中的容器虚
    Apache CN

  • Azure上Linux管理-九、Azure 中的容器虚拟化 cloudconfig,集装箱技术导论,系统生成,Docker,Azure 容器实例,Buildah, Podman, and Skopeo,容器和储存,问题,进一步,容器历史,chroot 环境,OpenVZ,LXC,创建一个带启动的
    Apache CN

  • Azure上Linux管理-七、部署你的虚拟机 ResourceGroup 不存在,创建它:,vnet 不存在,创建 vnet,cloudconfig,Vagrant.config 结束,部署场景,Azure 中的自动部署选项,初始配置,流浪,封隔器,自定义虚拟机和 vhd,问题,进
    Apache CN

  • Azure上Linux管理-八、探索持续配置自动化 了解配置管理,使用 Ansible,使用地球形态,使用 PowerShell DSC,Azure 策略客户端配置,其他解决方案,问题,进一步,技术要求,Ansible 的安装,SSH 配置,裸最小配置,库存文件,Ansible 剧本和模
    Apache CN

  • Azure上Linux管理-五、高级 Linux 管理 技术要求,软件管理,网络,存储,systemd,问题,进一步,RPM 软件管理器,YUM 软件管理,基于 DNF 的软件管理,DPKG 软件管理器,运用 apt 进行软件管理,ZYpp 软件管理,识别网络接口,IP 地址识别,显示路由表
    Apache CN

  • Azure上Linux管理-六、管理 Linux 安全与身份 SELINUX=可以接受以下三个值之一:,permissiveSELinux 打印警告而不是强制执行。,disabled 没有加载 SELinux 策略。,SELINUXTYPE=可以接受以下三个值之一:,targeted 目标进程
    Apache CN

  • Azure上Linux管理-四、管理 Azure 使用 Azure CLI 和 PowerShell 管理 Azure 资源,技术要求,管理存储资源,管理网络资源,管理计算资源,虚拟机资源,问题,进一步,存储帐户,托管磁盘,Azure 文件,Azure Blob,虚拟网络,子网,网络安
    Apache CN

  • Azure上Linux管理-三、Linux 基础管理 Linux Shell,获取帮助,使用文本文件,在文件系统中找到你的方式,流程管理,自由访问控制,问题,执行命令,命令行编辑,与历史一起工作,自动完成,球状,重定向,处理变量,Bash 配置文件,使用手册页,使用信息文档,其他文档,文本
    Apache CN

  • 近期文章

    更多
    文章目录

      推荐作者

      更多