Java 8中如何使用ASM和JiteScript“烘焙”你自己的lambda
本文由 ImportNew - hejiani 翻译自 zeroturnaround。欢迎加入Java小组。转载请参见文章末尾的要求。
呃,Java字节码。我们已经在理解Java字节码一文中已经讨论过,但继续加深下记忆吧:Java字节码是源代码的二进制表示,JVM可以读取和执行字节码。
现在Java中广泛使用字节码库,尤其Java EE中普遍用到运行时的动态代理生成。字节码转换也是常见用例,比如支持AOP运行时织入切面,或JRebel等工具提供的可扩展类重载技术。在代码质量领域,常使用库解析和分析字节码。
如果要转换类字节码,有很多字节码库可供选择,其中最常用的有ASM,Javassist和BCEL。本文将简单介绍ASM和JiteScript,JiteScript基于ASM,为类的生成提供了更流畅的API。
ASM是“awesome”的缩写吗?
嗯,可能不是。ASM是由ObjectWeb consortium提供的用于分析,修改和生成JVM字节码的Java API类库。它被广泛使用,经常作为操纵字节码最快的解决方案。Oracle JDK8部分基础的lambda实现也使用了ASM类库,可见ASM用处之广。
很多其他框架和工具也利用了ASM类库的能力,包括很多JVM语言实现,比如JRuby,Jython和Clojure。可以看出ASM作为字节码库是很好的选择!
ASM的访问者模式
ASM类库的总体架构使用了访问者模式。ASM读写字节码时,运用访问者模式按顺序访问类文件字节码的各个部分。
分析类的字节码也很简单,为你感兴趣的部分实现访问者,然后使用ClassReader解析包含字节码的字节数组。
同样地,使用ClassWriter生成一个类的字节码,然后访问类中的所有数据,再调用toByteArray()将其转化为包含字节码的字节数组。
修改——或者转换——字节码就变成了两者结合的艺术,ClassReader访问ClassWriter,使用其他访问者增加/修改/删除不同的部分。
直接使用API时,仍然需要对类文件格式,可用的字节码操作以及栈机制有一定层次的总体了解。一些由编译器完成的隐藏在Java源码之后的事情现在就要由你来实现;比如在构造器中显式地调用父构造函数,如果要实例化类,确保它必须有一个构造函数;构造函数的字节码表示为名为”
实现Runnable接口的一个简单HelloWorld类,调用run()方法System.out字符串“Hello World!”,使用ASM API生成如下:
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES); cw.visit(V1_5, ACC_PUBLIC, "HelloWorld", null, Type.getInternalName(Object.class), new String[] { Type.getInternalName(Runnable.class)}); MethodVisitor consMv = cw.visitMethod(ACC_PUBLIC, "","()V",null,null); consMv.visitCode(); consMv.visitVarInsn(ALOAD, 0); consMv.visitMethodInsn(INVOKESPECIAL, Type.getInternalName(Object.class), "", "()V", false); consMv.visitInsn(RETURN); consMv.visitMaxs(1, 1); consMv.visitEnd(); MethodVisitor runMv = cw.visitMethod(ACC_PUBLIC, "run", "()V", null, null); runMv.visitFieldInsn(GETSTATIC, Type.getInternalName(System.class), "out", Type.getDescriptor(PrintStream.class)); runMv.visitLdcInsn("Hello ASM!"); runMv.visitMethodInsn(INVOKEVIRTUAL, Type.getInternalName(PrintStream.class), "println", Type.getMethodDescriptor(Type.getType(void.class), Type.getType(String.class)), false); runMv.visitInsn(RETURN); runMv.visitMaxs(2, 1); runMv.visitEnd();
从上面的代码可以看到,要使用ASM API的默认访问者模式方法,能正确地调用要求对各个操作码的所属类别有所了解。与之相反的方式是生成方法时使用GeneratorAdapter,它提供了命名接近的方法来暴露大部分操作码,比如当返回一个方法的值时能够选择正确的操作码。
爸爸,我可以和lambda表达式愉快地玩耍吗
Java 8中lambda表达式引入到Java语言;但是在字节码级别没有发生变化!我们仍然使用Java 7增加的已有的invokedynamic功能。那这是否意味着我们在Java 7也可以运行lambda表达式呢?
不幸的是,答案是否。为创建invokedynamic调用的调用点所必须的运行时支持类不存在;但是明白我们可以用它做什么仍然是件有趣的事情:
没有语言级别支持的情况下我们将生成lambda表达式!
所以lambda表达式是什么呢?简单来说,它是运行时包装在兼容接口中的函数调用。那就来看看我们是否也可以在运行时包装,使用Method类的实例来表示要包装的方法,但是并不真正地使用反射机制完成调用!
从lambda表达式生成的字节码我们注意到,invokedynamic指令的bootstrap方法包含了关于所要包装的方法,包装该方法的接口以及接口方法描述符的所有信息。那么似乎这只是个创建匹配我们方法和接口参数的字节码的问题。
你说要创建字节码?ASM又可以大显身手了!
所以我们需要以下输入:
- 我们要包装的方法的引用
- 包装该方法的功能接口的引用
- 如果是实例方法,还要有调用该方法的目标对象的引用
为此我们定义了以下方法:
public <T> T lambdafyVirtual(Class<?> iface, Method method, Object object) public <T> T lambdafyStatic(Class<?> iface, Method method) public <T> T lambdafyConstructor(Class<?> iface, Constructor constructor)
我们需要将这些方法转化为ASM可理解的内容写入字节码文件,这样lambdaMetafactory可以读取MethodHandle。ASM中MethodHandles由句柄类型表示,而且基于Method对象创建给定方法的句柄非常简单(这里是一个实例方法):
new Handle(H_INVOKEVIRTUAL, Type.getInternalName(method.getDeclaringClass()), method.getName(), Type.getMethodDescriptor(method));
那么现在Handle就可以在invokedynamic指令的bootstrap方法中使用,接下来就真正地生成字节码吧!生成一个工厂类,它提供了一个方法,用来生成我们的invokedynamic指令调用的lambda表达式。
总结以上部分,我们获得了下面的方法:
public <T> T lambdafyVirtual(Class<?> iface, Method method, Object object) { Class<?> declaringClass = method.getDeclaringClass(); int tag = declaringClass.isInterface()?H_INVOKEINTERFACE:H_INVOKEVIRTUAL; Handle handle = new Handle(tag, Type.getInternalName(declaringClass), method.getName(), Type.getMethodDescriptor(method)); Class<Function<Object, T>> lambdaGeneratorClass = generateLambdaGeneratorClass(iface, handle, declaringClass, true); return lambdaGeneratorClass.newInstance().apply(object); }
在最终生成字节码之后,还要将字节码转化为Class对象。为此我们使用了JDK Proxy实现的defineClass,目的是将工厂类注入到与定义了包装方法的类相同的类加载器中。而且,尝试将它加入到相同的包,这样我们也能访问protected和package方法!类具有正确的名称和包需要在生成字节码之前弄清楚。我们简单地随机生成了类名;对于这个例子的目的这么做是可接受的,但这并不是具备可延伸性的好的解决方案。
冗长的战斗:ASM vs. JiteScript
上面我们使用了经典的“TV-厨房”技术,悄悄地从桌子下面拉出一只装有完整产品的锅!但现在我们真正看一下生成字节码的小实验。
使用ASM实现的代码如下:
protected byte[] generateLambdaGeneratorClass( final String className, final Class<?> iface, final Method interfaceMethod, final Handle bsmHandle, final Class<?> argumentType) throws Exception { ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES); cw.visit(V1_7, ACC_PUBLIC, className, null, Type.getInternalName(Object.class), new String[]{Type.getInternalName(Function.class)}); generateDefaultConstructor(cw); generateApplyMethod(cw, iface, interfaceMethod, bsmHandle, argumentType); cw.visitEnd(); return cw.toByteArray(); } private void generateDefaultConstructor(ClassVisitor cv) { String desc = Type.getMethodDescriptor(Type.getType(void.class)); GeneratorAdapter ga = createMethod(cv, ACC_PUBLIC, "", desc); ga.loadThis(); ga.invokeConstructor(Type.getType(Object.class), new org.objectweb.asm.commons.Method("", desc)); ga.returnValue(); ga.endMethod(); } private void generateApplyMethod(ClassVisitor cv, Class<?> iface, Method ifaceMethod, Handle bsmHandle, Class<?> argType) { final Object[] bsmArgs = new Object[]{Type.getType(ifaceMethod), bsmHandle, Type.getType(ifaceMethod)}; final String bsmDesc = argType!= null ? Type.getMethodDescriptor(Type.getType(iface), Type.getType(argType)) : Type.getMethodDescriptor(Type.getType(iface)); GeneratorAdapter ga = createMethod(cv, ACC_PUBLIC, "apply", Type.getMethodDescriptor(Type.getType(Object.class), Type.getType(Object.class))); if (argType != null) { ga.loadArg(0); ga.checkCast(Type.getType(argType)); } ga.invokeDynamic(ifaceMethod.getName(), bsmDesc, metafactory, bsmArgs); ga.returnValue(); ga.endMethod(); } private static GeneratorAdapter createMethod(ClassVisitor cv, int access, String name, String desc) { return new GeneratorAdapter( cv.visitMethod(access, name, desc, null, null), access, name, desc); }
JiteScript实现的代码如下,使用了实例初始化方法:
protected byte[] generateLambdaGeneratorClass( final String className, final Class<?> iface, final Method ifaceMethod, final Handle bsmHandle, final Class<?> argType) throws Exception { final Object[] bsmArgs = new Object[] { Type.getType(ifaceMethod), bsmHandle, Type.getType(ifaceMethod) }; final String bsmDesc = argType != null ? sig(iface, argType) : sig(iface); return new JiteClass(className, p(Object.class), new String[] { p(Function.class) }) {{ defineDefaultConstructor(); defineMethod("apply", ACC_PUBLIC, sig(Object.class, Object.class), new CodeBlock() {{ if (argumentType != null) { aload(1); checkcast(p(argumentType)); } invokedynamic(ifaceMethod.getName(), bsmDesc, metafactory, bsmArgs); areturn(); }}); }}.toBytes(JDKVersion.V1_7); }
很明显像上面这样生成可预测模式的字节码,JiteScript可读性更好,代码更简洁。这也归功于可速记的工具方法,比如sig()而不是Type.getMethodDescriptor(),在这里它可以静态导入。
将所有的代码结合起来
MethodHandle部分实现与字节码生成部分合起来进行测试,看看是否正确运行!
IntStream.rangeClosed(1, 5).forEach( lamdafier.lambdafyVirtual( IntConsumer.class, System.out.getClass().getMethod("println", Object.class), System.out ));
看,它正确运行输出了期望的值:
1 2 3 4 5
上面的例子也展示了lambda表达式实现的真正优势之一:它具有按需转换/装箱/拆箱类型的能力,本例中将定义在IntConsumer接口中的void(Object)包装为void(int)!
总结:使用所有的工具!
ASM入门并不那么难;是的,需要对字节码的了解,但是一旦具备了这个基础,从表层深入和创建自己的类就会是充满乐趣和满足感的体验。而且,这样也可以充实你自己通过Java代码获取不到的东西。同样,创建特定于当前运行时环境的你自己的类,可能会发现从未想过的机会。
ASM在字节码转换方面非常强大,JiteScript使代码简洁,可读性更好,并不要求你二者择一,它们是兼容的,毕竟JiteScript基本上仅仅是ASM API的包装。
亲自试试吧!
回顾本文章,我们创建了简单的代码,使用ASM从Method反射对象生成lambda表达式,利用JDK8 lambda表达式要关注所有的必须参数和返回类型转换!
还在等什么呢?快来试试吧:http://www.importnew.com/https://bitbucket.org/michael_rasmussen/lambdafy
原文链接: zeroturnaround 翻译: ImportNew.com - hejiani
译文链接: http://www.importnew.com/11642.html
[ 转载请保留原文出处、译者和译文链接。]