简介

为什么会有动态代理?

举个例子,当前有一个用户操作类,要求每个方法执行前打印访问日志。

这里可以采用两种方式:

第一种,静态代理。即通过继承原有类来对方法进行扩展。

当然,这种方式可以实现需求,但是当类的方法很多时,我们需要逐个添加打印日志的代码,非常繁琐。此时,如果要求加入权限校验,这个时候又需要再创建一个代理类。

第二种,动态代理。即通过拦截器的方式来对方法进行扩展。

动态代理只需要重写拦截器的一个方法,相比静态代理,可以减少很多代码。而且,动态代理要实现不同的代理类,只要选择不同的拦截器就可以了(可以选择多个),代理类不需要我们自己实现,可以有效实现代码解耦和可重用。

不限于以上优点,动态代理被广泛应用于日志记录、性能统计、安全控制、事务处理、异常处理等等,是spring实现AOP的重要支持。

常见的动态代理有哪些?

常用的动态代理有:JDK动态代理、cglib。

感兴趣的可以研究下aspectJ

什么是cglib

cglib基于asm字节码生成框架,用于动态生成代理类。与JDK动态代理不同,有以下几点不同:

  1. JDK动态代理要求被代理类实现某个接口,而cglib无该要求。

  2. JDK动态代理生成的代理类是该接口实现类,也就是说,不能代理接口中没有的方法,而cglib生成的代理类继承被代理类。

  3. 在字节码的生成和类的创建上,JDK的动态代理效率更高。

  4. 在代理方法的执行效率上,由于采用了FastClass,cglib的效率更高(以空间换时间)。

注:因为JDK动态代理中代理类中的方法是通过反射调用的,而cglib因为引入了FastClass,可以直接调用代理类对象的方法。

使用例子

需求

模拟对用户数据进行增删改前打印访问日志

工程环境

JDK:1.8

maven:3.6.1

IDE:STS4

主要步骤

  1. 创建Enhancer对象:Enhancer是cglib代理的对外接口,以下操作都是调用这个类的方法
  2. setSuperclass(Class superclass):代理谁?
  3. setCallback(final Callback callback):怎么代理?(我们需要实现Callback的子接口MethodInterceptor,重写其中的intercept方法,该方法定义了代理规则)
  4. create():获得代理类
  5. 使用代理类

创建项目

项目类型Maven Project,打包方式jar

引入依赖

    <!-- cglib -->
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.2.5</version>
</dependency>
<!-- junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>

编写被代理类

包路径:cn.zzs.cglib

这里只是简单地测试,不再引入复杂的业务逻辑。

public class UserController {
public void save() {
System.out.println("增加用户");
}
public void delete() {
System.out.println("删除用户");
}
public void update() {
System.out.println("修改用户");
}
public void find() {
System.out.println("查找用户");
}
}

编写MethodInterceptor接口实现类

包路径:cn.zzs.cglib

public class LogInterceptor implements MethodInterceptor {

    @Override
public Object intercept( Object obj, Method method, Object[] args, MethodProxy proxy ) throws Throwable {
// 设置需要代理拦截的方法
HashSet<String> set = new HashSet<String>( 6 );
set.add( "save" );
set.add( "delete" );
set.add( "update" );
// 进行日志记录
if( method != null && set.contains( method.getName() ) ) {
System.out.println( "进行" + method.getName() + "的日志记录" );
}
// 执行被代理类的方法
Object obj2 = proxy.invokeSuper( obj, args );
return obj2;
}
}

编写测试类

这里的输出代理类的class文件,方便后面分析。

包路径:test下的cn.zzs.cglib

public class CglibTest {
@Test
public void test01() {
// 设置输出代理类到指定路径
System.setProperty( DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "D:/growUp/test" );
// 创建Enhancer对象,用于生成代理类
Enhancer enhancer = new Enhancer();
// 设置哪个类需要代理
enhancer.setSuperclass( UserController.class );
// 设置怎么代理,这里传入的是Callback对象-MethodInterceptor父类
LogInterceptor logInterceptor = new LogInterceptor();
enhancer.setCallback( logInterceptor );
// 获取代理类实例
UserController userController = ( UserController )enhancer.create();
// 测试代理类
System.out.println( "-------------" );
userController.save();
System.out.println( "-------------" );
userController.delete();
System.out.println( "-------------" );
userController.update();
System.out.println( "-------------" );
userController.find();
}
}

