avatar

Cglib使用教程

本文基于三篇博客编写博客:CGLIB(Code Generation Library)详解CGLib手册(CGLib: The Missing Manual)CGLib: The Missing Manual

什么是Cglib

CGLIB是一个强大的、高性能的代码生成库。CGLIB作为一个开源项目,其代码托管在github,地址为:https://github.com/cglib/cglib

Cglib库的应用

作为byte code库,CGLib是许多著名的java框架(Hibernate、Spring等)比较流行的选择。Byte code库允许在java应用的编译阶段之后,对class进行操作或者动态创建新的class。因为java虚拟机是在需要class时才加载它,这就让在程序运行时加载新的(新创建或者修改)class成为可能。比如,Hibernate在生成动态代理时,就会使用的cglib库生成。Hibernate在返回查询数据时,返回的其实是一个代理版本的对象,而不是一个包含数据库中完整数据的对象,只有当需要某个字段时,才真正对其进行加载。又比如,Spring在为方法添加安全性规则时,使用的也是cglib库。
Spring在访问方法时,会首先校验是否能访问该方法,仅当校验通过后才调用到具体的方法。而不是,直接就去访问你需要的方法。另外一个典型的cglib应用场景是在mock框架中,比如mockito。mock框架会使用cglib来代理目标类,并将目标类的所有方法都覆盖成空方法(同时会添加一些跟踪逻辑)。

为什么要使用Cglib

CGLIB代理主要通过对字节码的操作,为对象引入间接级别,以控制对象的访问。我们知道Java中有一个动态代理也是做这个事情的,那我们为什么不直接使用Java动态代理,而要使用CGLIB呢?答案是CGLIB相比于JDK动态代理更加强大,JDK动态代理虽然简单易用,但是其有一个致命缺陷是,只能对接口进行代理。如果要代理的类为一个普通类、没有接口,那么Java动态代理就没法使用了

Cglib组成结构

CGLIB底层使用了ASM(一个短小精悍的字节码操作框架)来操作字节码生成新的类。除了CGLIB库外,脚本语言(如Groovy何BeanShell)也使用ASM生成字节码。ASM使用类似SAX的解析器来实现高性能。我们不鼓励直接使用ASM,因为它需要对Java字节码的格式足够的了解

Cglib的简单使用

引入Cglib

我们创建一个maven工程,然后将Cglib的依赖引入进来,我们可以在maven库找到

<!-- https://mvnrepository.com/artifact/cglib/cglib -->
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>

简单实用

简单的一个hello world来使用Cglib对类进行增强

