Java 8中如何使用ASM和JiteScript“烘焙”你自己的lambda

呃,Java字节码。我们已经在理解Java字节码一文中已经讨论过,但继续加深下记忆吧:Java字节码是源代码的二进制表示,JVM可以读取和执行字节码。

现在Java中广泛使用字节码库,尤其Java EE中普遍用到运行时的动态代理生成。字节码转换也是常见用例,比如支持AOP运行时织入切面,或JRebel等工具提供的可扩展类重载技术。在代码质量领域,常使用库解析和分析字节码。

如果要转换类字节码,有很多字节码库可供选择,其中最常用的有ASM,JavassistBCEL。本文将简单介绍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表达式要关注所有的必须参数和返回类型转换!

还在等什么呢?快来试试吧:https://bitbucket.org/michael_rasmussen/lambdafy

原文链接: zeroturnaround 翻译: ImportNew.com - hejiani
译文链接: http://www.importnew.com/11642.html
[ 转载请保留原文出处、译者和译文链接。]

关于作者: hejiani

(新浪微博:@jianihe

查看hejiani的更多文章 >>



相关文章

发表评论

Comment form

(*) 表示必填项

还没有评论。

跳到底部
返回顶部