运行结果

CGLIB debugging enabled, writing to 'D:/growUp/test'
-------------
进行save的日志记录
增加用户
-------------
进行delete的日志记录
删除用户
-------------
进行update的日志记录
修改用户
-------------
查找用户

源码分析-获得代理类的过程

主要步骤

这里先简单说下过程:

  1. 根据当前Enhancer实例生成一个唯一标识key

  2. 用key去缓存中找代理类的Class实例

  3. 找到了就返回代理类实例

  4. 找不到就生成后放入map,再返回代理类实例

获得key

接下来具体介绍下。

首先,一进来就先调用了createHelper()

    public Object create() {
classOnly = false;
argumentTypes = null;
return createHelper();
}

createHelper()中,创建了key,这个用于唯一标识当前类及相关配置,用于在缓存中存取代理类的Class实例。接着调用父类AbstractClassGeneratorcreate(Object key)方法获取代理类实例。

    private Object createHelper() {
preValidate();
Object key = KEY_FACTORY.newInstance((superclass != null) ? superclass.getName() : null,
ReflectUtils.getNames(interfaces),
filter == ALL_ZERO ? null : new WeakCacheKey<CallbackFilter>(filter),
callbackTypes,
useFactory,
interceptDuringConstruction,
serialVersionUID);
this.currentKey = key;
//获取代理类实例
Object result = super.create(key);
return result;
}

create(Object key)中,会调用内部类ClassLoaderDataget(AbstractClassGenerator gen, boolean useCache)方法获取代理类的Class实例。

    protected Object create(Object key) {
try {
ClassLoader loader = getClassLoader();
Map<ClassLoader, ClassLoaderData> cache = CACHE;
ClassLoaderData data = cache.get(loader);
if (data == null) {
synchronized (AbstractClassGenerator.class) {
cache = CACHE;
data = cache.get(loader);
if (data == null) {
Map<ClassLoader, ClassLoaderData> newCache = new WeakHashMap<ClassLoader, ClassLoaderData>(cache);
data = new ClassLoaderData(loader);
newCache.put(loader, data);
CACHE = newCache;
}
}
}
this.key = key;
//获取代理类的Class类实例
Object obj = data.get(this, getUseCache());
//获取代理类实例
if (obj instanceof Class) {
return firstInstance((Class) obj);
}
return nextInstance(obj);
} catch (RuntimeException e) {
throw e;
} catch (Error e) {
throw e;
} catch (Exception e) {
throw new CodeGenerationException(e);
}
}

利用key从缓存中获取Class

ClassLoaderData有一个重要字段generatedClasses,是一个LoadingCache缓存对象,存放着当前类加载器加载的代理类的Class类实例。在以下方法中,就是从它里面寻找,通过key匹配查找。

        //这个对象存放着当前类加载器加载的代理类的Class类实例
private final LoadingCache<AbstractClassGenerator, Object, Object> generatedClasses;
public Object get(AbstractClassGenerator gen, boolean useCache) {
if (!useCache) {
return gen.generate(ClassLoaderData.this);
} else {
//获取代理类的Class类实例
Object cachedValue = generatedClasses.get(gen);
return gen.unwrapCachedValue(cachedValue);
}
}

下面重点看下LoadingCache这个类,需要重点理解三个字段的意思:

//K:AbstractClassGenerator 这里指Enhancer类
//KK:Object 这里指前面生成key的类
//V:Object 这里指代理类的Class类
public class LoadingCache<K, KK, V> {
//通过key可以拿到代理类的Class实例
protected final ConcurrentMap<KK, Object> map;
//通过loader.apply(Enhancer实例)可以获得代理类的Class实例
protected final Function<K, V> loader;
//通过keyMapper.apply(Enhancer实例)可以获得key
protected final Function<K, KK> keyMapper;
·······
}

这里通过key去map里找代理类的Class实例,如果找不到,会重新生成后放入map中。

    public V get(K key) {
final KK cacheKey = keyMapper.apply(key);
Object v = map.get(cacheKey);
if (v != null && !(v instanceof FutureTask)) {
return (V) v;
} return createEntry(key, cacheKey, v);
}