public class CglibDemo {
public static void main(String[] args) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(SampleClass.class);
enhancer.setCallback(new MethodInterceptor() {
@Override
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();//通过Enhancer生成一个代理类来增强SampleClass
sample.test();
}
}
//需要进行方法增强的类
class SampleClass
{
public void test()
{
System.out.println("hello world");
}
}

在mian函数中,我们通过一个Enhancer和一个MethodInterceptor来实现对方法的拦截,运行程序后输出为:

/**
* before method run...
* hello world
* after method run...
*/

这里简单认识一下Cglib的功能强大以及简单实用,下面我们来慢慢了解Cglib

Cglib中的类

Enhancer

我们从Enhancer类开始讲解,这个类可能是Cglib最常用的类了,一个Enhancer实例可以为一个不是先任何接口的类创建代理,可以将Enhancer与Java1.3中引入的Proxy类进行比较。
Enhancer会动态的为指定类创建子类,该子类的所有方法都可以被拦截处理。与Java 1.3中的Proxy不一样,Enhancer不仅能实现接口的动态代理,还可以实现类的动态代理。
后续的实例都根据这个POJO进行演示

public class SampleClass {
public String test(String input) {
return "Hello world!";
}
}

test(String)方法的返回值可以通过实用cglib的Ehancer和回调类FexedValue轻松替换

/**
* FixedValue主要是用于修改方法的返回值
*/
public class FixedValueDemo {
public static SampleClass testFixedValue()
{
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(SampleClass.class);
enhancer.setCallback(new FixedValue() {
@Override
public Object loadObject() throws Exception {//仅仅是用于控制方法的返回值,所以并不需要什么参数。最简单的一种
return "hello Cglib";
}
});
return (SampleClass) enhancer.create();
}
}
//客户端调用
public class Client {
public static void main(String[] args) {
SampleClass proxy = FixedValueDemo.testFixedValue();
System.out.println(proxy.test(null));
System.out.println(proxy.hashCode());
System.out.println(proxy.toString());
}
}
//则会输出 hello Cglib 而不是 Hello world!

在上面的事例中,enhancer将会返回一个增强的SampleClass的子类的实例,该实例会拦截所有方法的调用,从而直接返回callback中定义的返回值Hello cglib,该增强实例是通过Enhancer#create(Object...)方法创建的,该方法可输入多个参数,这些参数会被用于调用SampleClass的构造方法(尽管构造方法在字节码级别仅仅是普通的java方法,但是Enhancer不会增强(代理)构造方法。同时Enhancer也不能增强static或者final关键字定义的class
如果你只需要创建一个增强class,而不需要创建其实例,你可以使用Enhancer#createClass方法,当创建了增强class之后,你就可以使用它来动态创建实例了。增强class将包含SampleClass的所有构造方法,并且会将构造方法的调用都代理到SampleClass。

//return (SampleClass) enhancer.create();
Class sampleClass = enhancer.createClass();//创建一个Class类
return (SampleClass)sampleClass.newInstance();//实例化

需要注意的是,SampleClass的所有方法的调用都会被代理,包括从java.lang.Object继承而来的方法。
所以,调用proxy.toString()同样会返回”Hello cglib”,。这就会导致调用proxy.hashCode()方法抛出ClassCastException异常,因为调用proxy.hashCode()会被代理并返回”Hello cglib!”这个字符串类型的值,但是Object#hashCode方法却需要一个int类型的值。
通过观察可以做出另外一个推断:final方法是不会被代理(拦截)的,比如,Object#getClass会返回一个类似于class com.zenhsin.cglibdemo.SampleClass$$EnhancerByCGLIB$$fc03b50f的类名。这个类名是cglib随机生成的,目的是为了避免生成的类名冲突。cglib生成的类与被增强的类包名相同,这样就可以实现覆盖package-private方法了。与不能增强(代理)的final方法类似,通过生成子类实现增强的方案不能增强(代理)final类。所以,对于Hibernate这样的框架,不能持久化final类。

  • 更强大的callback——InvocationHandler:

    /**
    * InvocationHandler的invoke方法的参数有Object proxy, Method method, Object[] args
    * proxy:生成的代理类
    * method:需要代理的方法
    * args:方法参数
    * 如果使用method.invoke(),并传入proxy,那么就会调用proxy的方法,但是proxy的方法会因为代理再次进入InvocationHandler,造成循环
    * 解决办法就是将需要代理的类传入,作为参数,就能是实现代理了。
    */
    public class InvocationHandlerDemo {
    public static SampleClass GetProxy()
    {
    Enhancer enhancer = new Enhancer();
    enhancer.setSuperclass(SampleClass.class);
    enhancer.setCallback(new InvocationHandler() {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    if(method.getDeclaringClass() != Object.class
    && method.getReturnType() == String.class) {
    return "Hello cglib!";
    } else {
    throw new RuntimeException("Do not know what to do.");
    }
    }
    });
    return (SampleClass) enhancer.create();
    }
    }
    //客户端实用
    SampleClass proxy = InvocationHandlerDemo.GetProxy();
    System.out.println(proxy.test(null));
    System.out.println(proxy.toString());
    System.out.println(proxy.getClass());

    这个callback实现,可以让我们通过判断调用的方法进行处理并返回数据。需要注意的是,当使用InvocationHandler#invoke方法的输入参数proxy进行方法调用的时候,会导致无限循环。因为通过proxy调用的任何方法,都会被同一个InvocationHandler代理,从而调用其invoke方法,导致无限循环。为了避免这种情况,可以使用MethodInterceptor:

    /**
    * MethodInterceptor的intercept有四个参数:Object obj, Method method, Object[] args, MethodProxy proxy
    * obj:"this", the enhanced object,也就是Enhancer的实例化对象
    * method: intercepted Method,被拦截的方法
    * args:方法的参数
    * proxy:用于调用未拦截的方法,因为method会再次被拦截,所以需要proxy来调用没有拦截的时候的方法,使用proxy.invokeSuper(obj, args)即可
    * proxy.invokeSuper:在指定的对象上调用原始(父类)方法。
    */
    public class MethodInterceptorDemo {
    public static SampleClass GetProxy()
    {
    Enhancer enhancer = new Enhancer();
    enhancer.setSuperclass(SampleClass.class);
    enhancer.setCallback(new MethodInterceptor() {
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
    if(method.getDeclaringClass() != Object.class
    && method.getReturnType() == String.class) {
    return "Hello cglib!";
    } else {
    return proxy.invokeSuper(obj, args);
    }
    }
    });
    return (SampleClass) enhancer.create();
    }
    }
    //客户端和原来的一样,只需要把类名换一换就可以了

    MethodInterceptor可以让我们对被拦截的方法进行完全控制,并提供了一些工具用于我们调用被增强类SampleClass的原始方法(而不是被代理过后的方法)。但是,为什么需要使用原始方法呢?因为直接使用原始方法的效率更高,而cglib框架一般会被用于一些非常注重效率的框架中。MethodInterceptor的创建和链入需要生成额外的字节码(类)(byte code),同时会创建一些InvocationHandler不会创建的运行时对象(与InvocationHandler对比,性能更差)。由于此种原因,cglib还提供了另外的一些callback实现:

  • LazyLoader: 尽管LazyLoader仅有的一个方法与FixedValue的方法一样,但是LazyLoader与FixedValue从根本上还是不一样的。LazyLoader假设其返回的是增强子类的实例,这个实例仅在第一次访问其方法时才返回,然后调用者会缓存该实例并用于后续调用,这个特性可以用于对象的创建需要耗费大量资源的场景。需要注意的是,不管是proxy对象(FixedValue)还是lazy load对象(LazyLoader),都只能使用被增强类的构造方法来创建对象。因此,请确保被增强类拥有一个不耗时的构造方法,或者被增强类实现了接口,你可以通过使用Enhancer#create(Object…)方法来选择使用的构造方法。

  • Dispatcher: 与LazyLoader的功能相似,但是Dispatcher会在每个方法调用时都创建实例。这个特性适用于:在不改变引用对象的情况下,切换其实现类。同样需要注意的是,对象的创建只会使用到被增强类的构造方法。

  • ProxyRefDispatcher: 这个callback的方法需要一个输入参数,该输入参数为增强后的类的实例。这就允许将一个方法的调用代理到另外一个方法上去,这种使用方式很容易导致无限循环,特别是当在ProxyRefDispatcher#loadObject(Object)方法中始终调用同一个方式时必然会导致无限循环。其实ProxyRefDispatcher与Dispatcher类似,只不过多个一个输入参数而已,该输入参数就是proxy。

  • NoOp: 不像该类的名字所暗示的那样(什么都不做),该callback直接将方法的调用代理到原始类,不会增加任何额外特性。

现在看来,最后两个callback可能不会引起你的注意。什么场景下才会出现将一个类增强之后,还需要使用没增强的方法呢?你是对的,这两个callback一般只会与CallbackFilter结合起来使用,
CallbackFilter主要用于筛选方法是否进行拦截,下面是事例:

public class CallbackFilterDemo {
public static SampleClass GetProxy()
{
Enhancer enhancer = new Enhancer();
//CallbackHelper实现了CallbackFilter
//需要传入增强的类,和类实现的接口
CallbackHelper callbackHelper = new CallbackHelper(SampleClass.class,SampleClass.class.getInterfaces()) {
@Override
protected Object getCallback(Method method) {
if(method.getDeclaringClass() != Object.class
&& method.getReturnType() == String.class) {
return new FixedValue() {
@Override
public Object loadObject() throws Exception {
return "Hello cglib!";
}
};
} else {
return NoOp.INSTANCE; // A singleton provided by NoOp.
}
}
};
enhancer.setSuperclass(SampleClass.class);
enhancer.setCallbackFilter(callbackHelper);
enhancer.setCallbacks(callbackHelper.getCallbacks());
return (SampleClass) enhancer.create();
}
}
//客户端调用与之前类似

Enhancer的方法Enhancer#setCallbackFilter(CallbackFilter)接收一个CallbackFilter参数,这个方法期望被增强类的方法调用都被映射到一个Callback实例集合中去。该集合中能包含处理被增强类所有方法的Callback。当调用proxy的方法时,Enhancer会根据CallbackFilter的算法选择出合适的Callback,然后将强求转发到Callback。为了让CallbackFilter的创建不那么费劲,cglib提供了一个帮助类CallbackHelper。该类实现了CallbackFilter,且提供了可以直接为你创建Callback实例集合的方法。上面事例中的proxy功能等同于MethodInterceptor事例中的proxy,但是CallbackFilter允许你将编写自定义Callback逻辑与分发逻辑分开编写(解耦)。

