CGLib(Code Generation Library) 是一个强大、高效,以及高质量的字节码生成库,能够在运行时动态生成字节码,从而实现一些比较极客的功能。CGLib 被许多开源软件采用,我们在写一些基础库时也很青睐,但是官方给到的文档比较简单,所以本文参考 CGLib: The missing manual ,并结合自己的使用经验,总结了一个中文版本。
在正式开始之前,先给出 CGLib 在使用上的一些限制:
无法代理 final 类(受制于继承机制,final 类不允许继承)。
无法代理非静态(non-static)内部类。
创建代理类对象时,需要被代理类提供相应参数签名的构造方法。
创建代理相对于 java 原生动态代理性能较低,但是运行性能更高。
本文示例运行版本:3.2.x
动态代理:Enhancer
本小节所有示例都基于 SampleClass 进行,以该类作为被代理类,定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public class SampleClass { public SampleClass () { System.out.println(this .getClass().getSimpleName() + ": default construct method" ); } public SampleClass (String name) { System.out.println(this .getClass().getSimpleName() + ": construct method : " + name); } public String hello (String name) { System.out.println(this .getClass().getSimpleName() + ": hello method" ); return "Hello, " + name; } public final String finalMethod (String input) { System.out.println(this .getClass().getSimpleName() + ": final method" ); return "final method: " + input; } public static String staticMethod (String input) { System.out.println(SampleClass.class.getName() + ": static method" ); return "static method: " + input; } private String privateMethod (String input) { System.out.println(this .getClass().getSimpleName() + ": private method" ); return "private method: " + input; } }
FixedValue:替换实例方法返回值
FixedValue 用于替换被代理类方法的返回值(final/static 方法除外),假设我们希望替换 SampleClass#hello
方法的返回值,基于 CGLib 的 FixedValue 可以实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 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(); Assert.assertEquals("Hello, cglib!" , proxy.hello(null ));
如果我们在创建代理对象时指定了参数(如下),则要求 SampleClass 必须具备相同参数签名的构造方法,这里要求 SampleClass 必须具备仅有一个 String 类型参数的构造方法:
1 SampleClass proxy = (SampleClass) enhancer.create(new Class[] {String.class}, new Object[] {"zhenchao" });
注意 :代理对象的 toString 方法也会被代理,所以 toString 返回的也是 Hello, cglib!
。同理 hashCode 方法也会被代理,所以也会返回 Hello, cglib!
字符串,但是因为 hashCode 方法的返回类型是 int,所以会抛出 ClassCastException 异常。
此外,final 方法和 static 方法不会被代理。
InvocationHandler:拦截方法时提供获取方法的相关信息
FixedValue 仅允许我们修改实例方法的返回值,但是对于当前调用方法的相关信息我们无从了解,InvocationHandler 则以参数的方式将这些信息传递给我们,几乎等同于 jdk 自带的 java.lang.reflect.InvocationHandler
,两者都定义了 invoke 方法,且在方法签名上是一致的,使用示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(SampleClass.class); enhancer.setCallback(new InvocationHandler() { @Override public Object invoke (Object proxy, Method method, Object[] args) throws Throwable { System.out.println("proxy: " + proxy.getClass().getSimpleName()); System.out.println("method: " + method.getName()); System.out.println("args: " + Arrays.toString(args)); System.out.println("invoke method: " + method.getName()); return "Hello, cglib!" ; } }); SampleClass proxy = (SampleClass) enhancer.create(); Assert.assertEquals("Hello, cglib!" , proxy.hello("zhenchao" ));
InvocationHandler 接口的定义如下,参数 proxy 是 CGLib 生成的代理类对象,method 即为我们当前调用的方法签名,args 是我们传递给方法的参数列表。
1 2 3 public interface InvocationHandler extends Callback { public Object invoke (Object proxy, Method method, Object[] args) throws Throwable ; }
对于本次调用来说这三个参数值如下,其中代理对象对应的类名是 CGLib 在 SampleClass 类型的基础上随机生成的,以防止与系统中已有的类重名:
1 2 3 proxy: SampleClass$$EnhancerByCGLIB$$a01c21b9 method: hello args: [zhenchao]
注意 :我们在 invoke 中调用 proxy 的一般方法会循环触发 invoke 方法(包括执行 method.invoke(proxy, args)
),导致无穷循环,最终导致栈溢出,为了解决这一问题我们可以使用 MethodInterceptor 代替。
MethodInterceptor:InvocationHandler 的增强版本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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("obj: " + obj.getClass().getSimpleName()); System.out.println("method: " + method.getName()); System.out.println("args: " + Arrays.toString(args)); System.out.println("proxy: " + proxy.getSuperName()); return proxy.invokeSuper(obj, args); } }); SampleClass proxy = (SampleClass) enhancer.create(); Assert.assertEquals("Hello, zhenchao" , proxy.hello("zhenchao" ));
MethodInterceptor 接口的定义如下,其中 obj 是 CGLib 生成的代理类对象,等价于 InvocationHandler 中的 proxy,而 MethodInterceptor 中的 proxy 则是用来触发被代理类对应的方法(即被代理方法),我们可以触发任意次,而不会造成无穷循环。
1 2 3 public interface MethodInterceptor extends Callback { public Object intercept (Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable ; }
LazyLoader:仅在第一次调用方法时创建代理对象
LazyLoader 在方法签名上与 FixedValue 一致,但是在方法返回上却大相径庭,前面介绍了 FixedValue 用于替换方法的返回值,而 LazyLoader 则是返回一个代理对象,该代理对象由开发者自己指定和创建,并且 仅在第一次调用被代理方法时创建代理对象 ,后面的使用都会复用该对象:
1 2 3 4 5 6 7 8 9 10 11 12 Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(SampleClass.class); enhancer.setCallback(new LazyLoader() { @Override public Object loadObject () throws Exception { return new SampleClass(); } }); SampleClass proxy = (SampleClass) enhancer.create(); for (int i = 0 ; i < 3 ; i++) { Assert.assertEquals("Hello, zhenchao" , proxy.hello("zhenchao" )); }
上面的示例我们循环调用了 3 次被代理方法,由下面的输出可以看到只有在第一次调用被代理方法时触发创建了代理对象,这里需要注意的时被代理对象的构造方法被调用了两次,一次是 CGLib 创建代理对象时调用的,一次是我们 new SampleClass()
时调用的。
1 2 3 4 5 SampleClass$$EnhancerByCGLIB$$a6d4c46: default construct method SampleClass: default construct method SampleClass: hello method SampleClass: hello method SampleClass: hello method
Dispatcher: 每次调用被代理方法时都会创建代理对象
Dispatcher 的作用与 LazyLoader 相同,区别在于每次调用被代理方法时都会创建返回新的代理对象:
1 2 3 4 5 6 7 8 9 10 11 12 Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(SampleClass.class); enhancer.setCallback(new Dispatcher() { @Override public Object loadObject () throws Exception { return new SampleClass(); } }); SampleClass proxy = (SampleClass) enhancer.create(); for (int i = 0 ; i < 3 ; i++) { Assert.assertEquals("Hello, zhenchao" , proxy.hello("zhenchao" )); }
输出如下,由输出可以看到每次调用被代理方法时都会创建代理对象:
1 2 3 4 5 6 7 SampleClass$$EnhancerByCGLIB$$21d43ab6: default construct method SampleClass: default construct method SampleClass: hello method SampleClass: default construct method SampleClass: hello method SampleClass: default construct method SampleClass: hello method
ProxyRefDispatcher 是 Dispatcher 的增强版本,唯一的区别在于在方法中传递了一个代理对象 proxy 进来:
1 2 3 4 5 6 7 8 9 10 11 12 13 Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(SampleClass.class); enhancer.setCallback(new ProxyRefDispatcher() { @Override public Object loadObject (Object proxy) throws Exception { System.out.println("proxy: " + proxy.getClass().getSimpleName()); return new SampleClass(); } }); SampleClass proxy = (SampleClass) enhancer.create(); for (int i = 0 ; i < 3 ; i++) { Assert.assertEquals("Hello, zhenchao" , proxy.hello("zhenchao" )); }
输出如下:
1 2 3 4 5 6 7 8 9 10 SampleClass$$EnhancerByCGLIB$$f6b27b53: default construct method proxy: SampleClass$$EnhancerByCGLIB$$f6b27b53 SampleClass: default construct method SampleClass: hello method proxy: SampleClass$$EnhancerByCGLIB$$f6b27b53 SampleClass: default construct method SampleClass: hello method proxy: SampleClass$$EnhancerByCGLIB$$f6b27b53 SampleClass: default construct method SampleClass: hello method
NoOp:直接委托给被代理类执行
NoOp 接口未声明任何方法,如果将 callback 设置为 NoOp 实例,则每次方法调用都会直接委托给被代理方法执行:
1 2 3 4 5 Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(SampleClass.class); enhancer.setCallback(NoOp.INSTANCE); SampleClass proxy = (SampleClass) enhancer.create(); Assert.assertEquals("Hello, zhenchao" , proxy.hello("zhenchao" ));
CallbackFilter:组合多种回调
Dispatcher 和 NoOp 从上面的示例看起来似乎是多此一举,没有太大的用处,但是在一些场景下可以使用 CallbackFilter 将它们组合起来使用,以应对不同调用分而治之,示例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(SampleClass.class); 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; } } }; enhancer.setCallbackFilter(callbackHelper); enhancer.setCallbacks(callbackHelper.getCallbacks()); SampleClass proxy = (SampleClass) enhancer.create(); Assert.assertEquals("Hello, cglib!" , proxy.hello(null )); Assert.assertNotEquals("Hello, cglib!" , proxy.toString()); System.out.println("hash code : " + proxy.hashCode());
上面的示例我们为 SampleClass 创建了多个代理对象,如果当前是定义在被代理类中的 String 方法,则直接修改返回值,否则直接委托给被代理类执行,从而能够避免直接使用 FixedValue 一刀切的问题。这个时候我们调用 toString、hashCode 方法就能够正常返回我们期望的结果。
工具类集合
Bean 工具类集合
ImmutableBean:创建对象的不可变引用
ImmutableBean 用来创建一个已有对象的不可变引用(ImmutableBean#create
方法),我们可以改变原对象的属性,这会反应在 CGLib 创建出来的不可变引用上,但是不允许通过不可变引用来修改原对象的值,示例如下:
1 2 3 4 5 6 7 8 9 10 SampleBean bean = new SampleBean(); bean.setValue("Hello world!" ); SampleBean immutableBean = (SampleBean) ImmutableBean.create(bean); Assert.assertEquals("Hello world!" , immutableBean.getValue()); bean.setValue("Hello world, again!" ); Assert.assertEquals("Hello world, again!" , immutableBean.getValue()); immutableBean.setValue("Hello cglib!" );
上面的示例中我们通过 ImmutableBean#create
方法创建了一个 SampleBean 对象的不可变引用,然后我们可以修改原对象的值,相应的修改会反应到不可变引用上,但是如果我们尝试修改不可变引用则会抛出异常。
BeanGenerator:动态创建 bean 对象
BeanGenerator 可以在 运行时 动态创建一个 bean 对象,这个对象可以是实际不存在的类,例如下面的示例中就创建了一个包含 name 属性的 bean 对象,并调用了相应的 setter 和 getter 方法,但是我们并没有为该 bean 显式的定义类:
1 2 3 4 5 6 7 8 9 10 11 BeanGenerator generator = new BeanGenerator(); generator.addProperty("name" , String.class); Object bean = generator.create(); Method setter = bean.getClass().getMethod("setName" , String.class); setter.invoke(bean, "zhenchao" ); Method getter = bean.getClass().getMethod("getName" ); Assert.assertEquals("zhenchao" , getter.invoke(bean));
BeanCopier: 在两个不同类型 bean 对象之间执行属性拷贝
BeanCopier 允许在两个不同类型(也可以是相同类型) bean 对象之间执行属性拷贝(按照属性名称匹配),如果属性名称相同但是类型不同,我们可以指定转换器。示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 BeanCopier copier = BeanCopier.create(SampleBean.class, SampleBean2.class, false ); SampleBean bean = new SampleBean(); bean.setValue("Hello cglib!" ); SampleBean2 bean2 = new SampleBean2(); copier.copy(bean, bean2, null ); Assert.assertEquals("Hello cglib!" , bean2.getValue()); bean.setValue("1103" ); copier = BeanCopier.create(SampleBean.class, SampleBean3.class, true ); SampleBean3 bean3 = new SampleBean3(); copier.copy(bean, bean3, new Converter() { @Override public Object convert (Object value, Class target, Object context) { return NumberUtils.isCreatable(String.valueOf(value)) ? NumberUtils.toInt(String.valueOf(value)) : value; } }); Assert.assertEquals(Integer.valueOf(1103 ), bean3.getValue());
BulkBean:为 bean 创建一个代理
BulkBean 在定义时需要指定 bean 类型、setter 和 getter 方法,以及方法类型,然后我们就可以依据 BulkBean 提供的 property 相关方法来操作被代理 bean 对应的属性。示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 BulkBean bulkBean = BulkBean.create( User.class, new String[] {"getName" , "getAge" }, new String[] {"setName" , "setAge" }, new Class[] {String.class, Integer.class}); User user = new User().setName("zhenchao" ).setAge(26 ); Assert.assertEquals(2 , bulkBean.getPropertyValues(user).length); Assert.assertEquals("zhenchao" , bulkBean.getPropertyValues(user)[0 ]); Assert.assertEquals(26 , bulkBean.getPropertyValues(user)[1 ]); bulkBean.setPropertyValues(user, new Object[] {"xiaotang" , 26 }); Assert.assertEquals("xiaotang" , user.getName());
BeanMap:将一个 bean 对象转换成 map 集合
1 2 3 4 5 User user = new User(); BeanMap map = BeanMap.create(user); user.setName("zhenchao" ).setAge(26 ); Assert.assertEquals("zhenchao" , map.get("name" )); Assert.assertEquals(26 , map.get("age" ));
KeyFactory:用来动态生成多个值的键
通常我们的键都是单一元素,而有多个值组成的键称为 multi-valued 键,KeyFactory 就是用来生成这种类型的键。首先我们需要定义一个接口,该接口必须仅包含一个 Object newInstance(...)
的方法,即方法名称为 newInstance,返回类型为 Object,示例:
1 2 3 public interface SampleKeyFactory { Object newInstance (String... keys) ; }
然后我们就可以使用 KeyFactory 来创建 multi-values 键:
1 2 3 4 5 6 SampleKeyFactory factory = (SampleKeyFactory) KeyFactory.create(SampleKeyFactory.class); Object key = factory.newInstance("foo" , "bar" ); Map<Object, String> map = new HashMap<>(); map.put(key, "Hello, cglib!" ); Assert.assertEquals("Hello, cglib!" , map.get(factory.newInstance("foo" , "bar" ))); Assert.assertFalse(map.containsKey(map.get(factory.newInstance("a" , "b" ))));
KeyFactory 会为键对象生成正确的 equals 方法和 hashCode 方法,从而保证键能够被正确的使用。KeyFactory 在 CGlib 的内部实现中被大量使用。
Mixin:混合多个接口的实现
假设现在有两个接口:Interface1 和 Interface2,类 Class1 和 Class2 分别实现了这两个接口,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public interface Interface1 { String first () ; } public class Class1 implements Interface1 { @Override public String first () { return "this is first" ; } } public interface Interface2 { String second () ; } public class Class2 implements Interface2 { @Override public String second () { return "this is second" ; } }
现在我们希望有一个对象能够包含 Class1 和 Class2 中所有的功能实现,则可以定义一个接口 MixinInterface 继承 Interface1 和 Interface2,实现如下:
1 public interface MixinInterface extends Interface1 , Interface2 { }
然后我们就可以使用 Mixin 机制动态创建一个 MixinInterface 接口的实现类对象,同时指定让该对象聚合 Class1 和 Class2 实现类的功能:
1 2 3 4 5 6 Mixin mixin = Mixin.create( new Class[] {Interface1.class, Interface2.class, MixinInterface.class}, new Object[] {new Class1(), new Class2()}); MixinInterface delegate = (MixinInterface) mixin; System.out.println(delegate.first()); System.out.println(delegate.second());
StringSwitcher:建立 String 和 int 之间的映射关系
StringSwitcher 可以建立一个 String 数组中值到 int 数组中值依据下标对应的映射关系,例如下面的示例中 one 对应 20,two 对应 10:
1 2 3 4 5 6 String[] strings = new String[] {"one" , "two" }; int [] values = new int [] {20 , 10 };StringSwitcher switcher = StringSwitcher.create(strings, values, true ); Assert.assertEquals(20 , switcher.intValue("one" )); Assert.assertEquals(10 , switcher.intValue("two" )); Assert.assertEquals(-1 , switcher.intValue("three" ));
这一机制在 java 7 之前可以被应用到 switch 中,因为 java 7 之前 switch 不支持匹配字符串,但是现在如果不是必须则不推荐使用。
InterfaceMaker:动态创建接口
InterfaceMaker 用于动态创建一个接口,我们可以使用 Signature 来声明接口中的方法:
1 2 3 4 5 6 7 8 Signature signature = new Signature("hello" , Type.INT_TYPE, new Type[] {Type.DOUBLE_TYPE}); InterfaceMaker maker = new InterfaceMaker(); maker.add(signature, new Type[0 ]); Class iface = maker.create(); Assert.assertEquals(1 , iface.getMethods().length); Assert.assertEquals("hello" , iface.getMethods()[0 ].getName()); Assert.assertEquals(int .class, iface.getMethods()[0 ].getReturnType());
MethodDelegate:建立方法间的代理关系
MethodDelegate 可以动态生成指定接口的实现类对象,并指定一个 bean 和无参数方法,调用接口的所有方法都会被委托给该 bean 的无参数方法:
1 2 3 4 SampleBean bean = new SampleBean().setValue("Hello, cglib!" ); Interface1 delegate = (Interface1) MethodDelegate.create(bean, "getValue" , Interface1.class); Assert.assertEquals("Hello, cglib!" , delegate.first());
MulticastDelegate:广播调用指定接口方法到注册的实现类
MulticastDelegate 允许我们在调用指定接口的某个方法时,会将其广播给所有的注册的实现类实例,即调用所有注册的实现类实例的相应方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public interface Provider { String setValue (String value) ; } public class SampleProvider implements Provider { private String name; private String value; public SampleProvider (String name) { this .name = name; } @Override public String setValue (String value) { this .value = value; return name; } public String getValue () { return value; } }
上面我们定义了一个 Provider 接口,该接口声明了一个 setValue 方法,类 SampleProvider 实现了该接口。MulticastDelegate 允许我们在调用 Provider#setValue
方法时,将其广播应用到实现类 SampleProvider 所有在册的实例上:
1 2 3 4 5 6 7 8 9 10 11 MulticastDelegate delegate = MulticastDelegate.create(Provider.class); SampleProvider provider1 = new SampleProvider("provider a" ); SampleProvider provider2 = new SampleProvider("provider b" ); delegate = delegate.add(provider1); delegate = delegate.add(provider2); Provider provider = (Provider) delegate; System.out.println(provider.setValue("Hello world!" )); Assert.assertEquals("Hello world!" , provider1.getValue()); Assert.assertEquals("Hello world!" , provider2.getValue());
注意 :该机制要求对应的接口只能声明一个方法,如果对应的方法有返回值,则只能拿到最后一个注册实例的返回值。
ConstructorDelegate:字节码层面的工厂方法
ConstructorDelegate 允许调用指定类型的指定构造方法来创建类型对象,首先我们需要声明一个 Object newInstance(...)
方法的接口(参数数目和类型没有限制,但只能有一个 newInstance 方法):
1 2 3 public interface UserConstructorDelegate { Object newInstance (String name, int age) ; }
假设我们有一个 User 类,该类刚好有一个构造方法 User(String name, int age)
,接下来我们就可以使用 ConstructorDelegate 来构造 User 对象:
1 2 3 4 5 UserConstructorDelegate delegate = (UserConstructorDelegate) ConstructorDelegate.create(User.class, UserConstructorDelegate.class); User user = (User) delegate.newInstance("zhenchao" , 28 ); Assert.assertTrue(User.class.isAssignableFrom(user.getClass())); Assert.assertEquals("zhenchao" , user.getName()); Assert.assertEquals(28 , user.getAge().intValue());
ParallelSorter:更加快速的多维数组排序器
ParallelSorter 声明对于多维数组进行排序具备更加高效的性能,示例:
1 2 3 4 5 6 7 8 Integer[][] value = { {4 , 3 , 9 , 0 }, {2 , 1 , 6 , 0 } }; ParallelSorter.create(value).mergeSort(0 ); for (final Integer[] v : value) { System.out.println(Arrays.toString(v)); }
FastClass:更快的反射方法调用
我们在使用 java 反射时往往会顾忌反射执行的性能开销,而 FastClass 声明相对于 java 原生反射调用而言具备更加高效的性能,所以可以将其作为反射场景下部分 Class 对象功能的替代,使用示例:
1 2 3 4 5 6 7 FastClass fastClass = FastClass.create(SampleBean.class); SampleBean bean = new SampleBean(); FastMethod fastMethod = fastClass.getMethod(SampleBean.class.getMethod("hello" , String.class)); Assert.assertEquals("hello, zhenchao" , fastMethod.invoke(bean, new Object[] {"zhenchao" })); fastMethod = fastClass.getMethod(SampleBean.class.getMethod("getValue" )); bean.setValue("Hi, 2019~" ); Assert.assertEquals("Hi, 2019~" , fastMethod.invoke(bean, new Object[0 ]));
上面的示例中我们使用 FastClass 创建了 SampleBean 的类 Class 对象,并利用该对象反射调用执行 SampleBean 的相关方法。FastClass 主要用于对方法的反射调用(包括构造方法),而对于字段的操作则没有提供相应的实现。
FastClass 是否真的比 java 原生反射调用快这里不做讨论,CGLib: The missing manual 给到的观点是,现代 JVM 针对原生反射调用已经进行了优化,所以 FastClass 的优势并不明显。原文如下:
How can the FastClass be faster than normal reflection?
Java reflection is executed by JNI where method invocations are executed by some C-code. The FastClass on the other side creates some byte code that calls the method directly from within the JVM. However, the newer versions of the HotSpot JVM (and probably many other modern JVMs) know a concept called inflation where the JVM will translate reflective method calls into native version ‘s of FastClass when a reflective method is executed often enough. You can even control this behavior (at least on a HotSpot JVM) with setting the sun.reflect.inflationThreshold
property to a lower value. (The default is 15.) This property determines after how many reflective invocations a JNI call should be substituted by a byte code instrumented version. I would therefore recommend to not use FastClass on modern JVMs, it can however fine-tune performance on older Java virtual machines.
参考