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

呃,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读写字节码时,运用访问者模式按顺序访问类文件字节码的各个部分。

分析类的字节码也很简单,为你感兴趣的部分实现访问者,然后使用Cla***eader解析包含字节码的字节数组。

同样地,使用ClassWriter生成一个类的字节码,然后访问类中的所有数据,再调用toByteArray()将其转化为包含字节码的字节数组。

修改——或者转换——字节码就变成了两者结合的艺术,Cla***eader访问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表达式要关注所有的必须参数和返回类型转换!
加Java架构师进阶交流群获取Java工程化、高性能及分布式、高性能、深入浅出。高架构。
性能调优、Spring,MyBatis,Netty源码分析和大数据等多个知识点高级进阶干货的直播免费学习权限
都是大牛带飞 让你少走很多的弯路的 群号是: 558787436 对了 小白勿进 最好是有开发经验

注:加群要求

1、具有工作经验的,面对目前流行的技术不知从何下手,需要突破技术瓶颈的可以加。

2、在公司待久了,过得很安逸,但跳槽时面试碰壁。需要在短时间内进修、跳槽拿高薪的可以加。

3、如果没有工作经验,但基础非常扎实,对java工作机制,常用设计思想,常用java开发框架掌握熟练的,可以加。

4、觉得自己很牛B,一般需求都能搞定。但是所学的知识点没有系统化,很难在技术领域继续突破的可以加。

5.阿里Java高级大牛直播讲解知识点,分享知识,多年工作经验的梳理和总结,带着大家全面、科学地建立自己的技术体系和技术认知!

原文地址:http://blog.51cto.com/13917525/2170668

时间: 2024-10-12 03:09:12

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

java项目中可能会使用到的jar包解释

一.Struts2 用的版本是struts2.3.1.1 一个简单的Struts项目所需的jar包有如下8个 1. struts2-core-2.3.1.1.jar: Struts2的核心类库. 2. xwork-core-2.3.1.1.jar: XWork核心类,XWork是一个标准的command模式实现,并且完全从web层剥离出来.WebWork被构建在Xwork上,而Struts2由Struts1和WebWork两个经典的MVC框架发展而来. 3. ognl-3.0.3.jar: 支持

[转载] 深入了解Java ClassLoader、Bytecode 、ASM、cglib

转载自http://www.iteye.com/topic/98178 一.Java ClassLoader 1,什么是ClassLoader 与 C 或 C++ 编写的程序不同,Java 程序并不是一个可执行文件,而是由许多独立的类文件组成,每一个文件对应于一个 Java 类. 此外,这些类文件并非立即全部都装入内存,而是根据程序需要装入内存.ClassLoader 是 JVM 中将类装入内存的那部分. 而且,Java ClassLoader 就是用 Java 语言编写的.这意味着创建您自己的

java.lang.NoSuchMethodError: org.objectweb.asm.ClassVisitor.visit(IILjava/lang/String;Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;)V

异常完整信息 严重: Servlet.service() for servlet RegServlet threw exception java.lang.NoSuchMethodError: org.objectweb.asm.ClassVisitor.visit(IILjava/lang/String;Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;)V at net.sf.cglib.core.ClassEmitter.begi

Java单例模式中双重检查锁的问题

单例创建模式是一个通用的编程习语.和多线程一起使用时,必需使用某种类型的同步.在努力创建更有效的代码时,Java 程序员们创建了双重检查锁定习语,将其和单例创建模式一起使用,从而限制同步代码量.然而,由于一些不太常见的 Java 内存模型细节的原因,并不能保证这个双重检查锁定习语有效. 它偶尔会失败,而不是总失败.此外,它失败的原因并不明显,还包含 Java 内存模型的一些隐秘细节.这些事实将导致代码失败,原因是双重检查锁定难于跟踪.在本文余下的部分里,我们将详细介绍双重检查锁定习语,从而理解它

Hibernate异常:java.lang.NoClassDefFoundError: org/objectweb/asm/Type

解决方法:将Hibernate lib包下的asm.jar添加到项目的library中. 解决方法:将Hibernate lib包下的cglib-2.1.3.jar添加到项目的library中.  Hibernate异常:java.lang.NoClassDefFoundError: org/objectweb/asm/Type

使用CGlib出现java.lang.NoClassDefFoundError: org/objectweb/asm/Type异常【补充】

援引:使用CGlib出现java.lang.NoClassDefFoundError: org/objectweb/asm/Type异常 援引:class net.sf.cglib.core.DebuggingClassWriter has interface org.objectweb.asm.ClassVisitor as super class 源环境: IDE:IntelliJ IDEA 项目目的:测试动态代理 -- CGLIB 项目属性:Java Application 项目结构: 被

java.lang.NoSuchMethodError: org.objectweb.asm.ClassWriter.(Z)V

参考链接:https://blog.csdn.net/baicp3/article/details/13512237 参考链接https://blog.csdn.net/sinat_32034679/article/details/76572613?utm_source=blogxgwz5 除了以上链接中可能会出现的问题外,我这边是因为使用了多创建了相同名称的实体类,导致跳转url的时候报java.lang.NoSuchMethodError: org.objectweb.asm.ClassWr

java注解中的元注解

一:java注解中的元注解 四个元注解分别是:@Target,@Retention,@Documented,@Inherited , 再次强调下元注解是java API提供,是专门用来定义注解的注解,其作用分别如下: @Target 表示该注解用于什么地方,可能的值在枚举类 ElemenetType 中,包括: ElemenetType.CONSTRUCTOR----------------------------构造器声明 ElemenetType.FIELD ----------------

Java NIO中的缓冲区Buffer(一)缓冲区基础

什么是缓冲区(Buffer) 定义 简单地说就是一块存储区域,哈哈哈,可能太简单了,或者可以换种说法,从代码的角度来讲(可以查看JDK中Buffer.ByteBuffer.DoubleBuffer等的源码),Buffer类内部其实就是一个基本数据类型的数组,以及对这个缓冲数组的各种操作: 常见的缓冲区如ByteBuffer.IntBuffer.DoubleBuffer...内部对应的数组依次是byte.int.double... 与通道的关系 在Java NIO中,缓冲区主要是跟通道(Chann