设计模式之访问者模式(十五)(ASM原理分析)

前言

本篇文章会以访问者设计模式为出发点,来分析ASM框架动态生成类的底层原理,上半部分讲解访问者模式,相信看懂了上半部分的访问者模式,理解下半部分的ASM底层原理不是难事(需要有一点点的字节码基础)。主要议题如下所示:

  • 访问者模式是什么?
  • 访问者模式解决了什么问题(有什么好处)?
  • 动态双分派
  • 结合ASM框架进一步理解访问者模式与ASM动态生成类原理

访问者模式

定义来自于《Design Pattern》:表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素类的前提下定义作用于这些元素的新操作

首先来分析以上这段定义

  • “表示一个作用于某对象结构中的各元素的操作”:说明了此设计模式是用来作用在一个结构上,此结构有多个元素,可以对这些个元素进行一系列操作
  • “它使你可以在不改变各元素类的前提下定义作用于这些元素的新操作”:也就是说,元素与对元素的操作解耦了,可以定义任意对元素的操作,与元素本身无关

或许还是有一些抽象,下面我们举一个具体的例子来看看

假设场景:

  • 元素(固定的对象结构):有一系列文章对象
  • 操作(对元素):我们需要对文章做字数统计、文章质量评分、文章自定义的格式化输出、修改文章的某个结构部分

元素

我们的元素就是一堆文章,文章有标题、内容、结尾这三个部分,很简单

// 考虑到之后的文章可能会有多种类型,例如新闻、小说等等,抽象了一个接口,不重要
public interface Article {

  // 文章的三大部分,这部分就是所谓元素的固定结构
  String getTitile();

  String[] getContent();

  String getEnd();
}
public class Paper implements Article {

  // 文章之三大部分
  private String titile;
  private String[] content;
  private String end;

  public Paper(String titile, String[] content) {
    this.titile = titile;
    this.content = content;
  }

  public Paper(String titile, String[] content, String end) {
    this.titile = titile;
    this.content = content;
    this.end = end;
  }
	// getter...
}

操作

这里就是访问者模式最为多变的部分了,首先我们定义一个读取者,它可以用来对元素(文章)做一个自定义的格式化输出、统计字数和文章质量评分,假设我们的需求的格式是,先输出文章标题,再输出文章内容(文章或许有很多段落,每个段落需要分行输出,并且段落开头需要一些空格),最后看看是否存在结尾,没有结尾系统需要补上一个结尾

public class ArticleReader {

  // 文章
  private final Article article;
  // 字数统计
  private int sum;

  public ArticleReader(Article article) {
    this.article = article;
  }

  // 接收一个访问者,开始进行访问元素操作
  public void accpt(Visitor visitor) {
    // 通知访问者开始访问了!
		visitor.visitStart();
    
    String titile = article.getTitile();

    // 首先访问标题
    visitor.visitTitle(titile);

    // 其次访问内容
    for (String content : article.getContent()) {
      visitor.visitContent(content);
    }

    String end = article.getEnd();
    // 如果存在结尾,就按它的来
    if (end != null && end.length() > 0){
      visitor.visitEnd(end);
    }else {
      // 没有结尾,系统要自动添加默认结尾
      visitor.visitEnd("没有结尾!系统自动添加-------------");
    }
    // 通知访问者访问结束了!
    visitor.visitLast();
    // 拿到字数统计
    sum = visitor.getSum();
  }

  public Grade calculateScore(){

    // 很粗糙的评分标准,凭借文章字数来评判好坏
    if (sum >= 30){
      return Grade.PERFECT;
    }else if (sum >= 20){
      return Grade.GOOD;
    }else {
      return Grade.BAD;
    }
  }

  public int getSum(){
    return this.sum;
  }
}

接下来就是最重要的角色,访问者

public interface Visitor {

  void visitStart();

  void visitTitle(String value);

  void visitContent(String value);

  void visitEnd(String value);

  void visitLast();
  
  int getSum();
}

