Skip to content

2019 12 21 [有点干货]JDK、CGLIB动态代理使用以及源码分析

fuzhengwei edited this page May 16, 2020 · 1 revision

作者:小傅哥
博客:https://bugstack.cn - 原创系列专题

沉淀、分享、成长,让自己和他人都能有所收获!

前言介绍

在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不生效,因为有时候有些类是被代理操作的,并没有执行你的自定义注解也就是切面

📝 首页

🌏 知识星球码农会锁

实战项目:「DDD+RPC分布式抽奖系统」、专属小册、问题解答、简历指导、架构图稿、视频课程

🐲 头条

⛳ 目录

  1. 源码 - :octocat: 公众号:bugstack虫洞栈 文章所涉及到的全部开源代码
  2. Java
  3. Spring
  4. 面向对象
  5. 中间件
  6. Netty 4.x
  7. 字节码编程
  8. 💯实战项目
  9. 部署 Dev-Ops
  10. 📚PDF 下载
  11. 关于

💋 精选

🐾 友链

建立本开源项目的初衷是基于个人学习与工作中对 Java 相关技术栈的总结记录,在这里也希望能帮助一些在学习 Java 过程中遇到问题的小伙伴,如果您需要转载本仓库的一些文章到自己的博客,请按照以下格式注明出处,谢谢合作。

作者小傅哥
链接https://bugstack.cn
来源bugstack虫洞栈

2021年10月24日,小傅哥 的文章全部开源到代码库 CodeGuide 中,与同好同行,一起进步,共同维护。

这里我提供 3 种方式:

  1. 提出 Issue :在 Issue 中指出你觉得需要改进/完善的地方(能够独立解决的话,可以在提出 Issue 后再提交 PR )。
  2. 处理 Issue : 帮忙处理一些待处理的 Issue
  3. 提交 PR: 对于错别字/笔误这类问题可以直接提交PR,无需提交Issue 确认。

详细参考:CodeGuide 贡献指南 - 非常感谢你的支持,这里会留下你的足迹

  • 加群交流 本群的宗旨是给大家提供一个良好的技术学习交流平台,所以杜绝一切广告!由于微信群人满 100 之后无法加入,请扫描下方二维码先添加作者 “小傅哥” 微信(fustack),备注:加群。
微信:fustack

  • 公众号(bugstack虫洞栈) - 沉淀、分享、成长,专注于原创专题案例,以最易学习编程的方式分享知识,让自己和他人都能有所收获。
公众号:bugstack虫洞栈

感谢以下人员对本仓库做出的贡献或者对小傅哥的赞赏,当然不仅仅只有这些贡献者,这里就不一一列举了。如果你希望被添加到这个名单中,并且提交过 Issue 或者 PR,请与我联系。

Clone this wiki locally