呃, 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));
看, 它正确运行输出了期望的值:
12345
上面的例子也展示了 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://www.bubuko.com/infodetail-2756213.html