然后是一个标准的自定义输出格式的访问者

public class ArticleWriter implements Visitor {

  // 最终文章的输出
  private final StringBuilder articleBuilder = new StringBuilder();

  // 记录段落
  private int num = 0;

  @Override
  public void visitStart() {
    // do noting
  }

  @Override
  public void visitTitle(String value) {
    // 字数统计
    sum += value.length();
    articleBuilder.append("标题: ").append(value).append('\n');
  }

  @Override
  public void visitContent(String value) {
    // 字数统计
    sum += value.length();
    articleBuilder
      .append("段落")
      .append(++num)
      .append(':')
      .append('\n')
      .append("  ")
      .append(value)
      .append('\n');
  }

  @Override
  public void visitEnd(String value) {
    // 结尾不统计字数了
    articleBuilder
      .append("结尾:")
      .append(value)
      .append('\n');
  }

  @Override
  public void visitLast() {
    // do noting
  }

  @Override
  public String toString() {
    return articleBuilder.toString();
  }
}

这里很简单,只是按照约定的格式来输出一篇文章,我们来编写一个测试看看效果

public class Main {

  public static void main(String[] args) {

    Article paper = new Paper("报纸标题", new String[]{"第一段内容...", "第二段内容..."});

    ArticleReader reader = new ArticleReader(paper);

    Visitor articleWriter = new ArticleWriter();

    reader.accpt(articleWriter);

    System.out.println(articleWriter);
    System.out.println("字数: " + reader.getSum());
    System.out.println("得分: " + reader.calculateScore());
  }
}

在这里插入图片描述

可以看到,我们通过定义Reader读取者的一个大行为(统计字数,按顺序访问文章各个部分),定义访问者的行为操作,就可以得到自定义的格式输出、字数统计、质量评分

那么现在如果来了几个新需求:

  • 一些指定的标题有额外字数加成,助力文章质量评分
  • 类似AOP那样,我想要在文章的最开头或者最结尾的部分增加一些内容
  • 我不要系统帮我自动添加默认结尾,我要自定义默认结尾

此时我只需要做一个新的访问者即可完成以上操作!为了方便,这里使用了适配器模式做了一个适配所有访问者的适配器

public class VisitAdapt implements Visitor {

  private final Visitor visitor;

  VisitAdapt(Visitor visitor) {
    this.visitor = visitor;
  }

  @Override
  public void visitStart() {
    visitor.visitStart();
  }

  @Override
  public void visitTitle(String value) {
    visitor.visitTitle(value);
  }

  @Override
  public void visitContent(String value) {
    visitor.visitContent(value);
  }

  @Override
  public void visitEnd(String value) {
    visitor.visitEnd(value);
  }

  @Override
  public void visitLast() {
    visitor.visitLast();
  }

  @Override
  public int getSum() {
    return visitor.getSum();
  }
}

普通的适配器而已,只求方便,也可以不要。接下来就是符合需求的自定义访问者了

public class CustomVisit extends VisitAdapt {

  public CustomVisit(Visitor visitor) {
    super(visitor);
  }

  @Override
  public void visitTitle(String value) {
    // 如果标题前面以*为开头,我们就开个后门,助力拿高分
    if (value.startsWith("*")){
      super.visitTitle(value + "-" + "拿高分!拿高分!拿高分!拿高分!拿高分!拿高分!拿高分!");
    }else {
      // 如果不是,照常即可
      super.visitTitle(value);
    }
  }

  @Override
  public void visitStart() {
    // 类似AOP
    super.visitTitle("-------start--------");
  }

  @Override
  public void visitEnd(String value) {
    // 不要系统的默认结尾,我们自定义一个默认结尾
    if ("没有结尾!系统自动添加-------------".equals(value)){
      super.visitEnd("自定义默认结尾");
    }else {
      super.visitEnd(value);
    }
  }

  @Override
  public void visitLast() {
    // 类似AOP
    super.visitEnd("--------end---------");
  }
}