How does it work?

当Enhancer创建一个增强类时,它会为每一个Callback创建一个private static变量,且该操作是在被代理类创建之后执行的。这就意味着,cglib创建的类不能被复用,因为注册的callback不会成为类定义的一部分,而只是cglib在JVM加载类之后手动添加的。同时,从技术层面来说由cglib创建的类在初始化后是还未达到ready状态的,比如该类不能通过网络发送到另一台机器,因为在目标机器上,该类可能并不存在(目标机器就算同样算法生成了类,但是类名还是可能不一样)。
对于不同的Callback类,cglib可能会注册不同的额外变量。比如,MethodInterceptor就会注册两个private static变量(一个用于保存Method的反射,另一个是MethodProxy的反射)到代理的每个方法中。需要注意的是,MethodProxy会过渡使用FastClass,而FastClass的创建会触发额外的类的创建,后面将会详细介绍FastClass。
由于以上所有原因,请在使用Enhancer的时候多多注意。在注册callback的时候需要格外小心,比如MethodInterceptor会额外创建一些类,同时还会在增强类中注册额外的static变量。这在将callback保存为静态变量的时,尤为危险:这可能会隐式的导致从不会对增强类进行垃圾回收(除非是它的Classloader被回收)。另外一种危险的情况是,使用匿名类时,会使用到其外层类的引用。回想一下上面的事例:

@Test
public void testFixedValue() throws Exception {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(SampleClass.class);
enhancer.setCallback(new FixedValue() {
@Override
public Object loadObject() throws Exception {
return "Hello cglib!";
}
});
SampleClass proxy = (SampleClass) enhancer.create();
assertEquals("Hello cglib!", proxy.test(null));
}

这个FixedValue的匿名子类,将会变得很难从被增强类SampleClass中进行引用(因为在GC中判断一个类是不是需要被销毁,需要进行可达性分析,因为Callback是静态的,所以从上倒下就会一直有引用,大概是这个意思),因此这个匿名的子类以及包含这个@Test方法的类将永远不会被垃圾回收,这将会导致严重的内存泄露。因此,不要在cglib中使用非静态内部类(本章实例只是实例,是为了让代码更简洁)
最后,千万不要拦截Object#finalize()方法!由于cglib是通过子类方式实现的代理,所以finalize方法会被覆盖,但是覆盖finalize通常不是个好主意。这些拦截了finalize方法的增强类实例不会被垃圾回收器特别对待,同样会被放入JVM的finalization队里。如果你不小心在callback中硬编码应用了被增强类,那么你就创建了一个永远不被回收的实例。以上问题通常是你不希望发生的。庆幸的是,cglib不会代理所有的final方法,因此Object#wait,Object#notifyObject#notifyAll方法不会遇到这些问题。需要注意的是Object#clone是会被代理的,这通常是你不希望发生的。

Immutable Bean

cglib库的ImmutableBean允许创建一个不可变的包装器,该特性与Collections#immutableSet(不可变集合)类似。所有要修改被包装Bean的操作都会被阻止,并且抛出IllegalStateException异常(不是使用的java API建议的UnsupportedOperationException)。让我们来看一些Bean:

public class SampleBean {
private String value;
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}

让我们来将这个Bean变成immutable不可变的:

public class testImmutableBean {
public static SampleBean getProxy()
{
SampleBean sampleBean = new SampleBean();//创建一个bean
sampleBean.setValue("Hello world!");
SampleBean immutableBean = (SampleBean) ImmutableBean.create(sampleBean);
return immutableBean;
}
}
//客户端获取进行修改
public class Client {
public static void main(String[] args) {
SampleBean sampleBean = testImmutableBean.getProxy();
sampleBean.setValue("aa");//java.lang.IllegalStateException: Bean is immutable
}
}

从上面的事例可以明显看出,Immutable bean阻止了对bean的任何状态的修改,并且会抛出IllegalStateException异常。然而,通过原始的bean修改是可行的,通过所有对原始bean的修改会反映到Immutable bean中。

Bean Generator

Bean Generator是cglib提供的另一个强大的工具类,它可以实现在运行时动态的创建bean:

