简介

为什么会有动态代理?

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

这里可以采用两种方式:

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

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

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

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

不限于以上优点,动态代理被广泛应用于日志记录、性能统计、安全控制、事务处理、异常处理等等,是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. Springboot与任务整合(四)

    一 异步任务 启动类 @MapperScan("com.topcheer.*.*.dao") @SpringBootApplication @EnableCaching @Enab ...

  2. SpringBoot整合FastJson(七)

    一.Maven依赖 <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson&l ...

  3. Okhttp 请求流程梳理

    最近在看 Okhttp 的源码.不得不说源码设计的很巧妙,从中能学到很多.其实网上关于 Okhttp 的文章已经很多了,自己也看了很多.但是俗话说得好,好记性不如烂笔头,当你动手的时候,你会发现你在看 ...

  4. 明解C语言 入门篇 第一章答案

    练习1-1 #include <stdio.h> int main() { int a; a = 15; int b; b = 37; int c; c = a - b; printf(& ...

  5. u检验粗浅理解

    假设检验是以小概率事件,在一次实验中是不可能发生为前提(事实上是有可能发生的,但不是这样说的话,就落入一个圈,不能继续玩了),来否认原假设. u检验的定义: 已知从正态母体N(u,σ2)中抽得容量为n ...

  6. Django学习day1——Django的简单介绍

    1.了解Web基本的开发 使用Python开发Web,最简单,原始和直接的办法是使用CGI标准现在从应用角度解释它是如何工作: 首先做一个Python脚本,输出HTML代码,然后保存成.cgi扩展名的 ...

  7. [考试反思]1107csp-s模拟测试104: 速度

    20分钟能做什么? 不粘排行榜,没意义,第一机房集体重启,我侥幸找回了两个文件才有分. 实际得分应该是70+100+60,第二机房rank1...放在第一机房就不知道了 T1:中间值 比较喜欢题解的第 ...

  8. [考试反思]0727NOIP模拟测试9

    啊哈?水到一个rk1? 谢谢诸位大佬放水让我这种人体验到了rk1的滋味. 怪怪的滋味.不太像我的水平. 其实这次考试心态已经佛了,刚意识到前6次考试累计的挺高的分数被清空了,7,8两场又爆炸了... ...

  9. UiPath之基础知识(一)

    各位小伙伴,大家好.在10月份小U的微信订阅号做了一个投票,主题是UiPath目前已经掌握的程度. 从投票的结果来看,有一半以上的人还是刚刚起步,为了帮助刚刚起步的小伙伴,准备陆续发布一些基础性的内容 ...

  10. Docker 开篇2 | 树莓派安装docker 续

    问题1:安装后出现错误Error! The dkms.conf for this module includes a BUILD_EXCLUSIVE directive which does not ...