来看看同样的一篇文章,用新的访问者的效果吧

public class Main {

    public static void main(String[] args) {

        Article paper = new Paper("*报纸标题", new String[]{"第一段内容...", "第二段内容..."});

        ArticleReader reader = new ArticleReader(paper);
        Visitor articleWriter = new ArticleWriter();
        Visitor custom = new CustomVisit(articleWriter);

        reader.accpt(custom);

        System.out.println(articleWriter);
        System.out.println("字数: " + reader.getSum());
        System.out.println("得分: " + reader.calculateScore());
    }
}

在这里插入图片描述
可以看到,新的访问者完成了以上所有的需求

小结

那么,到这里我们可以来总结一下访问者模式的优点了,在上面的更改需求中也可以看的出来,如果我们想变幻一个格式,只需要增加一个新的visitor类就可以了,但如果把对元素结构的操作封装在元素对象中,要做到变更需求就必须在元素对象中多出很多个 if/else ,这个设计模式可以说做到了元素与操作之间的解耦,也体现出了单一职责的原则。

但缺点也是很明显的,像本文中的文章有固定的结构,标题、内容、结尾,如果增加一个引言,那么访问者都需要再度改变,所以在元素结构会变化的情况下是比较有劣势的,所以访问者模式比较适合一些元素结构不变的场景。

动态双分派

首先,Java是一个动态单分派语言,但是可以利用访问者模式做到动态多分派,但是什么是动态单分派呢?

interface Article{}
static class Paper implements Article{}

static abstract class Reader{
  public void read(Article article){
    System.out.println("不知道是什么文章,先看了再说");
  }

  public void read(Paper paper){
    System.out.println("看报纸!");
  }
}
static class Student extends Reader{
  @Override
  public void read(Paper paper) {
    System.out.println("不想看报纸!");
  }
}

public static void main(String[] args) {
  Reader reader = new Student();

  Article article = new Paper();
  Paper paper = new Paper();

  reader.read(article);
  reader.read(paper);
}

首先,静态类型就是Article这样的,编译期可以知道的类型,动态类型就是Paper这样的运行期才可以知道的类型,动态分派,就是根据动态类型决定调用的方法,体现在方法的重写上,也就是子类override父类的方法(Student重写了Reader类的read方法),当reader的实际类型是student的时候,调用read方法会调用到重写的那个子类版本(动态连接)。

那么,什么是多分派呢?首先看看上面这个例子的输出结果,看看什么是单分派
在这里插入图片描述

可以看到,动态分派的时候,决定调用哪个方法只由调用者(由reader的实际类型决定,就算reader的静态类型是Reader,他也能调用到Student的重写方法上)动态决定,参数只看静态类型,也就是reader.read(article)这个方法调用,决定调用哪个方法是由reader的动态类型Student,和article的静态类型Article决定的,所以结果判断调用的是Article那个方法。而reader.read(paper)方法调用,paper的静态类型是Paper,所以才能调用到子类那个方法上。

方法调用只由一种因素(调用者)决定,其即为单分派

那么多分派会有怎样的效果呢?在上面例子中,如果article的动态类型是Paper,就算静态类型是Article,也应该调用到Paper那个方法上,这样方法调用就由多个因素(调用者和参数)决定,即为多分派。那么怎么做才能多分派呢?

interface Article{
    void accept(Reader reader);
}
static class Paper implements Article{

    @Override
    public void accept(Reader reader) {
        reader.read(this);
    }
}
public static void main(String[] args) {

  // 静态类型是Reader
  Reader reader = new Student();
  // 静态类型是Article
  Article article = new Paper();
  
  article.accept(reader);
}

类似访问者那样,给元素开辟一个访问的口子,就可以做到多分派了,输出如下:
在这里插入图片描述

这里虽然两个因素(调用者和参数)的静态类型都是静态类型,但都分派了对应的动态类型(Student、Paper)上,利用访问者模式做到了动态双分派。

