上接http://tianya23.blog.51cto.com/1081650/565660

 上面的内容看起来很复杂,好像需要很深奥的字节码指令知识。但是在编译的类上使用ASMifierClassVisitor就可以让你知道如何用ASM生成给定的字节码。此外,在两个编译的类上(一个原始的和另一个应用特定的转换)使用然后进行比较就可以给出什么样的ASM调用应该被使用在转换器上。这个过程在几篇文章中已经详细解释了(可以参看最后的资源部分)。目前已经有了Eclipse使用的插件了,如图4,提供了从Java源生成ASM代码及比较ASMifier输出的良好支持,还包含了上下文字节码的参考。

ASM2.0字节码框架介绍 - 2_ASM框架介绍


  Figure 4. Eclipse ASM plugin (Click on the picture to see a full-size p_w_picpath)

  用ASM的访问者来跟踪类的依赖

  已经有一些文章介绍了如何用ASM生成字节码。现在,我们来看一下如何用ASM分析已有的类。我们来做一个有趣的应用来获取给定的.jar文件中使用的外部类和包。简单起见,这个例子仅获取外部的依赖而不会取依赖的类型(如父类/方法参数/局部变量类型等)。仅为分析,我们不会创建那些注解/成员/方法的子访问者实例。所有使用的访问者(包括类和标识访问者)都在一个类中实现:

