CGLib手册(CGLib: The Missing Manual)

本文翻译自:https://dzone.com/articles/cglib-missing-manual,如果觉得有帮助,请为作者点赞!

作为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来代理目标类,并将目标类的所有方法都覆盖成空方法(同时会添加一些跟踪逻辑)。
与ASM库(另外一个byte code库,cglib基于ASM提供一些高级别的byte code操作功能)不同,cglib提供了更高级别的byte code操作,可以让用户在不了解任何Java类编译原理的情况下使用。很不幸的是,cglib的文档相当的短,甚至可以说相当于没有。除了一篇2005年写的介绍Enhance类的博客之外,没有更多的可以找到的资料了(译者注:原文写于2014年7月,可能当时cglib的使用者较少),这篇博客将尝试着对cglib以及其API进行说明和事例演示。

Enhancer

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

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

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

@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));
}

在上面的事例中,enhancer将会返回一个增强的SampleClass的子类的实例,该实例会拦截所有方法的调用,从而直接返回callback中定义的返回值Hello cglib!。该增强实例是通过Enhancer#create(Object…)方法创建的(译者注:在译者使用的版本3.2.10中,该方法已经不存在),该方法可输入多个参数,这些参数会被用于调用SampleClass的构造方法(尽管构造方法在字节码级别仅仅是普通的java方法,但是Enhancer不会增强(代理)构造方法。同时Enhancer也不能增强static或者final关键字定义的class)。如果你只需要创建一个增强class,而不需要创建其实例,你可以使用Enhancer#createClass方法,当创建了增强class之后,你就可以使用它来动态创建实例了。增强class将包含SampleClass的所有构造方法,并且会将构造方法的调用都代理到SampleClass
需要注意的是,SampleClass的所有方法的调用都会被代理,包括从java.lang.Object继承而来的方法。所以,调用proxy.toString()同样会返回”Hello cglib!”。这就会导致调用proxy.hashCode()方法抛出ClassCastException异常,因为调用proxy.hashCode()会被代理并返回”Hello cglib!”这个字符串类型的值,但是Object#hashCode方法却需要一个int类型的值。
通过观察可以做出另外一个推断:final方法是不会被代理(拦截)的,比如,Object#getClass会返回一个类似于”SampleClass$$EnhancerByCGLIB$$e277c63c“的类名。这个类名是cglib随机生成的,目的是为了避免生成的类名冲突。cglib生成的类与被增强的类包名相同,这样就可以实现覆盖package-private方法了。与不能增强(代理)的final方法类似,通过生成子类实现增强的方案不能增强(代理)final类。所以,对于Hibernate这样的框架,不能持久化final类。
下面,让我们来使用更强大的callback——InvocationHandler:

@Test
public void testInvocationHandler() throws Exception {
  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.");
      }
    }
  });
  SampleClass proxy = (SampleClass) enhancer.create();
  assertEquals("Hello cglib!", proxy.test(null));
  assertNotEquals("Hello cglib!", proxy.toString());
}

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

@Test
public void testMethodInterceptor() throws Exception {
  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 {
        proxy.invokeSuper(obj, args);
      }
    }
  });
  SampleClass proxy = (SampleClass) enhancer.create();
  assertEquals("Hello cglib!", proxy.test(null));
  assertNotEquals("Hello cglib!", proxy.toString());
  proxy.hashCode();// Does not throw an exception or result in an endless loop.
}

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

  • LazyLoader:尽管LazyLoader仅有的一个方法与FixedValue的方法一样,但是LazyLoaderFixedValue从根本上还是不一样的。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结合起来使用,下面是事例:

@Test
public void testCallbackFilter() throws Exception {
  Enhancer enhancer = new Enhancer();
  CallbackHelper callbackHelper = new CallbackHelper(SampleClass.class, new Class[0]) {
    @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(MyClass.class);
  enhancer.setCallbackFilter(callbackHelper);
  enhancer.setCallbacks(callbackHelper.getCallbacks());
  SampleClass proxy = (SampleClass) enhancer.create();
  assertEquals("Hello cglib!", proxy.test(null));
  assertNotEquals("Hello cglib!", proxy.toString());
  proxy.hashCode(); // Does not throw an exception or result in an endless loop.
}

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?这部分涉及到cglib的实现原理,翻译难度较大,建议查看原文。

当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中进行引用(译者注:不太明白这句的意思),因此这个匿名的子类以及包含这个@Test放到的类将永远不会被垃圾回收,这将会导致严重的内存泄露。因此,不要在cglib中使用非静态类(我在这篇博客中使用匿名类仅仅是为了让事例更短小些)。
最后,千万不要拦截Object#finalize()方法!由于cglib是通过子类方式实现的代理,所以finalize方法会被覆盖,但是覆盖finalize通常不是个好主意(译者注:链接文章建议不要使用finalize方法)。这些拦截了finalize方法的增强类事例不会被垃圾回收器特别对待,同样会被放入JVM的finalization队里。如果你不小心在callback中硬编码应用了被增强类,那么你就创建了一个永远不被回收的实例。以上问题通常是你不希望发生的。庆幸的是,cglib不会代理所有的final方法,因此Object#wait, Object#notify和Object#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变成不可变:

@Test(expected = IllegalStateException.class)
public void testImmutableBean() throws Exception {
  SampleBean bean = new SampleBean();
  bean.setValue("Hello world!");
  SampleBean immutableBean = (SampleBean) ImmutableBean.create(bean);
  assertEquals("Hello world!", immutableBean.getValue());
  bean.setValue("Hello world, again!");
  assertEquals("Hello world, again!", immutableBean.getValue());
  immutableBean.setValue("Hello cglib!"); // Causes exception.
}

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

Bean Generator

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

@Test
public 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");
  assertEquals("Hello cglib!", getter.invoke(myBean));
}

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

  • <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可以将繁琐逐个属性的方法调用,变成一个接口操作所有属性的access方法,首先来看一个Bean:

public class MultiFieldBean {
    private String name;
    private String address;
    private Integer age;

    public MultiFieldBean() {
    }

    public MultiFieldBean(String name, String address, int age) {
        this.name = name;
        this.address = address;
        this.age = age;
    }
    // 省略getter和setter
}

现在我们就可以使用BulkBean来操作MultiFieldBean了:

    @Test
    public void bulkBeanTest() {
        BulkBean bulkBean = BulkBean.create(MultiFieldBean.class,
                new String[]{"getName", "getAge", "getAddress"},
                new String[]{"setName", "setAge", "setAddress"},
                new Class[]{String.class, Integer.class, String.class}
        );
        MultiFieldBean bean = new MultiFieldBean("mallen", "chengdu", 18);
        Object[] values = bulkBean.getPropertyValues(bean);
        assertEquals("mallen", values[0]);
        assertEquals(18, values[1]);
        assertEquals("chengdu", values[2]);
        // 设置属性
        bulkBean.setPropertyValues(bean, new Object[]{"mallen1", 19, "chengdu1"});
        values = bulkBean.getPropertyValues(bean);
        assertEquals("mallen1", values[0]);
        assertEquals(19, values[1]);
        assertEquals("chengdu1", values[2]);
    }

BulkBean创建时需要使用到一个getter数组、一个setter数组和一个属性类型的数组,创建后的代理对象就可以使用getPropertyValues和setPropertyValues方法操作属性了。可以明显的看出,这两个方法操作的数组中的属性顺序与创建BulkBean时的顺序是严格一致的。

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来解决(译者注:可能是说类型强制转换,直接将mixin转换为Interface1或者Interface2)。

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异常,比参考长度长的行超出位置的数字不会被排序。

就我个人而言,我怀疑ParallelSorter是否真的在排序时间上有优势。诚然,我还没有尝试对它进行基准测试。如果你尝试过,我很乐意在评论中听到你的回复。

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

作者:雪落孤村
链接:https://www.jianshu.com/p/cdbdf3da2b7e
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享