public class BeanGeneratorDemo {
public static void testBeanGenerator() throws Exception
{
BeanGenerator beanGenerator = new BeanGenerator();
beanGenerator.addProperty("value",String.class);
Object myBean = beanGenerator.create();

Method setter = myBean.getClass().getMethod("setValue",String.class);
setter.invoke(myBean,"Hello Cglib");
Method getter = myBean.getClass().getMethod("getValue");
System.out.println(getter.invoke(myBean));
}
}
//客户端调用就会输出Hello Cglib

从上面的事例可以明显看出,BeanGenerator会首先通过addProperty方法设置属性名以及属性类型,在创建阶段,BeanGenerator会自动为属性创建gettersetter

  • <type> get<name>()
  • void set<name>(<type>)

当另一个库期望通过反射解析到bean,但你在运行时不知道这些bean时,这可能很有用(这种场景的一个实例是Apache Wicket)。

Bean Copier

Bean Copier是另外一个工具类,这个工具类可以复制bean的属性。假设现在有一个与SampleBean拥有相同属性的bean:

public class OtherSampleBean {
private String value;
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}

现在你就可以通过这个工具类拷贝这两个bean的属性值了:

@Test
public void testBeanCopier() throws Exception {
BeanCopier copier = BeanCopier.create(SampleBean.class, OtherSampleBean.class, false);
SampleBean bean = new SampleBean();
myBean.setValue("Hello cglib!");
OtherSampleBean otherBean = new OtherSampleBean();
copier.copy(bean, otherBean, null);
assertEquals("Hello cglib!", otherBean.getValue());
}

这种拷贝不受两个bean的类型限制。BeanCopier#copy方法可以传递一个可选的参数,这个参数可以用于更加细粒度的控制属性拷贝逻辑。如果BeanCopier在通过create方法创建时,第三个参数传递的false。则在调用copy方法时,会忽略第三个参数,所以可以直接传null。

Bulk Bean

BulkBean可以通过数组指定Bean的一组访问方法来对属性进行批量操作。

@Test
public void testBulkBean() throws Exception {
BulkBean bulkBean = BulkBean.create(SampleBean.class,
new String[]{"getValue"}, // 传一组getter
new String[]{"setValue"}, // 传一组setter
new Class[]{String.class}); // 指定这组属性相应的类型
SampleBean bean = new SampleBean();
bean.setValue("Hello world!");
// getPropertyValues会返回这一组属性
assertEquals(1, bulkBean.getPropertyValues(bean).length);
assertEquals("Hello world!", bulkBean.getPropertyValues(bean)[0]);
bulkBean.setPropertyValues(bean, new Object[] {"Hello cglib!"});
assertEquals("Hello cglib!", bean.getValue());
}

BulkBean需要一个getter数组、一个setter数组和一个相应属性类型的数组来作为构造参数。然后可以通过BulkBean#getPropertyValues(Object)方法提取出一个属性数组。相应地,可以通过BulkBean#setPropertyValues(Object, Object[])方法设置一组属性。

Bean Map

这是cglib库中的最后一个bean的工具类,这个工具类会将bean的所有属性转换为以String为key,Object为值的Map:

@Test
public void testBeanGenerator() throws Exception {
SampleBean bean = new SampleBean();
BeanMap map = BeanMap.create(bean);
bean.setValue("Hello cglib!");
assertEquals("Hello cglib", map.get("value"));
}

另外,可以使用BeanMap#newInstance(Object)方法,在不重新创建BeanMap实例的情况下转换另外的bean实例。但是,另外的bean必须与创建BeanMap时指定的Bean是同一个Class实例。

Key Factory

KeyFactory可以用来动态创建key,这些key可以由多个值组成。为了达到这个目的,KeyFactory需要一个接口,该接口用于定义组成key需要的值。这个接口需要有一个方法,方法名必须为newInstance,返回值必须为Object实例,比如:

public interface SampleKeyFactory {
Object newInstance(String first, int second);
}

有了接口后,就可以创建key的实例了:

@Test
public void testKeyFactory() throws Exception {
SampleKeyFactory keyFactory = (SampleKeyFactory) KeyFactory.create(Key.class);
Object key = keyFactory.newInstance("foo", 42);
Map<Object, String> map = new HashMap<Object, String>();
map.put(key, "Hello cglib!");
assertEquals("Hello cglib!", map.get(keyFactory.newInstance("foo", 42)));
}

KeyFactory会确保正确实现Object#equals(Object)Object#hashCode方法,因此生成的key可以直接用于Map和set中。在cglib库内部,KeyFactory也是被频繁使用的。

Mixin