分析ASM

到这里会进行ASM框架粗略的源码分析,需要有字节码基础

从上面的小结可以看到,访问者模式主要适用在固定的对象结构上,如果元素会变化,则访问者模式就不是很适用了。所以对于字节码这种拥有固定结构的元素上,是非常适用访问者模式的。

ASM框架被用在动态生成类或增强既有类的方法上,其通过改写或直接写出字节码(class文件)去生成新的类,相对于JDK动态代理(Proxy.newProxyInstance)增强来说,性能更高,因为后者是利用了反射,而前者直接生成想要的类行为。
在这里插入图片描述

就像我们开头举的例子,文章那样,字节码是拥有固定的结构的,一个字节码文件前四个字节一定是魔数,用来快速分辨出Java文件和非Java文件。后面一定跟着Java版本号信息,紧接着就是常量池,其中存放了各种文字字符串、类名、方法名等等,其中存储了用到的所有类型和方法的符号引用,在动态连接中起到了核心的作用,这一部分约占整个类大小60%。后面就不一一列举了,如果是AOP增强,那么就会找到固定的方法区部分,改写方法区代码即可,具体需要如何变化,只需要自定义一个访问者即可。

其实开头的访问者例子,是仿造ASM框架中访问者模式去做的,相信看懂以上例子以及访问者的读者,可以很快上手ASM框架的原理

使用ASM进行AOP编程

在分析ASM原理之前,先看看如何使用ASM增强一个类的行为。这里我们将字节码看作是元素。

public class Human {
  public void talk(){
    System.out.println("说话...");
  }
}

首先是待增强的类,比如想要在talk方法的前置加一句话。此时Human类的字节码就可以看作是一个固定对象结构的元素,我们想要达成的效果是,生成一个新的类,其继承了Human(类似CGLib那样的行为),并且talk方法在调用之前自定义加一句话。

public class ASM {

  public static void main(String[] args) throws Exception {

    // 首先读取类的字节码到reader中
    ClassReader reader = new ClassReader("com.mytest.visit.pojo.Human");
    // 写字节码的主要类
    ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
    // 自定义的访问者,有自定义的行为
    ClassVisitor visitor = new HumanClassVisitor(writer);

    reader.accept(visitor, ClassReader.SKIP_DEBUG);
    // 写出修改后的字节码
    byte[] code = writer.toByteArray();

    // 自定义的类加载器,将生成之后的类加载出来
    Class<?> clazz = AsmClassLoader.INSTANCE.defineClassPublic("com.mytest.visit.pojo.Human$EnhancedByASM", code);

    // 反射创建对象
    Human human = (Human) clazz.newInstance();
    // 调用talk方法
    human.talk();
  }

  static class HumanClassVisitor extends ClassAdapter {

    // 父类的类名
    String enhancedSuperName;

    public HumanClassVisitor(ClassVisitor cv) {
      super(cv);
    }

    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
      // 在类的开头会将类进行声明,可以在此时对生成类的类名做修改,以及增加父类
      // 修改后的类名
      String enhancedName = name + "$EnhancedByASM";
      // 保存原先那个类名,即为Human
      enhancedSuperName = name;
      // 第五个参数就是extends,也就是继承的类名,这里写上Human,表示继承被增强的类
      // 第三个参数就是本类的类名,这里的新类名是Human$EnhancedByASM
      super.visit(version, access, enhancedName, signature,
                  enhancedSuperName, interfaces);
    }

    // 这个方法会在写类中每一个方法时调用
    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {

      // 这里会先获取ClassVisitor中的MethodVisitor
      MethodVisitor methodVisitor = cv.visitMethod(access, name, desc, signature, exceptions);

      // 如果是talk,就是我们需要增强的方法
      if (methodVisitor != null && "talk".equals(name)) {
        // 将MethodVisitor包装为我们自定义的访问者
        methodVisitor = new HumanMethodVisitor(methodVisitor);
      } else if (name.equals("<init>")) {
        // 构造方法,因为我们继承了Human类,所以需要调用父类的构造方法
        // 这里我们自定义一个访问者做
        methodVisitor = new ChangeToChildConstructorMethodAdapter(methodVisitor,
                                                                  enhancedSuperName);
      }
			// 如果都不是以上的方法,就不需要管了
      return methodVisitor;
    }
  }

  static class ChangeToChildConstructorMethodAdapter extends MethodAdapter {

    private String superName;

    public ChangeToChildConstructorMethodAdapter(MethodVisitor mv, String enhancedSuperName) {
      super(mv);
      superName = enhancedSuperName;
    }

    @Override
    public void visitMethodInsn(int opcode, String owner, String name,
                                String desc) {
      // 调用父类的构造函数时
      if (opcode == Opcodes.INVOKESPECIAL && name.equals("<init>")) {
        owner = superName;
      }
      // 改写父类为 superClassName
      super.visitMethodInsn(opcode, owner, name, desc);
    }
  }

  static class HumanMethodVisitor extends MethodAdapter {

    public PaperMethodVisitor(MethodVisitor mv) {
      super(mv);
    }

    // 这个方法在每次写类的方法的开头就会被调用
    @Override
    public void visitCode() {
      // 取出System的out变量,其为PrintStream类型,放到操作栈中
      mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
      // 取出字符串常量,放到操作栈中
      mv.visitLdcInsn("在方法前动态增加的一句话");
      // 调用PrintStream的println方法(也就是刚刚放入操作栈的字符串常量和PrintStream变量)
      // 简而言之,这里就是System.out.println("在方法前动态增加的一句话");
      mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V");
    }
  }

  // 自定义的一个ClassLoader
  public static class AsmClassLoader extends ClassLoader {

    private static final AsmClassLoader INSTANCE = AccessController.doPrivileged(
      (PrivilegedAction<AsmClassLoader>) AsmClassLoader::new);

    public AsmClassLoader() {
      super(getParentClassLoader());
    }

    private static ClassLoader getParentClassLoader() {
      return Thread.currentThread().getContextClassLoader();
    }

    public Class<?> defineClassPublic(String name, byte[] b) throws ClassFormatError {
      return defineClass(name, b, 0, b.length);
    }
  }
}

跑一下main方法,控制台输出如下
在这里插入图片描述

我们可以将动态生成出来的字节码反编译一下,首先输出到文件中去

// ASM操作...
byte[] code = writer.toByteArray();
File file = new File("/tmp/Test.class");
FileOutputStream fout = new FileOutputStream(file);
fout.write(code);
fout.close();

然后去/tmp目录下,将.class文件放到idea的编译输出目录,即可反编译,结果如下
在这里插入图片描述

可以看到,类名改变了,talk方法的行为也改变了

关于直接生成完整的类,请看使用ASM进行JSON序列化一文,其使用ASM直接生成完整的类而不是像这样在原先类上进行增强

ASM源码分析

现在我们就可以对着main方法进行分析了,首先new了一个Reader,构造函数是原先类的路径

public class ClassReader {
  
  public final byte[] b;
  
  public ClassReader(String name) throws IOException {

    // 这里主要做的事情就是读出name这个路径下的那个类,将其字节码转换为byte数组
    // 然后赋值给b这个变量
  }
}

到这里我们可以看到,ClassReader构造器的作用是用来保存需要被增强的类的字节码的。接下来看看ClassReader的accept方法做了什么

public void accept(
    final ClassVisitor classVisitor,
    final Attribute[] attributePrototypes,
    final int parsingOptions) {
  
  // 重度简化版,源码中逻辑比较繁琐,所以这里只看大致流程
  classVisitor.visit(
    readInt(cpInfoOffsets[1] - 7), accessFlags, thisClass, signature, superClass, interfaces);
  
  // ...
  
  // 依次访问域
  while (fieldsCount-- > 0) {
    // 如下类似访问方法的过程
    currentOffset = readField(classVisitor, context, currentOffset);
  }
  int methodsCount = readUnsignedShort(currentOffset);
  currentOffset += 2;
  
  // 依次访问方法
  while (methodsCount-- > 0) {
    // 这里就会调用Visitor的visitMethod方法,返回一个MethodVisitor
    // 然后继续按照字节码格式调用MethodVisitor的visit方法
    currentOffset = readMethod(classVisitor, context, currentOffset);
  }

  // 类似开头的例子中的,visitLast那样,在最后会留一个收尾的点
  // Visit the end of the class.
  classVisitor.visitEnd();
}

从这个重度简化版可以看出,其accept方法其实就是调用Visitor类的各种visit方法,大致讲几个

  • 首先visit类签名:类的访问修饰符(例如public)、类名、类的父类、类实现的接口等等
  • visit类的各个域:类的各个成员变量
  • visit类的各个方法:类的构造器、类的各种方法

而AOP即为创建一个方法的visitor,然后在visit方法时做手脚,关键就在Visitor的visit方法,来看看基础Visitor类ClassWriter其做了什么

// visit方法签名
public final void visit(
  final int version,
  final int access,
  final String name,
  final String signature,
  final String superName,
  final String[] interfaces) {
  this.version = version;
  this.accessFlags = access;
  this.thisClass = symbolTable.setMajorVersionAndClassName(version & 0xFFFF, name);
  if (signature != null) {
    this.signatureIndex = symbolTable.addConstantUtf8(signature);
  }
  this.superClass = superName == null ? 0 : symbolTable.addConstantClass(superName).index;
  if (interfaces != null && interfaces.length > 0) {
    interfaceCount = interfaces.length;
    this.interfaces = new int[interfaceCount];
    for (int i = 0; i < interfaceCount; ++i) {
      this.interfaces[i] = symbolTable.addConstantClass(interfaces[i]).index;
    }
  }
  if (compute == MethodWriter.COMPUTE_MAX_STACK_AND_LOCAL && (version & 0xFFFF) >= Opcodes.V1_7) {
    compute = MethodWriter.COMPUTE_MAX_STACK_AND_LOCAL_FROM_FRAMES;
  }
}

可以看到,visit方法签名只不过是把各类需要的信息更新到本类的各个成员变量而已,再看看visitMethod是什么逻辑

public final MethodVisitor visitMethod(
  final int access,
  final String name,
  final String descriptor,
  final String signature,
  final String[] exceptions) {
  // new一个方法的Writer
  MethodWriter methodWriter =
    new MethodWriter(symbolTable, access, name, descriptor, signature, exceptions, compute);
  // 将其赋值给成员变量中
  if (firstMethod == null) {
    firstMethod = methodWriter;
  } else {
    lastMethod.mv = methodWriter;
  }
  return lastMethod = methodWriter;
}

其也只是普通的new了一个方法Writer,保存起来而已。可以发现,这两种visit只不过是保存一些元信息(例如类名、父类名、名法签名信息、方法会抛出的一些异常等等的元信息),回忆一下,如果是我们自定义的方法Writer,这里就会把我们自定义的那个MethodWriter保存起来。

最后也即为最关键的方法:writer.toByteArray()生成最后的字节码

