CGLIB 是一个功能强大,高性能的代码生成包。它为没有实现接口的类提供代理,为 JDK 的动态代理提供了很好的补充。通常可以使用Java的动态代理创建代理,但当要代理的类没有实现接口或者为了更好的性能,CGLIB 是一个好的选择。
CGLIB 作为一个开源项目,其代码托管在 Github,地址为:https://github.com/cglib/cglib
广泛的被许多 AOP 的框架使用,例如 Spring AOP 和 dynaop 。Hibernate 使用 CGLIB 来代理单端single-ended(多对一和一对一)关联。
CGLIB 代理主要通过对字节码的操作,为对象引入间接级别,以控制对象的访问。我们知道 Java 中有一个动态代理也是做这个事情的,那我们为什么不直接使用 Java 动态代理,而要使用 CGLIB 呢?
答案是 CGLIB 相比于 JDK动态代理 更加强大,JDK动态代理 虽然简单易用,但是其有一个致命缺陷是,只能对接口进行代理。如果要代理的类为一个普通类、没有接口,那么 Java动态代理 就没法使用了。
Java动态代理机制 的出现,使得 Java开发人员 不用手工编写代理类,只要简单地制定一组接口及委托类对象,便能动态地获得代理类。代理类会负责将所有的方法调用分配到委托对象上反射执行,配置执行过程中,开发人员还可以进行修改。
代理是一种常用的设计模式,其目的就是为其他对象提供一个代理以控制对某个对象的访问。代理类负责为委托类预处理消息、过滤消息并转发消息,以及进行消息被委托类执行后的后续处理。
- 为了保持行为的一致性,代理类和委托类通常会实现相同的接口
- 引入代理能够控制对委托对象的直接访问,可以很好的隐藏和保护委托对象,也更加具有灵活性
要了解 Java 动态代理的机制,首先需要了解以下相关的类或接口:
- :这是 Java 动态代理机制的主类,它提供了一组静态方法来为一组接口动态地生成代理类及其对象
- :这是调用处理器接口,它自定义了一个 方法,用于几种处理在动态代理类对象上的方法调用。通常在该方法中实现对委托类的代理访问。
- :Proxy 静态方法生成动态代理类同样需要通过类装载器来进行装载才能使用,它与普通类的唯一区别就是其字节码是由 在运行时动态生成的而非预存在于任何一个 文件中。
首先让我们来了解一下如何使用 Java 动态代理。具体有如下四步骤:
- 通过实现 接口创建自己的调用处理器;
- 通过为 Proxy 类指定 对象和一组 来创建动态代理类;
- 通过反射机制获得动态代理类的构造函数,其唯一参数类型是调用处理器接口类型;
- 通过构造函数创建动态代理类实例,构造时调用处理器对象作为参数被传入。
实际使用过程更加简单,因为 Proxy 的静态方法 已经为我们封装了步骤 2 到步骤 4 的过程,所以简化后的过程如下:
动态生成的代理类本身的一些特点
- 包:如果所代理的接口都是 的,那么它将被定义在顶层包(即包路径为空),如果所代理的接口中有 的接口(因为接口不能被定义为 或 ,所以除 之外就是默认的 访问级别,那么它将被定义在该接口所在包,这样设计的目的是为了最大程度的保证动态代理类不会因为包管理的问题而无法被成功定义并访问;
- 类修饰符:该代理类具有 和 修饰符,意味着它可以被所有的类访问,但是不能被再度继承;
- 类名:格式是 ,其中 N 是一个逐一递增的阿拉伯数字,代表 类 第 N 次 生成的动态代理类,值得注意的一点是,并不是每次调用 Proxy 的静态方法创建动态代理类都会使得 N 值增加,原因是如果对同一组接口(包括接口排列的顺序相同)试图重复创建动态代理类,它会很聪明地返回先前已经创建好的代理类的类对象,而不会再尝试去创建一个全新的代理类,这样可以节省不必要的代码重复生成,提高了代理类的创建效率。
- 类继承关系:Proxy 类是它的父类,这个规则适用于所有由 Proxy 创建的动态代理类。而且该类还实现了其所代理的一组接口。
代理类实例的一些特点:
- 每个实例都会关联一个 (调用处理器对象),在代理类实例上调用其代理接口中声明的方法时,最终都会由 的 执行;
- 中有三个方法也同样会被分派到调用处理器的 执行,它们是 , 和 ;
被代理接口的一组特点
- 要注意不能有重复的接口
- 接口对于类装载器必须可见,否则类装载器将无法链接它们
- 被代理的所有非 public 的接口必须在同一个包中,接口的数目不能超过65535
Proxy 只能对 interface 进行代理,无法实现对 class 的动态代理。观察动态生成的代理继承关系图可知原因,他们已经有一个固定的父类叫做 Proxy ,Java语法 限定其不能再继承其他的父类。
最后以一个简单的动态代理例子结束
程序输出为:
CGLIB 底层使用了ASM(一个短小精悍的字节码操作框架)来操作字节码生成新的类。除了 CGLIB库 外,脚本语言(如 Groovy 和 BeanShell )也使用 ASM 生成字节码。ASM 使用类似 SAX 的解析器来实现高性能。
- cglib-nodep-2.2.jar:使用 nodep 包不需要关联 asm 的 jar 包,jar 包内部包含 asm 的类。
- cglib-2.2.jar:使用此 jar 包需要关联 asm 的 jar 包,否则运行时报错。
说了这么多,可能大家还是不知道 CGLIB 是干什么用的。下面我们将使用一个简单的例子来演示如何使用 CGLIB 对一个方法进行拦截。 首先,我们需要在工程的 POM 文件中引入 CGLIB 的 dependency,这里我们使用的是 2.2.2 版本。
依赖包下载后,我们就可以干活了,按照国际惯例,写个
在 函数中,我们通过一个 和一个 来实现对方法的拦截,运行程序后输出为:
- Enhancer 可能是 CGLIB 中最常用的一个类,和 Java1.3动态代理 中引入的 Proxy 类差不多。
- 和Proxy 不同的是,Enhancer 既能够代理普通的 class ,也能够代理接口。
- Enhancer 创建一个被代理对象的子类并且拦截所有的方法调用(包括从 Object 中继承的 toString 和 hashCode 方法)。
- Enhancer 不能够拦截 final 方法,例如 方法,这是由于 Java final 方法语义决定的。
- 基于同样的道理,Enhancer 也不能对 fianl 类进行代理操作。这也是 Hibernate 为什么不能持久化 final class 的原因。
下面我们将以这个类作为主要的测试类,来测试调用各种方法
输出:
- 上述代码中, 用来对所有拦截的方法返回相同的值,从输出我们可以看出来:
- 对 非final方法test()、toString()、hashCode()进行了拦截,没有对getClass进行拦截。
- 由于 方法需要返回一个 ,但是我们返回的是一个 ,这解释了上面的程序中为什么会抛出异常。
- 用来设置父类型,从 可以看出,使用 生成的类为被代理类的一个子类,形如:
- 方法是用来创建增强对象的,其提供了很多不同参数的方法用来匹配被增强类的不同构造方法。(虽然类的构造方法只是 层面的函数,但是 却不能对其进行操作。 同样不能操作 或者 类)。
- 我们也可以先使用 来创建字节码( ),然后用字节码动态的生成增强后的对象。
可以使用一个 作为回调,使用 方法来替换直接访问类的方法,但是你必须注意死循环。因为 中调用的任何原代理类方法,均会重新代理到 方法中。
为了避免这种死循环,我们可以使用 , 的例子在前面的 中已经介绍过了,这里就不浪费时间了。
有些时候我们可能只想对特定的方法进行拦截,对其他的方法直接放行,不做任何操作,使用Enhancer处理这种需求同样很简单,只需要一个 即可:
在编写程序时,有时候会调用许多 中实现实现的函数,但某些方法需要我们传入一个方法,以便在需要的时候调用我们传入进去的函数。这个被传入的函数称为回调函数(Callback function)。
在上面的例子中,炒菜是我们需要调用的方法,也是 API 库中所提供的,而炒菜的方式,则是我们去选择的,可以我们自己去定义的。
这个就可以回调函数,有库函数()来执行我们传入的回调函数()
Callable接口
在 Java1.8 官方文档中给出的内容为
- 参数类型:V - 回调方法的返回值类型
- 已经实现的子接口:,
- 这个接口位函数试接口
- 返回结果可能引发异常,这个接口与 Runnable 非常相似,这两个接口的设计可以在实例化后,开启新的线程,与 Runnable 的差别是,Runnable 不能返回参数也不能抛出异常。
例子:
测试:
返回值:
Callback接口
已知实现此接口的类 AuthorizeCallback, ChoiceCallback, ConfirmationCallback, LanguageCallback, NameCallback, PasswordCallback, RealmCallback, RealmChoiceCallback, TextInputCallback, TextOutputCallback
这个接口的实现了会被传递给 ,允许有能力的底层服务去回应这个回调方法,已便进行诸如数据检索等信息。回调函数不检索或显示底层安全服务请求的信息。回调实现只是提供了将这些请求传递给应用程序的方法,并且对于应用程序,如果合适的话,可以将请求的信息返回给底层的安全服务。
这个接口是可以自己定义的,定制适用于当前业务的callback 接口类型来表示不同类型的回调函数。
callback接口的源码
CallbackHandler 接口
方法: ,这个方法是用来处理处理 callback 类型的
官方实例:
通过传入不同的已经实现了 Callback 接口的实现类,通过分析不同实现类的类型来进行不同的处理,调用形参实现类内的方法(回调)。
在一般工作中,我们都是自己定义接口,写实现类,来进行回调的。
自定义的回调函数实例
- 这个是Callback接口类,我们一会儿要是用它来创造内部匿名类,来实现这个接口,完成字表的筛选工作:
- 这个是处理端,通过handler方法,调用传入的 类型中的方法,来对字表进行操作
- 使用 接口并实现它,来让 来调用它其中的 方法来完成对字表的筛选
结果:
true 为 process 的返回值,剩下的为我们筛选出字表中包含有 1 的字符串。
通过名字就可以知道,不可变的 Bean 。ImmutableBean 允许创建一个原来对象的包装类,这个包装类是不可变的,任何改变底层对象的包装类操作都会抛出 IllegalStateException 。但是我们可以通过直接操作底层对象来改变包装类对象。这有点类似于 Guava 中的不可变视图。为了对 ImmutableBean 进行测试,这里需要再引入一个bean:
然后编写测试类如下:
要是报以下的错误:
请更换 JDK 的版本,这是由于 JDK 8 中有关反射相关的功能自从 JDK 9 开始就已经被限制了,为了兼容原先的版本,需要在运行项目时添加 选项来开启这种默认不被允许的行为。如果是通过 IDEA 来运行项目,那么可以在 “Edit Configurations” 中 ——> “VM options” 输入框中输入该选项来完成,最终结果如下图所示:
结果:
CGLIB 提供的一个操作 bean 的工具,使用它能够在运行时动态的创建一个 bean 。
在上面的代码中,我们使用 CGLIB 动态的创建了一个和 相同的 Bean 对象,包含一个属性 value 以及 getter 、 setter 方法。
结果:
CGLIB 提供的能够从一个 bean 复制到另一个 bean 中,而且其还提供了一个转换器,用来在转换的时候对 bean 的属性进行操作。
测试:
相比于 BeanCopier ,BulkBean 将 copy 的动作拆分为 getPropertyValues 和 setPropertyValues 两个方法,允许自定义处理属性
使用注意:
- 避免每次进行 创建对象,一般将其声明为 的
- 应用场景:针对特定属性的 操作,一般适用通过 配置注入和注出的属性,运行时才确定处理的 类,只需要关注属性名即可。
BeanMap 类实现了Java Map,将一个bean对象中的所有属性转换为一个 String-to-Obejct 的 Java Map
keyFactory 类用来动态生成接口的实例,接口需要只包含一个 newInstance 方法,返回一个 Object 。keyFactory 为构造出来的实例动态生成了 Object.equals 和 Object.hashCode 方法,能够确保相同的参数构造出的实例为单例的。
我们首先构造一个满足条件的接口,然后进行测试
Mixin 能够让我们将多个对象整合到一个对象中去,前提是这些对象必须是接口的实现。可能这样说比较晦涩,以代码为例:
Mixin 类比较尴尬,因为他要求 Minix 的类(例如 MixinInterface )实现一些接口。既然被 Minix 的类已经实现了相应的接口,那么我就直接可以通过纯 Java 的方式实现,没有必要使用 Minix 类。
用来模拟一个 String 到 int 类型的 Map 类型。如果在 Java7 以后的版本中,类似一个 switch 语句。
正如名字所言,Interface Maker 用来创建一个新的 Interface
上述的 Interface Maker 创建的接口中只含有一个方法,签名为 double foo(int)。Interface Maker与上面介绍的其他类不同,它依赖 ASM 中的 Type 类型。由于接口仅仅只用做在编译时期进行类型检查,因此在一个运行的应用中动态的创建接口没有什么作用。但是 InterfaceMaker 可以用来自动生成代码,为以后的开发做准备。
MethodDelegate 主要用来对方法进行代理
测试:
关于 Method.create 的参数说明:
- 第二个参数为即将被代理的方法
- 第一个参数必须是一个无参数构造的 bean 。因此 MethodDelegate.create 并不是你想象的那么有用
- 第三个参数为只含有一个方法的接口。当这个接口中的方法被调用的时候,将会调用第一个参数所指向 bean 的第二个参数方法
缺点:
- 为每一个代理类创建了一个新的类,这样可能会占用大量的永久代堆内存
- 你不能代理需要参数的方法
- 如果你定义的接口中的方法需要参数,那么代理将不会工作,并且也不会抛出异常;
- 如果你的接口中方法需要其他的返回类型,那么将抛出 IllegalArgumentException
- 多重代理和方法代理差不多,都是将代理类方法的调用委托给被代理类。使用前提是需要一个接口,以及一个类实现了该接口
- 通过这种interface的继承关系,我们能够将接口上方法的调用分散给各个实现类上面去。
- 多重代理的缺点是接口只能含有一个方法,如果被代理的方法拥有返回值,那么调用代理类的返回值为最后一个添加的被代理类的方法返回值
测试:
为了对构造函数进行代理,我们需要一个接口,这个接口只含有一个 方法,用来调用相应的构造函数
测试:
能够对多个数组同时进行排序,目前实现的算法有归并排序和快速排序
顾明思义, 就是对 Class 对象进行特定的处理,比如通过数组保存 method 引用,因此 引出了一个 index 下标的新概念,比如 就是以前的获取method的方法。通过数组存储method,constructor等class信息,从而将原先的反射调用,转化为 的直接调用,从而体现所谓的
由于 的大部分类是直接对Java字节码进行操作,这样生成的类会在Java的永久堆中。如果动态代理操作过多,容易造成永久堆满,触发 异常。
- Java动态代理只能够对接口进行代理,不能对普通的类进行代理(因为所有生成的代理类的父类为 Proxy,Java 类继承机制不允许多重继承);CGLIB 能够代理普通类;
- Java动态代理使用Java原生的反射 API 进行操作,在生成类上比较高效;CGLIB 使用 ASM 框架直接对字节码进行操作,在类的执行过程中比较高
CGLIB相关的文章:
- http://jnb.ociweb.com/jnb/jnbNov2005.html
- http://www.iteye.com/topic/
- http://mydailyjava.blogspot.kr/2013/11/cglib-missing-manual.html
版权声明:
本文来源网络,所有图片文章版权属于原作者,如有侵权,联系删除。
本文网址:https://www.mushiming.com/mjsbk/2528.html