`

ASM入门

    博客分类:
  • ASM
ASM 
阅读更多

     最近也是研究Spring源码时发现Spring在对于注解的解析时使用的ASM来分析输入的Resource资源对象,分析其中的annotation元素,构造MetadataReader对象,并将annotation信息封装到AnnotationMetadata类型的annotationMetadata属性中,这引起了我对于ASM的好奇心。O(∩_∩)O哈哈~

  • 什么是ASM

    ASM是一个通用的字节码操作和分析的框架。它能够修改已经存在的class文件或者动态地以二进制流的形式生成字节码文件。它提供了通用的转换和分析算法,允许用户去组合自己自定义的复杂的转换工具和代码分析工具。ASM提供了像其他字节码操作框架相似的功能,但是ASM专注于简化它们的使用和执行,以为ASM的设计理念就是尽可能小巧,而且执行起来又不失速度,因此,ASM也受到了不少动态系统的青睐。

比如:AspectJ、BEA WebLogic、Oracle TopLink、BeanShell、Groovy、CGLIB等等。

  •      初识ASM2.0 

    尽管ASM到目前为止,已经出到了5.0.2,但是我觉得ASM2.0还是比较适合入门学习的,他的功能基本都齐全,2.0~5.0大部分是对于部分内容的bug修复和性能调优,(ASM 3.x的确提供了不少新功能,所以现在ASM3.x使用的还是比较广泛的)。

   java的字节码动态加载和反射功能使它成为了动态语言,尽管如此,java的反射在很多情况下也有时候会显得捉襟见肘,比如说很多开发者可能需要从非java源码中生成字节码,代表性的脚本语言像,Groovy(JSR-241)、BeanShell(JSR-274),或者是从元数据中获取信息生成字节码(比如,OR-mapping映射配置)。亦或当我们在处理存在的class文件或是java源码无法获得,一些工具需要去分析相互依赖 甚至是方法的行为去获得测试覆盖率、检测bug和反面模式。在JDK5 中,添加的新特性,注解和泛型,影响了字节码的结构,需要来自字节码操作工具特别关注来保持良好的运行表现。接下来让我们来认识一下小巧高速的字节码操作工具---ASM

   ASM是java编写的使用visitor模式去生成java字节码文件、转换存在的class文件。它允许开发者避免直接处理类常量池和方法字节码的偏移量。和其他的字节码操作工具像javassist、BCEL或SERP等相比,ASM为开发者隐藏了字节码的复杂性,提供了较好的表现性能。

   ASM的包结构如下图:

  

 

 Core Package:核心包提供了读、写、转换java字节码的API,为其他包定义方法,核心包的功能足以生成java字节码,实现主要的字节码转换工作。

Tree Package:java 字节码的内存中的抽象。

Analysis Package:提供了基本的数据流分析以及树状结构中的方法的类型检查算法

Commons Package:ASM2.0添加的包,提供了几个通用的字节码转换方法和简化字节码生成的适配器

Util Package:包括几个帮助字节码和可以帮助开发和测试的简单的字节码校验

XML Package:提供了一个适配器可以让java字节码和xml文件相互转换,SAX适配器允许使用XSLT 去定义字节码转换。

   接下来我们需要有一些对于JVM虚拟机规范定义的字节码结构的了解。Java类文件是8位字节的二进制流。数据项均是按照顺序紧密的排列开来,相邻的项之间没有间隔,这样的设计也使得class文件显得紧凑,而且比较节省存储存储空间。Java字节码文件中的数据项有大小不同许多项,由于每一项都有比较严格的定义,所有class文件能够比较顺利的从头到尾地解析。

[1]-------------------------------------------+
   | Header and Constant Stack                  |
   +--------------------------------------------+
   | [*] Class Attributes                       |
  [2]------------+------------------------------+
   | [*] Fields  | Field Name, Descriptor, etc  |
   |             +------------------------------+
   |             | [*] Field Attributes         |
  [3]------------+------------------------------+
   | [*] Methods | Method Name, Descriptor, etc |
   |             +------------------------------|
   |             | Method max stack and locals  |
   |             |------------------------------|
   |             | [*] Method Code table        |
   |             |------------------------------|
   |             | [*] Method Exception table   |
   |             |------------------------------|
   |             | [*] Method Code Attributes   |
   |             +------------------------------|
   |             | [*] Method Attributes        |
   +-------------+------------------------------+

   上图简单描述了java字节码的结构。

    所有的描述符,字符串字面量和其他常量在字节码文件的开头部分

     每个类文件必须包含类名称、父类信息、接口信息等等以及常量池,其他一些可选元素,eg,字段列表、方法列表和所有的属性。

    每个字段区包含字段信息(字段名称、访问标识、描述符和字段属性)

    每个方法区包含相似的头信息以及用于校验字节码的栈信息和最大本地变量数目。对于非抽象、非本地方法包含一个方法指令表、一个异常表以及代码属性,除此以外还可以包含其他的方法属性

   Class、Field、Method以及Method Code都有自己的名字,这些属性都表示关于字节码的各种各样的信息,比如说,源文件名称、内部类、签名(用于存储泛型信息)、行号、本地变量表、注解。JVM规范也允许定义自定义的属性,这些自定义属性会被标准VM忽略,但是可能需要包含附加信息

    方法代码表包含了一系列java虚拟机指令

 

  • 基于事件驱动字节码处理

    对于ASM来说,java class文件被描述成一棵tree,使用visitor模式遍历整个字节码的二进制结构。基于事件驱动的处理方式让用户只需要关注于与编程有意义的部分,而不必理解java字节码的所有具体细节。

   ASM的核心包使用了push模式去遍历复杂的字节码文件结构,ASM定义几个接口,比如ClassVisitor,FieldVisitor,MethodVisitor以及AnnotationVisitor。AnnotationVisitor是一个特殊的接口,允许我们表达继承关系的Annotation结构。接下来我们将来看看,这些接口是如何相互作用以及相互合作完成字节码文件的转换、获取字节码文件中的信息。

   Core包可以逻辑上可以分成两个部分:

   1.字节码生产者,比如ClassReader

    2.字节码消费者,比如Writers(ClassWriter,FieldWriter,MethodWriter,and AnnotationWriter) adapters(ClassAdapter and MethodAdapter) 或者其他实现了Visitor接口的类。

    ASM中的ClassReader可以直接通过字节数组或class文件间接的获得字节码数据,它能正确的分析构建字节码结构,并在内存中抽象出树状结构。ClassReader的accept方法接受一个实现了ClassVisitor接口的对象实例作为参数,然后依次调用ClassVisitor接口的各个方法。

   字节码空间上的偏移量被转换成visit事件(对不同visit方法的调用)时间上调用的先后,这个过程用户是无法进行干预的,我们可以做的是通过提供不同的Visitor来对字节码树进行修改。ClassReader的内部顺序访问时有一定要求的,但是我们也可以抛开ClassReader手动的执行这一过程,只要我们调用visit的顺序正确。但是这也无疑在获取最大化的灵活度的同时增加了字节码修改的复杂度。

   ClassVisitor通过责任链模式,可以简单地封装对于字节码文件的各种修改,将字节码的字节便宜对开发者透明化,我们只需override相应的visit方法即可。ClassAdapter实现了ClassVisitor接口定义的所有方法,实例化一个ClassAdapter需要传入一个实现了ClassVisitor接口的对象实例,对ClassAdapter中的方法的调用其实都委托给了这个作为参数传入的实现ClassVisitor接口的对象实例。然后依次传递下去形成责任链。所以当我需要修改一个字节码文件时,只需从ClassVisitor派生一个子类即可。这个责任链最终可以以ClassWriter结束,生成我们需要的处理过的字节码文件。eg:

 ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
		 ClassVisitor cc = new CheckClassAdapter(cw);
		 ClassVisitor tv = new TraceClassVisitor(cc, new PrintWriter(System.out));
		 ClassVisitor cv = new TransformingClassAdapter(tv);
		 ClassReader cr = new ClassReader(bytecode);
		 cr.accept(cv, ClassReader.SKIP_DEBUG);
		 byte[] newBytecode = cw.toByteArray();

 

  • 大小: 96 KB
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics