什么是CGLIB
CGLIB是一个强大的、高性能的代码生成库。其被广泛应用于AOP框架(Spring、dynaop)中,用以提供方法拦截操作。Hibernate作为一个比较受欢迎的ORM框架,同样使用CGLIB来代理单端(多对一和一对一)关联(延迟提取集合使用的另一种机制)。CGLIB作为一个开源项目,其代码托管在github,地址为:https://github.com/cglib/cglib
为什么使用CGLIB
CGLIB代理主要通过对字节码的操作,为对象引入间接级别,以控制对象的访问。我们知道Java中有一个动态代理也是做这个事情的,那我们为什么不直接使用Java动态代理,而要使用CGLIB呢?答案是CGLIB相比于JDK动态代理更加强大,JDK动态代理虽然简单易用,但是其有一个致命缺陷是,只能对接口进行代理。如果要代理的类为一个普通类、没有接口,那么Java动态代理就没法使用了。关于Java动态代理,可以参者这里Java动态代理分析
CGLIB组成结构
CGLIB底层使用了ASM(一个短小精悍的字节码操作框架)来操作字节码生成新的类。除了CGLIB库外,脚本语言(如Groovy何BeanShell)也使用ASM生成字节码。ASM使用类似SAX的解析器来实现高性能。我们不鼓励直接使用ASM,因为它需要对Java字节码的格式足够的了解
例子
说了这么多,可能大家还是不知道CGLIB是干什么用的。下面我们将使用一个简单的例子来演示如何使用CGLIB对一个方法进行拦截。
首先,我们需要在工程的POM文件中引入cglib的dependency,这里我们使用的是2.2.2版本
<dependency>
<groupId>cglib<groupId>
<artifactId>cglib<artifactId>
<version>2.2.2<version>
dependency>
依赖包下载后,我们就可以干活了,按照国际惯例,写个hello world
publicclass SampleClass {
public void test(){
System.out.println("hello world");
}
public static void main(String[] args) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(SampleClass.class);
enhancer.setCallback(new MethodInterceptor() {
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { System.out.println("before method run...");
Object result = proxy.invokeSuper(obj, args);
System.out.println("after method run...");
return result; } });
SampleClass sample = (SampleClass) enhancer.create();
sample.test();
}}
在mian函数中,我们通过一个Enhancer和一个MethodInterceptor来实现对方法的拦截,运行程序后输出为:
before methodrun...helloworldaftermethodrun... 1 2 3 在上面的程序中,我们引入了Enhancer和MethodInterceptor,可能有些读者还不太了解。别急,我们后面将会一一进行介绍。就目前而言,一个使用CGLIB的小demo就完成了
常用的API
目前网络上对CGLIB的介绍资料比较少,造成对cglib的学习困难。这里我将对cglib中的常用类进行一个介绍。为了避免解释的不清楚,我将为每个类都配有一个demo,用来做进一步的说明。首先就从Enhancer开始吧。
Enhancer
Enhancer可能是CGLIB中最常用的一个类,和Java1.3动态代理中引入的Proxy类差不多(如果对Proxy不懂,可以参考这里)。和Proxy不同的是,Enhancer既能够代理普通的class,也能够代理接口。Enhancer创建一个被代理对象的子类并且拦截所有的方法调用(包括从Object中继承的toString和hashCode方法)。Enhancer不能够拦截final方法,例如Object.getClass()方法,这是由于Java final方法语义决定的。基于同样的道理,Enhancer也不能对fianl类进行代理操作。这也是Hibernate为什么不能持久化final class的原因。
publicclass SampleClass { public String test(String input){ return"hello world"; }} 1 2 3 4 5 下面我们将以这个类作为主要的测试类,来测试调用各种方法
@TestpublicvoidtestFixedValue(){
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(SampleClass.class);
enhancer.setCallback(new FixedValue() {
public Object loadObject() throws Exception {
return"Hello cglib"; } });
SampleClass proxy = (SampleClass) enhancer.create();
System.out.println(proxy.test(null)); //拦截test,输出Hello cglib
System.out.println(proxy.toString());
System.out.println(proxy.getClass());
System.out.println(proxy.hashCode());}
程序的输出为:
Hello cglibHello cglibclass com.zeus.cglib.SampleClass$$EnhancerByCGLIB$$e3ea9b7java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Number at com.zeus.cglib.SampleClass$$EnhancerByCGLIB$$e3ea9b7.hashCode() ...
上述代码中,FixedValue用来对所有拦截的方法返回相同的值,从输出我们可以看出来,Enhancer对非final方法test()、toString()、hashCode()进行了拦截,没有对getClass进行拦截。由于hashCode()方法需要返回一个Number,但是我们返回的是一个String,这解释了上面的程序中为什么会抛出异常。
Enhancer.setSuperclass用来设置父类型,从toString方法可以看出,使用CGLIB生成的类为被代理类的一个子类,形如:SampleClass$$EnhancerByCGLIB$$e3ea9b7
Enhancer.create(Object…)方法是用来创建增强对象的,其提供了很多不同参数的方法用来匹配被增强类的不同构造方法。(虽然类的构造放法只是Java字节码层面的函数,但是Enhancer却不能对其进行操作。Enhancer同样不能操作static或者final类)。我们也可以先使用Enhancer.createClass()来创建字节码(.class),然后用字节码动态的生成增强后的对象。
可以使用一个InvocationHandler(如果对InvocationHandler不懂,可以参考这里)作为回调,使用invoke方法来替换直接访问类的方法,但是你必须注意死循环。因为invoke中调用的任何原代理类方法,均会重新代理到invoke方法中。
publicvoidtestInvocationHandler() throws Exception{ Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(SampleClass.class); enhancer.setCallback(new InvocationHandler() { @Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if(method.getDeclaringClass() != Object.class && method.getReturnType() == String.class){ return"hello cglib"; }else{ thrownew RuntimeException("Do not know what to do"); } } }); SampleClass proxy = (SampleClass) enhancer.create(); Assert.assertEquals("hello cglib", proxy.test(null)); Assert.assertNotEquals("Hello cglib", proxy.toString());}