有些人可能已经从其他编程语言(如Ruby或Scala)中了解了Mixin类的概念,cglib中的Mixins允许将多个对象组合成一个对象。但是,这些对象必须要实现接口:

public interface Interface1 {
String first();
}

public interface Interface2 {
String second();
}

public class Class1 implements Interface1 {
@Override
public String first() {
return "first";
}
}

public class Class2 implements Interface2 {
@Override
public String second() {
return "second";
}
}

现在可以通过附加接口将类Class1和Class2组合到单个类中:

public interface MixinInterface extends Interface1, Interface2 { 
/* empty */
}

@Test
public void testMixin() throws Exception {
Mixin mixin = Mixin.create(
new Class[]{Interface1.class, Interface2.class, MixinInterface.class},
new Object[]{new Class1(), new Class2()}
);
MixinInterface mixinDelegate = (MixinInterface) mixin;
assertEquals("first", mixinDelegate.first());
assertEquals("second", mixinDelegate.second());
}

不可否认,上面事例中的Mixin API相当笨拙,因为需要额外的定义MixinInterface接口,这个问题可以通过非检测的Java来解决(这里不是很理解什么意思)

String Switcher

String Switcher可以将String模拟成int:

@Test
public void testStringSwitcher() throws Exception {
String[] strings = new String[]{"one", "two"};
int[] values = new int[]{10, 20};
StringSwitcher stringSwitcher = StringSwitcher.create(strings, values, true);
assertEquals(10, stringSwitcher.intValue("one"));
assertEquals(20, stringSwitcher.intValue("two"));
assertEquals(-1, stringSwitcher.intValue("three"));
}

StringSwitcher可以模拟switch的分支逻辑,就像java7或者更高版本已经内建的swtich一样。如果在Java 6或更少的版本中使用StringSwitcher确实会给代码带来一些好处,但这是值得怀疑的,我个人是不建议使用的。

Interface Maker

就像InterfaceMaker的名字描述的一样,这个类允许我们动态的创建Interface:

@Test
public void testInterfaceMaker() throws Exception {
Signature signature = new Signature("foo", Type.DOUBLE_TYPE, new Type[]{Type.INT_TYPE});
InterfaceMaker interfaceMaker = new InterfaceMaker();
interfaceMaker.add(signature, new Type[0]);
Class iface = interfaceMaker.create();
assertEquals(1, iface.getMethods().length);
assertEquals("foo", iface.getMethods()[0].getName());
assertEquals(double.class, iface.getMethods()[0].getReturnType());
}

与其他的cglib库的public API不同的是,interface maker依赖于ASM的类型。在一个运行的程序中创建interface是几乎没有意义的,因为一个interface仅仅代表着一个类型,一般是在编译器的进行类型检测时使用。当然,如果是你是要将生成的代码用于后续的开发,还是有一些用处的。

Method Delegate

MethodDelegate允许通过将方法调用绑定到某个接口来模拟类似于c#的方法委托,例如,下面的代码将SampleBean#getValue方法绑定到委托:

public interface BeanDelegate {
String getValueFromDelegate();
}

@Test
public void testMethodDelegate() throws Exception {
SampleBean bean = new SampleBean();
bean.setValue("Hello cglib!");
BeanDelegate delegate = (BeanDelegate) MethodDelegate.create(
bean, "getValue", BeanDelegate.class);
assertEquals("Hello world!", delegate.getValueFromDelegate());
}

然而,有一些事情需要注意:

  • 工厂方法MethodDelegate#create只接受一个方法名称作为其第二个参数,这个参数是MethodDelegate将为您代理的方法;
  • MethodDelegate#create的第一个参数,必须是一个包含无参方法的对象实例,由此可以看出MethodDelegate并没有达到应该有的强大程度;
  • 第三个参数必须是一个interface,且该interface有且只能有一个方法(与原文不一样,原文写的是有且仅有一个参数,可能是原文笔者笔误吧~~)。MethodDelegate会实现这个接口,并且可以将create的实例强制转换为该接口。当这个代理接口的方法被调用的时候,在其内部会将调用代理到第一个参数的方法上。

除此之外,我们还可以考虑下MethodDelegate的缺点:

  • cglib会为每个代理创建一个新的类,最终会导致你的永久代(保存类定义的内存空间,java8之后不再将类定义保存在永久代,但同样会影响到整机的内存空间)内存空间的浪费;
  • 不能代理包含参数的方法;
  • 如果你的接口方法包含参数,Method Deletegate将不会正常的工作,但是它也不会抛出任何的异常信息(方法的返回值永远都是null)。如果你的接口方法的返回类型与被代理的方法不一致(即便是被代理方法返回对象的父类),你将会收到一个IllegalArgumentException异常。

Multicast Delegate

尽管MulticastDelegate与MethodDelegate要解决的目标功能是一致的,但是二者之间的工作方式还是会有一些区别的。为了使用MulticastDelegate,我们的对象需要实现一个接口:

public interface DelegatationProvider {
void setValue(String value);
}

public class SimpleMulticastBean implements DelegatationProvider {
private String value;
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}

基于这个实现了DelegateProvider接口的Bean,我们就可以创建一个DelegateProvider。这个DelegateProvider会将所有调用setValue(String)方法的请求,分发到多个实现了DelegateProvider接口的类:

@Test
public void testMulticastDelegate() throws Exception {
MulticastDelegate multicastDelegate = MulticastDelegate.create(
DelegatationProvider.class);
SimpleMulticastBean first = new SimpleMulticastBean();
SimpleMulticastBean second = new SimpleMulticastBean();
multicastDelegate = multicastDelegate.add(first);
multicastDelegate = multicastDelegate.add(second);
DelegatationProvider provider = (DelegatationProvider)multicastDelegate;
provider.setValue("Hello world!");
assertEquals("Hello world!", first.getValue());
assertEquals("Hello world!", second.getValue());
}

同样,MulticastDelegate也有一些缺点:

  • 所有的对象都需要实现一个包含一个方法的接口,这对于第三方库来说很糟糕,当你想要使用CGlib实现一些隐藏性的操作时,是不可行的,实现这些操作的代码就会暴露到正常的代码中。其实,你可以自己轻松的实现这种代理方式(尽管没有使用到byte code框架,但是我猜你自己实现会更好)。

  • 当被代理方法需要返回数据时,你只能拿到最后一个对象返回的数据,其他对象返回的数据都会丢失(但是可以在某些点被multicast delegate检测到)。

Constructor Delegate

ConstructorDelegate允许创建一个以字节为单位的工厂方法,要使用ConstructorDelegate首先需要一个接口,这个接口必须包含一个名称为newInstance的方法,该方法的返回值必须为Object,这个方法可以包含任意多个参数(参数个数和类型与需要代理的构造方法相同)。例如,为了为SampleBean创建ConstructorDelegate,我们需要以下内容来调用SampleBean的默认(无参数)构造函数:

public interface SampleBeanConstructorDelegate {
Object newInstance();
}

@Test
public void testConstructorDelegate() throws Exception {
SampleBeanConstructorDelegate constructorDelegate = (SampleBeanConstructorDelegate) ConstructorDelegate.create(
SampleBean.class, SampleBeanConstructorDelegate.class);
SampleBean bean = (SampleBean) constructorDelegate.newInstance();
assertTrue(SampleBean.class.isAssignableFrom(bean.getClass()));
}

Parallel Sorter

在排序数组数组时,ParallelSorter声称是Java标准库的数组排序器的更快替代品:

@Test
public void testParallelSorter() throws Exception {
Integer[][] value = {
{4, 3, 9, 0},
{2, 1, 6, 0}
};
ParallelSorter.create(value).mergeSort(0);
for(Integer[] row : value) {
int former = -1;
for(int val : row) {
assertTrue(former < val);
former = val;
}
}
}

ParallelSorter创建时使用的是二维数组(使用二维数组主要是为了将要排序的数组作为一个集合统一传入),然后就可以第二级数组(子数组)进行归并排序和快速排序。然而,在使用时你需要注意:

  • 当对原始类型进行排序时,你只能使用mergeSort方法的重载方法,手动指定排序范围(比如:e.g. ParallelSorter.create(value).mergeSort(0, 0, 4), 其中4表示排序数组的长度),否则ParallelSorter出现一个明显的bug,因为ParallelSorter(在获取最大index时)会将强制转换为Object数组,导致ClassCastException异常;
  • 如果被排序的数组的长度不一致,mergeSort的第一个参数会决定采用哪一行的长度作为参考长度(译者注:3.2.10版本不是这样的,它始终使用第一行作为长度参考)。比参考长度短的行会导致ArrayIndexOutOfBoundException异常,比参考长度长的行超出位置的数字不会被排序。

Fast Class and Fast Members