生成代理类Class

以上基本说完如何从缓存中拿到代理类实例的方法,接下来简单看下生成代理类的过程,即loader.apply(Enhancer实例),里面的generate会生成所需的Class对象,比较复杂,后面有时间再研究吧。

    public Object apply(AbstractClassGenerator gen) {
Class klass = gen.generate(ClassLoaderData.this);
return gen.wrapCachedClass(klass);
}

代理类代码分析

cglib生成文件

在一开始指定的路径下,可以看到生成了三个文件,前面简介里说到在代理类的生成上,cglib的效率低于JDK动态代理,主要原因在于多生成了两个FastClass文件,至于这两个文件有什么用呢?接下来会重点分析:

代理类源码

本文采用Luyten作为反编译工具,一开始用jd-gui解析,但错误太多。

下面看看代理类的源码。

在初始化时,代理类的字段都会被初始化,这里涉及到MethodProxycreate方法。

在实际调用update方法是会调用MethodInterceptor对象的intercept方法,执行我们自定义的代码后,最终会调用的是MethodProxyinvokeSuper方法。下面重点看看这些方法。

注:考虑篇幅问题,这里仅展示update方法。

//生成类的名字规则是:被代理classname + "$$"+classgeneratorname+"ByCGLIB"+"$$"+key的hashcode
public class UserController$$EnhancerByCGLIB$$e6f193aa extends UserController implements Factory {
private boolean CGLIB$BOUND;
public static Object CGLIB$FACTORY_DATA;
private static final ThreadLocal CGLIB$THREAD_CALLBACKS;
private static final Callback[] CGLIB$STATIC_CALLBACKS; //我们一开始传入的MethodInterceptor对象
private MethodInterceptor CGLIB$CALLBACK_0;
private static Object CGLIB$CALLBACK_FILTER;
//被代理类update方法
private static final Method CGLIB$update$0$Method;
//代理类update方法
private static final MethodProxy CGLIB$update$0$Proxy;
private static final Object[] CGLIB$emptyArgs; static void CGLIB$STATICHOOK1() {
CGLIB$THREAD_CALLBACKS = new ThreadLocal();
CGLIB$emptyArgs = new Object[0];
//代理类Class对象
final Class<?> forName = Class.forName("cn.zzs.cglib.UserController$$EnhancerByCGLIB$$e6f193aa");
//被代理类Class对象
final Class<?> forName2;
final Method[] methods = ReflectUtils.findMethods(new String[] { "update", "()V", "find", "()V", "delete", "()V", "save", "()V" },
(forName2 = Class.forName("cn.zzs.cglib.UserController")).getDeclaredMethods());
//初始化被代理类update方法
CGLIB$update$0$Method = methods[0];
//初始化代理类update方法
CGLIB$update$0$Proxy = MethodProxy.create((Class)forName2, (Class)forName, "()V", "update", "CGLIB$update$0");
} final void CGLIB$update$0() {
super.update();
} public final void update() {
MethodInterceptor cglib$CALLBACK_2;
MethodInterceptor cglib$CALLBACK_0;
if ((cglib$CALLBACK_0 = (cglib$CALLBACK_2 = this.CGLIB$CALLBACK_0)) == null) {
CGLIB$BIND_CALLBACKS(this);
cglib$CALLBACK_2 = (cglib$CALLBACK_0 = this.CGLIB$CALLBACK_0);
}
//一般走这里,即调用我们传入MethodInterceptor对象的intercept方法
if (cglib$CALLBACK_0 != null) {
cglib$CALLBACK_2.intercept((Object)this, UserController$$EnhancerByCGLIB$$e6f193aa.CGLIB$update$0$Method, UserController$$EnhancerByCGLIB$$e6f193aa.CGLIB$emptyArgs, UserController$$EnhancerByCGLIB$$e6f193aa.CGLIB$update$0$Proxy);
return;
}
super.update();
}

MethodProxy.create

通过以下代码可以知道,MethodProxy对象CGLIB$update$0$Proxy持有了代理类和被代理类的Class实例,以及代理方法和被代理方法的符号表示,这两个sig用于后面获取方法索引。

