設計模式之訪問者模式(十五)(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在原有類上做增強,點擊這裏可以看到更多教程

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章