FastClass承诺要成为一个比Java reflection API更快速的工具类,它包装一个Class,并提供与Java reflection API相同API:

@Test
public void testFastClass() throws Exception {
FastClass fastClass = FastClass.create(SampleBean.class);
FastMethod fastMethod = fastClass.getMethod(SampleBean.class.getMethod("getValue"));
MyBean myBean = new MyBean();
myBean.setValue("Hello cglib!");
assertTrue("Hello cglib!", fastMethod.invoke(myBean, new Object[0]));
}

除了FastMethod外,还可以创建FastConstructor,但是不能创建fast field。但是,FastClass是怎么样实现的以至于比Java reflection API快呢?Java reflection是通过JNI(Java Native Interface)执行的,JNI会调用C语言的代码执行反射方法,而FastClass是通过生成一些字节码直接在JVM中调用的。。然而,新版本的HotSpot JVM(或许还有其他的现代JVM)会有一个叫做inflation的概念,当使用JNI调用超过一定次数后会生成一个本地版本的FasClass字节码。你通过属性sun.reflect.inflationThreshold设置这个次数(默认为15次),以控制jvm的inflation行为(至少在HotSpot JVM中)。这个属性决定了在执行多少次JNI调用后会在本地生成字节码。我建议在现代的JVM中不再使用FastClass,但是在老版本的JVM中可以用来优化性能。

cglib Proxy

就像本文开头个所说,cglib Proxy是Java Proxy的重新实现版本。开发这个库的本意是想要在Java 1.3之前的版本中使用Java库的proxy,当时的cglib Proxy与Java Proxy仅有很少的细节上有区别。在Java Standard库的javadoc中有关于Java Proxy很好的文档,在此我将省略对其的细节讨论。

最后的警告

在概述了cglib的功能后,我想要在最后说一句警告的话。cglib在生成字节码时还会额外的生成一些类,这些类都会被保存在JVM的一块特殊内存中:所谓的perm space。就像他的名字一样,这块永久的内存空间适用于保存永久对象的,这些对象将不会被垃圾回收器回收。然后,这也不是完全正确的:当一个类被加载(load)后,如果加载它的ClassLoader没有准备好进行垃圾回收,它就不会被卸载(unload)。ClassLoader被回收的唯一场景是,这个ClassLoader不是JVM系统的ClassLoader而是自定义的(程序创建的)ClassLoader。这种ClassLoader如果自己准备好,且它加载的所有类以及这些类的实例都准备好回收了,垃圾回收器才会真正的回收。这就意味着,如果你在Java程序中创建越来越多的类,并且不认证考虑移除内存中的这些类,你迟早会将perm space耗尽,最终程序死在OutOfMemoryError之手。因此,请谨慎使用cglib。但是,如果你明知且谨慎的使用cglib,你将可以做很多纯Java程序不能做的奇妙的事情。
最后,当你创建依赖于cglib的项目的时候,你需要注意到一个事实:cglib项目没有得到应有的管理和开发积极性(考虑到它的流行程度来说)。缺少文档就是这么说的第一个证据,第二个就是它经常出现的凌乱的public接口。发布到Maven中心仓库也存在不好的地方,邮件列表就像是垃圾邮件一样,它的版本迭代也是相当的不稳定。因此你可能需要了解一下javassist,一个真正可以替代cglib的库(但是功能较cglib弱)。Javassist附带了一个伪Java编译器,这样就可以在不清楚Java字节码的情况下创建不可思议的字节码增强了。如果你想要亲力亲为,你可能更喜欢ASM(cglib就是基于这个构建的),ASM不管是库还是Java代码又或者是字节码,都有强大的文档支持。
需要注意的是,本文中的所有事例都只能运行在cglib 2.2.2中,与新的3.x的版本不兼容。不幸的是,我体验了cglib的最新版本,但是它偶尔会生成一些无效的字节码,所以我现在在生产环境中使用的还是老版本。另外一个需要注意的问题是,大多数使用cglig的项目都将cglib转移到了他们自己的namespace下,以防止与其他依赖包的版本冲突,比如Spring project。建议你在使用cglib的时候也这么做,很多工具可以帮助你自动完成这一最佳实践,比如jarjar

关于Cglib的文章

文章作者: zenshin
文章链接: https://zlh.giserhub.com/2020/05/01/java/cglib/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 zenshin's blog
打赏
  • 微信
    微信
  • 支付宝
    支付宝

评论