    //Class c1, 被代理对象
//Class c2, 代理对象
//String desc, 参数列表描述
//String name1, 被代理方法
//String name2,代理方法
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);
//创建createInfo
proxy.createInfo = new CreateInfo(c1, c2);
return proxy;
}

MethodProxy.invokeSuper

以下方法中会去创建两个FastClass文件,也就是我们看到的另外两个文件。当然,它们只会创建一次。

另外,通过原来的方法签名获得了update的方法索引。

    //传入参数obj:代理类实例
public Object invokeSuper(Object obj, Object[] args) throws Throwable {
try {
//初始化,创建了两个FastClass类对象,并根据原来的方法签名得到方法索引
init();
//这个对象持有两个FastClass类对象和方法的索引
FastClassInfo fci = fastClassInfo;
//调用了代理对象FastClass的invoke方法
return fci.f2.invoke(fci.i2, obj, args);
} catch (InvocationTargetException e) {
throw e.getTargetException();
}
}
private void init(){
if (fastClassInfo == null){
synchronized (initLock){
if (fastClassInfo == null){
CreateInfo ci = createInfo;
FastClassInfo fci = new FastClassInfo();
//helper方法用ASM框架去生成了两个FastClass类
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;
}
}
}
}
private static class FastClassInfo{
FastClass f1;//被代理对象FastClass
FastClass f2;//代理对象FastClass
int i1;//被代理update方法的索引
int i2; //代理update方法的索引
}

FastClass.invoke

根据方法索引进行匹配,可以直接调用代理类实例的方法,而不需要像JDK动态代理一样采用反射的方式,所以在方法执行上,cglib的效率会更高。

    //传入参数:
//n:方法索引
//o:代理类实例
//array:方法输入参数
public Object invoke(final int n, final Object o, final Object[] array) throws InvocationTargetException {
final UserController$$EnhancerByCGLIB$$e6f193aa userController$$EnhancerByCGLIB$$e6f193aa = (UserController$$EnhancerByCGLIB$$e6f193aa)o;
try {
switch (n) {
case 0: {
return new Boolean(userController$$EnhancerByCGLIB$$e6f193aa.equals(array[0]));
}
case 1: {
return userController$$EnhancerByCGLIB$$e6f193aa.toString();
}
case 2: {
return new Integer(userController$$EnhancerByCGLIB$$e6f193aa.hashCode());
}
case 3: {
return userController$$EnhancerByCGLIB$$e6f193aa.clone();
}
case 4: {
//通过匹配方法索引,直接调用该方法
userController$$EnhancerByCGLIB$$e6f193aa.update();
return null;
}
······· }
catch (Throwable t) {
throw new InvocationTargetException(t);
}
throw new IllegalArgumentException("Cannot find matching method/constructor");
}

相关源码请移步:https://github.com/ZhangZiSheng001/cglib-demo

本文为原创文章,转载请附上原文出处链接:https://www.cnblogs.com/ZhangZiSheng001/p/11917086.html

cglib测试例子和源码详解的更多相关文章

  1. dom4j的测试例子和源码详解(重点对比和DOM、SAX的区别)

    目录 简介 DOM.SAX.JAXP和DOM4J xerces解释器 SAX DOM JAXP DOM解析器 获取SAX解析器 DOM4j 项目环境 工程环境 创建项目 引入依赖 使用例子--生成xm ...

  2. jdbc-mysql测试例子和源码详解

    目录 简介 什么是JDBC 几个重要的类 使用中的注意事项 使用例子 需求 工程环境 主要步骤 创建表 创建项目 引入依赖 编写jdbc.prperties 获得Connection对象 使用Conn ...

  3. DBCP2的使用例子和源码详解(不包括JNDI和JTA支持的使用)

    目录 简介 使用例子 需求 工程环境 主要步骤 创建项目 引入依赖 编写jdbc.prperties 获取连接池和获取连接 编写测试类 配置文件详解 数据库连接参数 连接池数据基本参数 连接检查参数 ...

  4. [Spark内核] 第40课:CacheManager彻底解密:CacheManager运行原理流程图和源码详解

    本课主题 CacheManager 运行原理图 CacheManager 源码解析 CacheManager 运行原理图 [下图是CacheManager的运行原理图] 首先 RDD 是通过 iter ...

  5. [Qt Creator 快速入门] 第2章 Qt程序编译和源码详解

    一.编写 Hello World Gui程序 Hello World程序就是让应用程序显示"Hello World"字符串.这是最简单的应用,但却包含了一个应用程序的基本要素,所以 ...

  6. Spark Sort-Based Shuffle具体实现内幕和源码详解

    为什么讲解Sorted-Based shuffle?2方面的原因:一,可能有些朋友看到Sorted-Based Shuffle的时候,会有一个误解,认为Spark基于Sorted-Based Shuf ...

  7. spring查看生成的cglib代理类源码详解

    1.让程序阻塞(抛出异常会导致程序结束,所以在抛出异常之前阻塞) 2. windows控制台 cd到jdk目录下的lib目录,找到sa-jdi.jar 执行: java -classpath sa-j ...

  8. Arouter核心思路和源码详解

    前言 阅读本文之前,建议读者: 对Arouter的使用有一定的了解. 对Apt技术有所了解. Arouter是一款Alibaba出品的优秀的路由框架,本文不对其进行全面的分析,只对其最重要的功能进行源 ...

  9. go map数据结构和源码详解

    目录 1. 前言 2. go map的数据结构 2.1 核心结体体 2.2 数据结构图 3. go map的常用操作 3.1 创建 3.2 插入或更新 3.3 删除 3.4 查找 3.5 range迭 ...

随机推荐

  1. (一)如何理解java面向对象编程

    哲学中,事物总是螺旋式上升,波浪式前进.因而编程也逐渐向人类更容易理解的方向前进,多年来人们苦苦追求的编程境界 : 高扩展性(extensibility),高复用性(reuseable).java语言 ...

  2. 百万年薪python之路 -- 面向对象之 反射,双下方法

    面向对象之 反射,双下方法 1. 反射 计算机科学领域主要是指程序可以访问.检测和修改它本身状态或行为的一种能力(自省) python面向对象中的反射:通过字符串的形式操作对象相关的属性.python ...

  3. webpack 4 移除 CommonsChunkPlugin,取而代之的是两个新的配置项(optimization.splitChunks 和 optimization.runtimeChunk

    默认方式 webpack模式模式现在已经做了一些通用性优化,适用于多数使用者. 需要注意的是:默认模式只影响按需(on-demand)加载的代码块(chunk),因为改变初始代码块会影响声明在HTML ...

  4. 关于在vue-cli脚手架中使用CDN引入element-ui不成功的坑

    在前端开发过程中,为了减少最后打包出来的体积,我们会用到cdn引入一些比较大的库来解决. 常见我们引入的element-ui库,在最近使用cdn引入时,无论如何都引入不成功,其他的如Vue.vue-r ...

  5. 使用 pdf.js 跨域问题的处理方法1

    在<使用 pdf.js 在网页中加载 pdf 文件>中详细介绍了 pdf.js 的使用与集成网页开发的基本方法.展示效果如下图: 站点的目录为 http://localhost:8033/ ...

  6. ThreadPoolExecutor源码中的适配器模式

    什么是适配器模式 网上已有很多的教程,不细讲了.可以参考:五分钟了解设计模式(3)---适配器模式 在适配器模式中,一定要识别清楚,Target Adaptee Adapter分别是哪些类或接口,这样 ...

  7. 2019.NET Conf,我们在共同期待

    (一)回顾一个小社区红过的五分钟 不知不觉,距离中国.net社区组织的.net conf只有不到一周的时间,还记得年初在叶伟民老师,潘淳老师和张善友老师的号召下,我们长沙的十几位开发者自发组织起来,拉 ...

  8. spring @Value("${name}")使用

    在springmvc.xml配置文件中加入 (注意是springmvc配置文件不是spring配置文件的xml)不然可能取不到值 <context:property-placeholder lo ...

  9. jdk基础配置

    今遇到一事,tomcat启动是报错,将jdk位数错误,有问题,32位和64位的问题 cmd java -verison 显示的确实是jdk 64位,又跑到 环境变量看了下java_home的配置 这里 ...

  10. 前端技术之:使用webpack构建React程序配置方法

    package.json中需要引入以下依赖:   开发依赖: "@babel/core": "^7.1.2", "@babel/preset-env& ...