public class DependencyVisitor implements 
    AnnotationVisitor, SignatureVisitor,
    ClassVisitor, FieldVisitor, MethodVisitor {
...



  在这个例子中,我们会跟踪包之间的依赖,因此私有类必须包含包名:

  private String getGroupKey(String name) {
    int n = name.lastIndexOf('/');
    if(n>-1) name = name.substring(0, n);
    packages.add(name);
    return name;
  }

  为了获取依赖关系,访问者接口如ClassVisitor, AnnotationVisitor, FieldVisitor, and MethodVisitor应该选择性地集成方法的参数。几个常见的样例如下:

  private void addName(String name) {
    if(name==null) return;
    String p = getGroupKey(name);
    if(current.containsKey(p)) {
      current.put(p, current.get(p)+1);
    } else {
      current.put(p, 1);
    }
  }



  在这个例子中,current是依赖的当前包。另一个例子是类型描述符(注解/枚举/成员类型/newarray指令的参数等);如Ljava/lang/String;, J, and [[[I。这些可以用Type.getType( desc)来获取内部格式的类名:

  private void addDesc(String desc) {
    addType(Type.getType(desc));
  }

  private void addType(Type t) {
    switch(t.getSort()) {
      case Type.ARRAY:
        addType(t.getElementType());
        break;
      case Type.OBJECT:
        addName(t.getClassName().replace('.','/'));
        break;
    }
  }



  在方法定义中的方法描述法及激活指令中的描述参数类型及返回类型。可以通过Type.getReturnType(methodDescriptor) 和Type.getArgumentTypes(methodDescriptor)来解析并取得参数和返回类型。

  private void addMethodDesc(String desc) {
    addType(Type.getReturnType(desc));
    Type[] types = Type.getArgumentTypes(desc);
    for(int i = 0; i < types.length; i++) {
      addType(types[ i]);
    }
  }


  而使用在许多“访问”方法中的用来定义Java5泛型信息的标识参数是个特例。如果存在,这个参数重写描述符参数并包含编码后的泛型信息。可以被用SignatureReader来解析这个值。所以我们可以实现SignatureVisitor来被每一个标识工件来调用。
  private void addSignature(String sign) {
    if(sign!=null) {
      new SignatureReader(sign).accept(this);
    }
  }
  
  private void addTypeSignature(String sign) {
    if(sign!=null) {
      new SignatureReader(sign).acceptType(this);
    }
  }


  实现ClassVisitor接口的方法,如such as visit(), visitField(), visitMethod(), and visitAnnotation()就可以获取在父类/接口/成员类型/方法参数/返回值/异常上的依赖信息,就如注解一样。例如:
  public void visit(int version, int access, 
      String name, String signature,
      String superName, String[] interfaces) {
    String p = getGroupKey(name);
    current = groups.get(p);
    if(current==null) {
      current = new HashMap


  实现MethodVisitor接口的方法就可以获取关于参数注解类型和使用在可以使用对象引用的字节码指令上的依赖:
  public AnnotationVisitor 
      visitParameterAnnotation(int parameter,
          String desc, boolean visible) {
    addDesc(desc);
    return this;
  }

  /**
   * Visits a type instruction
   * NEW, ANEWARRAY, CHECKCAST or INSTANCEOF.
   */
  public void visitTypeInsn(int opcode,
        String desc) {
    if(desc.charAt(0)=='[') {
      addDesc(desc);
    } else {
      addName(desc);
    }
  }

  /**
   * Visits a field instruction
   * GETSTATIC, PUTSTATIC, GETFIELD or PUTFIELD.
   */
  public void visitFieldInsn(int opcode,
        String owner, String name, String desc) {
    addName(owner);
    addDesc(desc);
  }

  /**
   * Visits a method instruction INVOKEVIRTUAL,
   * INVOKESPECIAL, INVOKESTATIC or
   * INVOKEINTERFACE.
   */
  public void visitMethodInsn(int opcode,
        String owner, String name, String desc) {
    addName(owner);
    addMethodDesc(desc);
  }

  /**
   * Visits a LDC instruction.
   */
  public void visitLdcInsn(Object cst) {
    if(cst instanceof Type) {
      addType((Type) cst);
    }
  }

  /**
   * Visits a MULTIANEWARRAY instruction.
   */
  public void visitMultiANewArrayInsn(
        String desc, int dims) {
    addDesc(desc);
  }

  /**
   * Visits a try catch block.
   */
  public void visitTryCatchBlock(Label start,
        Label end, Label handler, String type) {
    addName(type);
  }


  现在我们可以用DependencyVisitor来获取整个.jar文件的依赖关系了。例如:
  DependencyVisitor v = new DependencyVisitor();
  ZipFile f = new ZipFile(jarName);
  Enumeration en = f.entries();
  while(en.hasMoreElements()) {
    ZipEntry e = en.nextElement();
    String name = e.getName();
    if(name.endsWith(".class")) {
      ClassReader cr =
          new ClassReader(f.getInputStream(e));
      cr.accept(v, false);
    }
  }


  可以用很多不同的方式来表示得到的信息。一种方式是构建依赖树并计算相关数据或者创建可视化的东西。例如,图5显示了ant1.6.5 jar包中的依赖关系的可视化表现,这是我使用一些简单的Java2D代码写的。下面的图在水平轴上显示包,在垂直轴上显示依赖。阴影部分表示包被多次引用。

 

 

 

 

ASM2.0字节码框架介绍 - 2_ASM框架介绍_02


  Figure 5. Dependencies in ant.1.6.5.jar, as discovered with ASM

  这个工具的全部代码会被包含在下一个ASM发布中。你可以从ASM CVS获取。

  ASM1.x后的改变

  如果你没有使用ASM1.x可以略过这个段。2.0中主要的结构变化是所有J2SE5.0的特性都被内建到访问者/过滤器的事件流中。因此新的API允许你用更轻便和自然的方式来处理泛型和注解。不需要显式创建注解属性实例,因为在事件流中已经包含了泛型和注解数据。例如,在1.x,ClassVisitor接口如下使用:

  CodeVisitor visitMethod(int access, String name, 
      String desc, String[] exceptions,
      Attribute attrs);
This has been split into several methods in ASM 2.0:
  在2.0中已经分为多个方法:
  MethodVisitor visitMethod(int access,
      String name, String desc, String signature,
      String[] exceptions)

  AnnotationVisitor visitAnnotation(String desc,
      boolean visible)

  void visitAttribute(Attribute attr)

 

  在1.x API中,为了定义泛型信息,你必须创建SignatureAttribute的实例,而定义注解你需要RuntimeInvisibleAnnotations, RuntimeInvisibleParameterAnnotations, RuntimeVisibleAnnotations, RuntimeVisibleParameterAnnotations, and AnnotationDefault的实例。然后你可以将这些实例放在相应的访问方法的attrs参数中。

在2.0,增加了新的标识参数来表示泛型信息。新的AnnotationVisitor接口被用来处理所有的注解。不再需要创建attrs集合了,而且注解数据是强类型的。然而在移植现有代码时,特别是在“适配器”类被使用时,必须注意确保所有来自适配器的方法需要重写来适应新的标识,因为编译器不用对这种情况给出警告。

ASM2.0还有些其他的改变。
1、增加了新的接口FieldVisitor 和AnnotationVisitor
2、CodeVisitor合并到MethodVisitor中了。
3、在MethodVisitor中增加了visitCode()方法简化检测首个指令。
4、Constants接口重构为Opcodes。
5、所有来自attrs包的属性被包含到ASM的事件模型中。
6、TreeClassAdapter and TreeCodeAdapter被包含到ClassNode and MethodNode中。
7、增加LabelNode类使指令集合的元素成为AbstractInsnNode的通用类型。
通常,建议使用如JDiff这样的工具来比较两个版本之间的区别。

  小结

ASM2.0为开发人员屏蔽了字节码的复杂性,因而使开发人员更有效在字节码级别上使用Java的特性。这个框架不仅允许你转换和生成字节码,而且可以从现有的类中取得具体的信息。他的API继续改善,现在已经包含了J2SE5.0中的泛型和注解。接下来,还会增加Mustang(J2SE6)中的新特性。

  资源

·Java Virtual Machine Specification Java虚拟机规范•·"“修订的类文件格式”(JVM规范的第4章)。包含J2SE5.0中支持的JSR-14/JSR-175/JSR-201中要求的修改及其他小的更正和调整。
·“使用ASM工具集来处理字节码”
·“使用ASM工具集来创建和读写J2SE5.0注解”
·字节码指令(BCI)。

Eugene Kuleshov是一个独立咨询师,拥有超过15年的软件设计开发经验。

  版权声明:任何获得Matrix授权的网站,转载时请务必保留以下作者信息和链接
原文:http://www.javaworld.com/
译文:http://www.matrix.org.cn/

【注意】

1、编译之后的.class文件是经过压缩的,使用ultraedit查看,只能查看一些基本的信息,最好使用javap -c Photo进行查看

2、在运行的时候,向jvm传递参数:-javaagent:"C:/Documents and Settings/keju.wangkj/桌面/asm.project.jar"