public byte[] toByteArray() {
  // First step: compute the size in bytes of the ClassFile structure.
  // The magic field uses 4 bytes, 10 mandatory fields (minor_version, major_version,
  // constant_pool_count, access_flags, this_class, super_class, interfaces_count, fields_count,
  // methods_count and attributes_count) use 2 bytes each, and each interface uses 2 bytes too.
  
  // 重度简化版...
  
  // Second step: allocate a ByteVector of the correct size (in order to avoid any array copy in
  // dynamic resizes) and fill it with the ClassFile content.
  // 类似一个StringBuilder,拼接一个个字节码内容
  ByteVector result = new ByteVector(size);
  // 首先拼接最为熟悉的CAFEBABE,然后就是JAVA版本号,这些开头都有说到
  result.putInt(0xCAFEBABE).putInt(version);
  
  //...
  
  fieldWriter = firstField;
  while (fieldWriter != null) {
    // 这里就将visitor收集到的元信息放入result中
    fieldWriter.putFieldInfo(result);
    fieldWriter = (FieldWriter) fieldWriter.fv;
  }
  
  //...
  
  methodWriter = firstMethod;
  while (methodWriter != null) {
    hasFrames |= methodWriter.hasFrames();
    hasAsmInstructions |= methodWriter.hasAsmInstructions();
    // 这里就将visitor收集到的元信息放入result中
    methodWriter.putMethodInfo(result);
    methodWriter = (MethodWriter) methodWriter.mv;
  }
  
  //...
  
  return result.data;
}

看到这里应该就能知道大致的原理了,Reader的构造函数、accept方法,只不过是在构造一个类的字节码结构,而真正写出字节码结构是在这个toByteArray方法中,因为Reader会按结构来一个个调用visit方法,给Visitor提供元信息,最终Visitor可以根据这些得到的元信息来写出最终的字节码。

详细代码比较繁琐,只需要知道一个大概流程即可:

  1. Reader读入字节码信息,保存起来(类似ArticleReader,读取一个文章信息)
  2. Reader调用accept方法,这个方法会按结构有顺序地调用visitor的各个方法,此时visitor中就会处理自己想得到的元信息,并将其组织起来放入成员变量中(类似ArticleReader,按照一定次序调用visitor的各个visit方法)
  3. 等accept结束之后,visitor就有足够的信息了,接下来调用visitor的toByteArray方法即可输出刚刚所访问的所有信息(类似ArticleWriter的toString方法,最后输出自己组织获取到的信息)

可以与开头访问者模式的例子做对比,就会发现两者其实差不多,只不过那个例子被我简化了,用了文章来代替字节码的复杂性,使其更好理解,其大致流程都是一样的。

总结来说,ASM中的访问者模式就是对固定的对象结构(字节码)做一些操作,有序地读取(访问)所有类中的元信息,并利用获得到的元信息再度生成一个字节数组(字节码),这样就可以做到动态生成类,其实就是动态生成字节码而已。

如果想要从头自己生成一个类,难度比这个AOP大,其不需要Reader去读取一个类结构,而是自己手动撸一个字节码结构

ClassWriter cw = new ClassWriter(0);
MethodVisitor mv;

cw.visit(52, ACC_PUBLIC + ACC_SUPER, fullClassName, null, javaBeanSerializer, null);

{
  // 自己visit做构造器信息
  mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
  mv.visitCode();
  mv.visitVarInsn(ALOAD, 0);
  mv.visitMethodInsn(INVOKESPECIAL, javaBeanSerializer, "<init>", "()V", false);
  mv.visitInsn(RETURN);
  mv.visitMaxs(1, 1);
  mv.visitEnd();
}

{
  // 自己visit每个方法,做每个方法的信息
  mv = cw.visitMethod(ACC_PUBLIC, "write", "(L" + serializeWriter + ";Ljava/lang/Object;)V", null, new String[]{"java/lang/Exception"});
  mv.visitCode();

  mv.visitVarInsn(ALOAD, 1);
  mv.visitIntInsn(BIPUSH, 123);
  mv.visitFieldInsn(PUTFIELD, serializeWriter, "preSymbol", "C");

  //...
}
cw.visitEnd();
// 将visit收集到的元信息输出成完整的一个字节码
byte[] code = cw.toByteArray();

可以看到,不要Reader,自己手撸一个字节码结构比较困难,需要比较熟悉字节码结构,关于怎么直接生成一个新的类而不是AOP在原有类上做增强,点击这里可以看到更多教程

发布了84 篇原创文章 · 获赞 85 · 访问量 10万+

猜你喜欢

转载自blog.csdn.net/qq_41737716/article/details/102721528