动态代理(二)—— CGLIB代理原理
前篇文章动态代理(一)——JDK中的动态代理中详细介绍了JDK动态代理的Demo实现,api介绍,原理详解。这篇文章继续讨论Java中的动态代理,并提及了Java中动态代理的几种实现方式。这里继续介绍CGLIB代理方式。
CGLIB动态代理在AOP、RPC中都有所使用,是Java体系中至关重要的一块内容。本篇文章的主要目标:
- 掌握使用CGLIB生成代理类
- 深入理解CGLIB的代理原理
从以上目标出发,本篇文章主要从以下几个方面逐步深入探索CGLIB:
- CGLIB的使用Demo
- CGLIB重要API介绍
- CGLIB代理原理
- 总结
一.CGLIB的使用Demo
使用CGLIB的大致分为四步骤:
- 创建被代理对象
- 创建方法拦截器
- 创建代理对象
- 调用代理对象
1.创建被代理对象
public class EchoServiceImpl implements EchoService {
public void echo(String message) {
System.out.println(message);
}
public void print(String message) {
System.out.println(message);
}
public int test() {
return 1;
}
public final void finalTest() {
System.out.println("I am final method.");
}
}
2.创建方法拦截器
public class EchoServiceInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("before invoking!");
Object object = methodProxy.invokeSuper(o, objects);
System.out.println("after invoking!");
return object;
}
}
接口MethodInterceptor是CGLIB库提供,用于应用开发者根据自己的业务逻辑进行扩展实现。
3.创建代理对象
//Enhancer是生成代理类的工厂
Enhancer enhancer = new Enhancer();
//设置代理的超类,即被代理对象
enhancer.setSuperclass(EchoServiceImpl.class);
//设置拦截方法
enhancer.setCallback(new EchoServiceInterceptor());
//生成代理对象
EchoService echoService = (EchoService) enhancer.create();
4.调用代理对象
echoService.echo("test");
执行结果:
before invoking!
test
after invoking!
二.CGLIB重要API介绍
CGLIB库的体积很小,但是学习难度确非常高,毕竟涉及到bytecode。所以该篇文章后续关于原理介绍,只涉及代理原理方面,关于如何生成代理对象的方面,由于个人能力所及,不敢妄加说明。
下面是CGLIB的包结构,每个包都是负责一个模块功能,定义非常明确,负责单一的功能职责:
net.sf.cglib.core
低级别的字节码操作的类,它们直接与ASM相关net.sf.cglib.transform
在运行或者构建时转换类文件的一些类net.sf.cglib.proxy
创建代理和方法拦截器定义的类net.sf.cglib.reflect
实现快速反射的一些基础类net.sf.cglib.util
用于集合排序的一些工具类net.sf.cglib.beans
与JavaBean相关的类
虽然cglib包含了如此多的功能模块,但是对于使用者,我们并不需要关注如此多的细节,只需要掌握几个重要的接口:
在看完上面的Demo,应该对Enhancer有一定了解。Enhancer字面义即增强,也正如其表述,Enhancer就是用来创建代理对象的接口。其中create方法可以生成代理对象,实际就是工厂模式。
在生成对象前,需要做关于代理方面的配置:
- 配置被代理对象(目标),即setSuperClass设置超类型,该superClass即Enhancer中持有的Class对象;
- 配置统一拦截方法(中间人),即setCallBack设置回调接口,对应上图的CallBack。AOP的实现使用methodInterpretor型CallBack;
- 可选性的配置拦截过滤器(核验流程),即setCallBackFilter,对应上图的CallBackFiler;
Enhancer的create api提供了生成代理对象的。以上即在编写cglib动态代理过程中使用的几个重要api。虽然字节码技术是非常晦涩深奥,但是cglib以简单易用的api使字节码增强技术变得非常容易上手。
通过以上的demo示例和几个重要api的介绍应该都能掌握使用cglib库生成代理类。下面依然通过反编译的方式继续深入cglib的动态代理的调用原理。
三.CGLIB代理原理
1.整体架构与调用过程概览
在详细查看被代理对象的原理之前,先了解下cglib的整体架构图:
从图中可以看出,cglib在字节码层面的操作技术主要依赖ASM提供的能力。在上节中提到的net.sf.cglib.core包,正是与ASM相关。CGLIB上层直接面向应用层,将深奥晦涩的字节码技术包装成应用易用能理解的api,为aop,dynaminc proxy等技术提供了实现基础。
在反编译看代理对象的源代码之前,先看下代理调用的过程图:
从图中可以看出:
- 客户端调用代理对象的被代理方法
- 代理对象将调用委派给方法拦截器统一接口intercept
- 方法拦截器中执行前置操作,然后调用方法代理的统一接口invokeSuper
- 方法代理的invokeSuper初始化代理对象的和被代理对象的fastClass
- 初始化后,再调用代理对象的fastClass
- 代理对象的fastClass能够fast的调用代理的代理对象
- 代理对象再调用被代理对象的被代理方法
- 调用栈弹出,到intercept中再执行后置操作,方法调用结束
通过以上的过程再来看下它们之间的UML:
在Enhancer中的配置的被代理对象、统一回调的最终都被聚合到生成的代理对象中。(工厂模式,零件组装成产品)
代理对象聚合同一方法回调、继承被代理对象,聚合方法代理的。
2.反编译代理类字节码
CGLIB提供生成代理类的class文件的配置项。在CGLIB中提供了DebuggingClassWriter类用于将字节码的byte字节写入class文件中。
public byte[] toByteArray() {
return (byte[]) java.security.AccessController.doPrivileged(
new java.security.PrivilegedAction() {
public Object run() {
// 获取代理类的字节内容
byte[] b = ((ClassWriter) DebuggingClassWriter.super.cv).toByteArray();
if (debugLocation != null) {
// 转换生成的class文件路径分隔符
String dirs = className.replace('.', File.separatorChar);
try {
// 创建class文件
new File(debugLocation + File.separatorChar + dirs).getParentFile().mkdirs();
File file = new File(new File(debugLocation), dirs + ".class");
OutputStream out = new BufferedOutputStream(new FileOutputStream(file));
try {
// 将字节内容写入class文件
out.write(b);
} finally {
out.close();
}
if (traceCtor != null) {
file = new File(new File(debugLocation), dirs + ".asm");
out = new BufferedOutputStream(new FileOutputStream(file));
try {
ClassReader cr = new ClassReader(b);
PrintWriter pw = new PrintWriter(new OutputStreamWriter(out));
ClassVisitor tcv = (ClassVisitor)traceCtor.newInstance(new Object[]{null, pw});
cr.accept(tcv, 0);
pw.flush();
} finally {
out.close();
}
}
} catch (Exception e) {
throw new CodeGenerationException(e);
}
}
return b;
}
});
}
从以上可以看出只要配置文件的生成路径变量debugLocation即可,再来看下该变量初始化赋值情况
static {
// 从System中取出属性DEBUG_LOCATION_PROPERTY赋值给文件class文件生成路径变量
debugLocation = System.getProperty(DEBUG_LOCATION_PROPERTY);
if (debugLocation != null) {
System.err.println("CGLIB debugging enabled, writing to '" + debugLocation + "'");
try {
Class clazz = Class.forName("org.objectweb.asm.util.TraceClassVisitor");
traceCtor = clazz.getConstructor(new Class[]{ClassVisitor.class, PrintWriter.class});
} catch (Throwable ignore) {
}
}
}
可以看出只要应用启动时
System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY,
"/Users/lixinyou/Documents/code-space/java/java-base/java-proxy/target/proxy/impl");
设置DebuggingClassWriter.DEBUG_LOCATION_PROPERTY属性,在运行时CGLIB便会生成代理类的class。
这种方式与JDK的动态代理中的class文件生成方式一致。这种用法,在日常应用开发中也可借鉴,利用应用启动参数的不同,可以在运行时改变行为,代码具有强扩展性。
3.fastClass机制
在看代理机制源码之前,做好一切准备。再来了解下CGLIB中关于实现快速调用的fastClass机制。
在JDK的动态代理中使用反射调用目标对象,在CGLIB中为了更好的提升性能,采用fastClass机制。
FastClass机制:将类的方法信息解析出来,然后为其建立索引。调用的时候,只要传索引,就能找到相应的方法进行调用。
- 为所有的方法建立索引
- 调用前先根据方法信息寻找到索引
- 调用时根据索匹配相应的方法进行直接调用
CGLIB在字节码层面将方法和索引的对应关系建立,避免了反射调用:
public int getIndex(Signature var1) {
String var10000 = var1.toString();
switch(var10000.hashCode()) {
case -2055565910:
if (var10000.equals("CGLIB$SET_THREAD_CALLBACKS([Lnet/sf/cglib/proxy/Callback;)V")) {
return 11;
}
break;
case -1980342926:
if (var10000.equals("print(Ljava/lang/String;)V")) {
return 6;
}
break;
case -1860420502:
if (var10000.equals("CGLIB$clone$7()Ljava/lang/Object;")) {
return 24;
}
break;
case -1725733088:
if (var10000.equals("getClass()Ljava/lang/Class;")) {
return 29;
}
break;
}
return -1;
}
上述获取方法的索引,下述代码再根据索引进行调用:
public Object invoke(int index, Object var2, Object[] var3) throws InvocationTargetException {
e77dd5ce var10000 = (e77dd5ce)var2;
try {
switch(index) {
case 0:
return new Boolean(var10000.equals(var3[0]));
case 1:
return var10000.toString();
} catch (Throwable var4) {
throw new InvocationTargetException(var4);
}
throw new IllegalArgumentException("Cannot find matching method/constructor");
}
4.运行时的代理类
CGLIB运行时,实际会生成三个class:
- 代理类
- 代理类对应的fastClass
- 被代理类的fastClass
如上述Demo中生成的代理类和相应的代理类:
- EchoServiceImpl$$EnhancerByCGLIB$$e77dd5ce$$FastClassByCGLIB$$1b37f797.class
- EchoServiceImpl$$EnhancerByCGLIB$$e77dd5ce.class
- EchoServiceImpl$$FastClassByCGLIB$$44f86581.class
下面就看下生成的类的代码片段,理解下CGLIB的运行时代理原理。
首先看下生成的代理类
public class EchoServiceImpl$$EnhancerByCGLIB$$e77dd5ce extends EchoServiceImpl implements Factory {
代理类是继承自被代理类,这里与JDK的不同是,JDK是实现了接口。
下面的即是对echo方法的代理方法:
public final void echo(String var1) {
// 获取方法拦截器
MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
if (this.CGLIB$CALLBACK_0 == null) {
CGLIB$BIND_CALLBACKS(this);
var10000 = this.CGLIB$CALLBACK_0;
}
if (var10000 != null) { // 如果不为空,则调用其统一拦截
var10000.intercept(this, CGLIB$echo$2$Method, new Object[]{var1}, CGLIB$echo$2$Proxy);
} else { // 如果为空,则直接调用父类即被代理类的方法
super.echo(var1);
}
}
最为让人关注的是intercept方法调用时的参数MethodProxy:CGLIB$echo$2$Proxy
static {
CGLIB$STATICHOOK1();
}
static void CGLIB$STATICHOOK1() {
CGLIB$echo$2$Proxy = MethodProxy.create(var1, var0, "(Ljava/lang/String;)V", "echo", "CGLIB$echo$2");
}
在代理类被加载时,执行静态方法CGLIB$STATICHOOK1(),创建了echo方法对应的方法代理。
public static MethodProxy create(Class c1, Class c2, String desc, String name1, String name2) {
MethodProxy proxy = new MethodProxy();
proxy.sig1 = new Signature(name1, desc);
proxy.sig2 = new Signature(name2, desc);
proxy.createInfo = new CreateInfo(c1, c2);
return proxy;
}
这里使用了工厂模式,在创建MethodProxy时,为期成员CreateInfo赋值。c1代表被代理类,c2代表代理类。desc代理方法和被代理方法的参数信息,name1是被代理方法名,name2是代理方法名。
下面再看来下intercepte方法中调用的methodProxy的invokeSuper方法:
public Object invokeSuper(Object obj, Object[] args) throws Throwable {
try {
// 先进行初始化
init();
// 获取fastClassInfo对象
FastClassInfo fci = fastClassInfo;
// 获取代理类对应的fastClass对象并按索引调用
return fci.f2.invoke(fci.i2, obj, args);
} catch (InvocationTargetException e) {
throw e.getTargetException();
}
}
先看下初始化中所执行的逻辑:
private void init()
{
/*
* Using a volatile invariant allows us to initialize the FastClass and
* method index pairs atomically.
*
* Double-checked locking is safe with volatile in Java 5. Before 1.5 this
* code could allow fastClassInfo to be instantiated more than once, which
* appears to be benign.
*/
if (fastClassInfo == null)
{
synchronized (initLock)
{
if (fastClassInfo == null)
{
CreateInfo ci = createInfo;
FastClassInfo fci = new FastClassInfo();
// 获取被代理类
fci.f1 = helper(ci, ci.c1);
// 获取代理类
fci.f2 = helper(ci, ci.c2);
// 获取被代理类的被代理方法的索引
fci.i1 = fci.f1.getIndex(sig1);
// 获取代理类的代理方法的索引
fci.i2 = fci.f2.getIndex(sig2);
fastClassInfo = fci;
createInfo = null;
}
}
}
}
这里使用了单例模式,fastClassInfo对象是单例。所以初始化方法只会在第一次调用代理方法的时候,才响应的进行对其初始化。
初始化后,就将代理类和代理方法的索引获取到了,然后再按照索引直接对代理方法进行调用:
public Object invoke(int var1, Object var2, Object[] var3) throws InvocationTargetException {
e77dd5ce var10000 = (e77dd5ce)var2;
int var10001 = var1;
try {
switch(var10001) {
case 19:
var10000.CGLIB$echo$2((String)var3[0]);
return null;
}
} catch (Throwable var4) {
throw new InvocationTargetException(var4);
}
throw new IllegalArgumentException("Cannot find matching method/constructor");
}
上述考虑到篇幅和简洁的原因,这里只摘取了case:19的代码片段。
通过索引直接调用代理类代理方法:CGLIB$echo$2:
final void CGLIB$echo$2(String var1) {
super.echo(var1);
}
在该方法中再调用被代理类(继承了被代理类)的被代理方法。
至此,CGLIB的代理调用原理就是以上的内容。
四.总结
CGLIB是一种字节码增强库,利用其提供的字节码技术可以实现动态代理。其底层依赖ASM字节码技术。
CGLIB的动态代理与JDK动态代理的不同点:
- JDK动态代理必须需要接口,JDK代理是基于接口进行动态代理。CGLIB中既支持对接口的代理,也支持对对象的代理。
- CGLIB动态代理使用fastClass机制实现快速调用被代理类,JDK中使用了反射方式调用被代理。所以CGLIB的动态代理的方式性能上更有优势。
- CGLIB额外对来源于Object中的finalize和clone方法也做了拦截代理,JDK只为了equals、hashCode、toString进行代理
注:JDK中生成的代理类已经静态解析了方法对象作为代理类的静态变量,类似做缓存,从而部分解决反射的性能问题。
CGLIB的动态代理与JDK动态代理的相同点:
- 都具有统一接口,JDK动态代理中中间统一接口是InvocationHandler,CGLIB中是MethodInteceptor。
- 生成的代理类和其中的方法都是final
参考
neoremind/dynamic-proxy
Are there alternatives to cglib
Spring AOP 实现原理与 CGLIB 应用
深入浅出CGlib-打造无入侵的类代理
CGLib: The Missing Manual
Create Proxies Dynamically Using CGLIB Library
动态代理(二)—— CGLIB代理原理的更多相关文章
- 设计模式---JDK动态代理和CGLIB代理
Cglig代理设计模式 /*测试类*/ package cglibProxy; import org.junit.Test; public class TestCglib { @Test public ...
- (转)Java动态代理与CGLib代理
<br>public class UserDAOImpl{ <br><br> public void save() { <br> / ...
- IT忍者神龟之Java动态代理与CGLib代理
<br>public class UserDAOImpl{ <br><br> public void save() { <br> / ...
- SpringAOP-JDK 动态代理和 CGLIB 代理
在 Spring 中 AOP 代理使用 JDK 动态代理和 CGLIB 代理来实现,默认如果目标对象是接口,则使用 JDK 动态代理,否则使用 CGLIB 来生成代理类. 1.JDK 动态代理 那么接 ...
- Spring中AOP的两种代理方式(Java动态代理和CGLIB代理)
第一种代理即Java的动态代理方式上一篇已经分析,在这里不再介绍,现在我们先来了解下GCLIB代理是什么?它又是怎样实现的?和Java动态代理有什么区别? cglib(Code Generation ...
- JDK动态代理和 CGLIB 代理
JDK动态代理和 CGLIB 代理 JDK动态代理:其代理对象必须是某个接口的实现,它是通过在运行期期间创建一个接口的实现类来完成对目标对象的代理. 代码示例 接口 public interface ...
- java代理(静态代理和jdk动态代理以及cglib代理)
版权声明:本文为Fighter168原创文章,未经允许不得转载. 目录(?)[+] 说到代理,脑袋中浮现一大堆代理相关的名词,代理模式,静态代理,jdk代理,cglib代理等等. 记忆特别深刻 ...
- 静态代理、动态代理和cglib代理
转:https://www.cnblogs.com/cenyu/p/6289209.html 代理(Proxy)是一种设计模式,提供了对目标对象另外的访问方式;即通过代理对象访问目标对象.这样做的好处 ...
- 关于JDK动态代理与Cglib代理
关于JDK动态代理与Cglib代理 最近有时间学习一下SpringAOP源码,底层用到了代理,大概是这样的: 当需要被代理的类实现了接口,则使用JDK动态代理创建代理对象,增加增强操作执行目标方法 当 ...
- Java动态代理和CGLib代理
本文参考 在上一篇"Netty + Spring + ZooKeeper搭建轻量级RPC框架"文章中涉及到了Java动态代理和CGLib代理,在这篇文章中对这两种代理方式做详解 下 ...
随机推荐
- JVM参数最佳实践:元空间的初始大小和最大大小
本文阅读时间大约4分钟. JVM加载类的时候,需要记录类的元数据,这些数据会保存在一个单独的内存区域内,在Java 7里,这个空间被称为永久代(Permgen),在Java 8里,使用元空间(Meta ...
- 剑指:包含min函数的栈(min栈)
题目描述 设计一个支持 push,pop,top 等操作并且可以在 O(1) 时间内检索出最小元素的堆栈. push(x)–将元素x插入栈中 pop()–移除栈顶元素 top()–得到栈顶元素 get ...
- using 中写 return 一样会释放using 中对象 但是会在外面定义一个一样的对象 赋值后 释放 最后 return 外面定义的那个对象
static DataTable getDataTable() { ")) { SqlCommand com = new SqlCommand("", con); Sql ...
- C# Net 计算周(可正推和逆推)
C# Net 计算周(可正推和逆推) 拷贝代码(方法): /// <summary> /// 计算周 /// </summary> /// <param name=&qu ...
- HDU 1372 Knight Moves 题解
Knight Moves Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 65536/32768 K (Java/Others)Tota ...
- C#8.0接口默认实现特性
文章:[译]C#8.0中一个使接口更加灵活的新特性-默认接口实现 原文示例代码: public interface IBook { void AddBook(string bookName, stri ...
- 关于交叉熵损失函数Cross Entropy Loss
1.说在前面 最近在学习object detection的论文,又遇到交叉熵.高斯混合模型等之类的知识,发现自己没有搞明白这些概念,也从来没有认真总结归纳过,所以觉得自己应该沉下心,对以前的知识做一个 ...
- JDBC连接池的九种查询
package JDBC_Demo; import java.sql.SQLException; import java.util.List; import java.util.Map; import ...
- Module build failed: Error: Cannot find module 'node-sass'
安装npm 遇到 Module build failed: Error: Cannot find module 'node-sass' 这次通过重装 npm 完成 先卸载npm npm uninsta ...
- Cannot execute statement: impossible to write to binary log since BINLOG_FORMAT = STATEMENT and at least one table uses a storage engine limited to row-based logging. InnoDB is limited to row-logging
1665 - Cannot execute statement: impossible to write to binary log since BINLOG_FORMAT = STATEMENT a ...