曹工說Spring Boot源碼(26)-- 學習字節碼也太難了,實在不能忍受了,寫了個小小的字節碼執行引擎

曹工說Spring Boot源碼(26)-- 學習字節碼也太難了,實在不能忍受了,寫了個小小的字節碼執行引擎

寫在前面的話

相關背景及資源:html

曹工說Spring Boot源碼(1)-- Bean Definition究竟是什麼,附spring思惟導圖分享java

曹工說Spring Boot源碼(2)-- Bean Definition究竟是什麼,我們對着接口,逐個方法講解git

曹工說Spring Boot源碼(3)-- 手動註冊Bean Definition不比遊戲好玩嗎,咱們來試一下spring

曹工說Spring Boot源碼(4)-- 我是怎麼自定義ApplicationContext,從json文件讀取bean definition的?json

曹工說Spring Boot源碼(5)-- 怎麼從properties文件讀取bean網絡

曹工說Spring Boot源碼(6)-- Spring怎麼從xml文件裏解析bean的oracle

曹工說Spring Boot源碼(7)-- Spring解析xml文件,到底從中獲得了什麼(上)框架

曹工說Spring Boot源碼(8)-- Spring解析xml文件,到底從中獲得了什麼(util命名空間)dom

曹工說Spring Boot源碼(9)-- Spring解析xml文件,到底從中獲得了什麼(context命名空間上)jvm

曹工說Spring Boot源碼(10)-- Spring解析xml文件,到底從中獲得了什麼(context:annotation-config 解析)

曹工說Spring Boot源碼(11)-- context:component-scan,你真的會用嗎(此次來講說它的奇技淫巧)

曹工說Spring Boot源碼(12)-- Spring解析xml文件,到底從中獲得了什麼(context:component-scan完整解析)

曹工說Spring Boot源碼(13)-- AspectJ的運行時織入(Load-Time-Weaving),基本內容是講清楚了(附源碼)

曹工說Spring Boot源碼(14)-- AspectJ的Load-Time-Weaving的兩種實現方式細細講解,以及怎麼和Spring Instrumentation集成

曹工說Spring Boot源碼(15)-- Spring從xml文件裏到底獲得了什麼(context:load-time-weaver 完整解析)

曹工說Spring Boot源碼(16)-- Spring從xml文件裏到底獲得了什麼(aop:config完整解析【上】)

曹工說Spring Boot源碼(17)-- Spring從xml文件裏到底獲得了什麼(aop:config完整解析【中】)

曹工說Spring Boot源碼(18)-- Spring AOP源碼分析三部曲,終於快講完了 (aop:config完整解析【下】)

曹工說Spring Boot源碼(19)-- Spring 帶給咱們的工具利器,建立代理不用愁(ProxyFactory)

曹工說Spring Boot源碼(20)-- 碼網恢恢,疏而不漏,如何記錄Spring RedisTemplate每次操做日誌

曹工說Spring Boot源碼(21)-- 爲了讓你們理解Spring Aop利器ProxyFactory,我已經拼了

曹工說Spring Boot源碼(22)-- 你說我Spring Aop依賴AspectJ,我依賴它什麼了

曹工說Spring Boot源碼(23)-- ASM又立功了,Spring原來是這麼遞歸獲取註解的元註解的

曹工說Spring Boot源碼(24)-- Spring註解掃描的瑞士軍刀,asm技術實戰(上)

曹工說Spring Boot源碼(25)-- Spring註解掃描的瑞士軍刀,ASM + Java Instrumentation,順便提提Jar包破解

工程代碼地址 思惟導圖地址

工程結構圖:

概要

原本,這兩三講,不是和asm有些關係嗎,可是asm難的地方,歷來不在他自身,而是難在如何讀懂字節碼。我給你們舉個例子,以下這個簡單的類:

public class CheckAndSet {
    private int f;

    public void checkAndSetF(int f) {
        if (f >= 0) {
            this.f = f;
        } else {
            throw new IllegalArgumentException();
        }
    }

    public boolean checkAndSetF1(int f) {
        boolean a = true;
        boolean b = f >= 0;
        return b;
    }
}

咱們假設要用asm來寫出這個代碼,要怎麼寫?能夠利用咱們上一講提到的asm插件:ASM ByteCode Outline來輔助,可是,若是不懂字節碼,仍是有不少坑的,一時半會趟不出來那種。字節碼這個東西,若是始終繞不開的話,那仍是要學。

上面那個簡單的類,用javap -v CheckAndSet.class 來反編譯的話,checkAndSetF1方法,會生成以下的字節碼:

public boolean checkAndSetF1(int);
    descriptor: (I)Z
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=4, args_size=2
         0: iconst_1
         1: istore_2
         2: iload_1
         3: iflt          10
         6: iconst_1
         7: goto          11
        10: iconst_0
        11: istore_3
        12: iload_3
        13: ireturn

這些字節碼看起來,是否是摳腦袋?怎麼知道字節碼對應的意思呢,這個固然是看文檔。

JVM虛擬機規範.pdf

或者

https://docs.oracle.com/javase/specs/jvms/se10/html/jvms-4.html#jvms-4.1

針對第一個pdf,你們能夠從後往前查找(pdf最後附了個全部字節碼指令的介紹),如:

再往上查找,還會有詳細的說明:

靠着這個文檔,我開始了逐行手動計算:執行這個字節碼以前,棧和本地變量表是什麼樣的;執行這個指令後,棧和本地變量表是什麼樣的。過程,那是至關痛苦,大概和下面的圖差很少(圖片來源於網絡,我只是拿來描述下):

我可能還要原始一點,圖也沒畫,直接在notepad++裏,記錄執行每一步以後,本地變量表和操做數棧的狀況。這樣的效率真的過低了,並且看一會,我就忘了。。

而後我以爲,這個東西,好像能夠寫個程序來幫我執行,無非就是一條條地執行字節碼,而後維護一個本地變量list,維護一個棧;執行字節碼的時候,我就照着字節碼的意思來作:要取本地變量我就取本地變量,要入棧我就入棧,要出棧我就出棧,反正文檔很詳細嘛,照着來便可。

說幹就幹。

效果展現

最終實現出來,效果以下,能夠展現每一步的字節碼和執行以後的本地變量表和操做數棧的狀態。
好比執行以下方法:

public void checkAndSetF(int f) {
        if (f >= 0) {
            this.f = f;
        } else {
            throw new IllegalArgumentException();
        }
    }

字節碼:

public void checkAndSetF(int);
    descriptor: (I)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: iload_1
         1: iflt          12
         4: aload_0
         5: iload_1
         6: putfield      #2                  // Field f:I
         9: goto          20
        12: new           #3                  // class java/lang/IllegalArgumentException
        15: dup
        16: invokespecial #4                  // Method java/lang/IllegalArgumentException."<init>":()V
        19: athrow
        20: return

執行效果:

大體思路與實現

  • 編譯目標class,我這裏拿前面的CheckAndSet.class舉例

  • javap -v CheckAndSet.class > a.txt,後續咱們就會讀取a.txt來獲取方法的指令集合

  • 編寫字節碼執行引擎,一條一條地執行字節碼

用javap -v來反編譯class,能夠拿到class的字節碼,大概有兩塊東西比較重要:

  1. 方法的指令集合,這是咱們最須要的東西,我拿一條指令來舉例:

    public void checkAndSetF(int);
        descriptor: (I)V
        flags: ACC_PUBLIC
        Code:
          stack=2, locals=2, args_size=2
             0: iload_1
             1: iflt          12
             4: aload_0
             5: iload_1
             6: putfield      #2                  // Field f:I
             9: goto          20
            12: new           #3                  // class java/lang/IllegalArgumentException
            15: dup
            16: invokespecial #4                  // Method java/lang/IllegalArgumentException."<init>":()V
            19: athrow
            20: return

    好比,其中的 6: putfield #2 // Field f:I這條,其中,真正的指令,其實只有下面這部分:

    6: putfield      #2

    剩下的// Field f:I是javap給咱們提供的註釋,真正的class中是沒有這部分的。那麼,

    6: putfield      #2

    要怎麼看呢,其中的#2是什麼鬼意思?別慌,接着看另外一塊很重要的東西:常量池。

  2. 常量池

    Constant pool:
       #1 = Methodref          #6.#26         // java/lang/Object."<init>":()V
       #2 = Fieldref           #5.#27         // com/yn/sample/CheckAndSet.f:I
       #3 = Class              #28            // java/lang/IllegalArgumentException
       ...
       #5 = Class              #29            // com/yn/sample/CheckAndSet
       ...
       #27 = NameAndType        #7:#8          // f:I

    前面的#2,就是上面的:

    #2 = Fieldref           #5.#27         // com/yn/sample/CheckAndSet.f:I

    其中,// com/yn/sample/CheckAndSet.f:I也是註釋,前面的#5.#27 纔是class中真實存在的。

    無論怎麼說,你們反正也知道#2的意思,就是CheckAndSetf這個field
    有了這兩塊東西,基本能夠開搞了。

單條指令的執行

好比,我要執行:

6: putfield      #2

利用#2拿到要執行指令的field(利用反射),而後再從棧裏,彈出來:目標對象、要設置的field的入參。就能夠像下面這樣執行了:

Field field;		
	...
          
	/**
         * 從堆棧依次出棧:
         * value,objectref
         */
        Object value = context.getOperandStack().removeLast();
        Object target = context.getOperandStack().removeLast();
        try {
            field.set(target,value);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }

執行引擎核心邏輯與指令的執行順序控制

原本,我一開始是直接遍歷某個方法的指令集的:

public boolean checkAndSetF1(int);

descriptor: (I)Z
flags: ACC_PUBLIC
Code:
  stack=1, locals=4, args_size=2
     0: iconst_1
     1: istore_2
     2: iload_1
     3: iflt          10
     6: iconst_1
     7: goto          11
    10: iconst_0
    11: istore_3
    12: iload_3
    13: ireturn

就是按順序執行,0 1 2 ...13 。可是這是有bug的,由於我忽略了下面這種跳轉指令:

3: iflt          10
	 ...
     7: goto          11

因此,後來我改爲了,將這個指令集合,弄成一個鏈表,每一個指令中,維護下一條指令的引用。

@Data
public class MethodInstructionVO {
    /**
     * 序列號
     */
    private String sequenceNumber;

    /**
     * 操做碼
     */
    private String opcode;

    /**
     * 操做碼的說明
     */
    private String opCodeDesc;

    /**
     * 操做數
     */
    private String operand;

    /**
     * 操做數的說明
     */
    private String comment;

    /**
     * 按順序執行的狀況下的下一條指令,好比,javap反編譯後,字節碼以下:
     *          0: iconst_1
     *          1: istore_2
     *          2: iload_1
     *          3: iflt          10
     *          6: iconst_1
     *          7: goto          11
     * 那麼,0: iconst_1 這條指令的nextInstruction就會執行偏移爲1的那個;
     */
    @JSONField(serialize = false)
    MethodInstructionVO nextInstruction;
}

上面的最後一個字段,就是用來指向下一條指令的。默認就是指向下一條,好比:

stack=1, locals=4, args_size=2
     0: iconst_1     -- next指向 1
     1: istore_2     -- next指向 2
     2: iload_1      -- next指向 3,最後一條的next爲null

大概的核心執行框架以下:

1. 
		MethodInstructionVO currentInstruction = instructionVOList.get(0);
		
        while (true) {
            // 2.
            ExecutorByOpCode executorByOpCode = executorByOpCodeMap.get(currentInstruction.getOpcode());
            if (executorByOpCode == null) {
                log.info("currentInstruction:{}", currentInstruction);
            }
            // 3.
            InstructionExecutionContext context = new InstructionExecutionContext();
            context.setTarget(target);
            context.setConstantPoolItems(constantPoolItems);
            context.setLocalVariables(localVariables);
            context.setOperandStack(operandStack);
            String desc = OpCodeEnum.getDescByNameIgnoreCase(currentInstruction.getOpcode());
            currentInstruction.setOpCodeDesc(desc);
            context.setInstructionVO(currentInstruction);

            /**
             * 4. 若是該字節碼執行後,返回值不爲空,則表示,須要跳轉到其餘指令執行
             */
            InstructionExecutionResult instructionExecutionResult =
                    executorByOpCode.execute(context);
            log.info("after {},\noperand stack:{},\nlocal variables:{}", JSONObject.toJSONString(currentInstruction, SerializerFeature.PrettyFormat),
                    operandStack, localVariables);

			// 5
            if (instructionExecutionResult == null) {
                currentInstruction = currentInstruction.getNextInstruction();
                if (currentInstruction == null) {
                    System.out.println("execute over---------------");
                    break;
                }
                continue;
            } else if (instructionExecutionResult.isReturnInstruction()) {
                // 6
                return instructionExecutionResult.getResult();
            } else if (instructionExecutionResult.isExceptional()) {
                // 7
                log.info("method execute over,throw exception:{}", instructionExecutionResult.getResult());
                throw (Throwable) instructionExecutionResult.getResult();
            }
          // 8
                String sequenceNum = instructionExecutionResult.getInstructionSequenceNum();
            currentInstruction = instructionVOHashMap.get(sequenceNum);
            log.info("will skip to {}", currentInstruction);
        }
  • 1處,默認獲取第一條指令

  • 2處,獲取指令對應的處理器,好比,獲取iconst_1指令對應的處理器

  • 3處,構造要傳入處理器的參數上下文,包括了當前指令、操做數棧、本地變量表、常量池等

  • 4處,調用第二步的處理器的execute方法,傳入第三步的參數;將執行結果賦值給局部變量

    instructionExecutionResult。

  • 5處,若是返回結果爲null,說明不須要跳轉,則將當前指令的next,賦值給當前指令。

    if (instructionExecutionResult == null) {
                    currentInstruction = currentInstruction.getNextInstruction();
  • 6處,若是返回結果不爲空,且是return指令,則直接返回結果

  • 7處,若是返回結果不爲空,且是拋出了異常,則將異常繼續拋出

  • 8處,若是返回結果不爲空,好比遇到goto 指令,處理器返回時,會在instructionExecutionResult的instructionSequenceNum字段,設置要跳轉到的指令;則查找到該指令,賦值給currentInstruction

如何根據字節碼指令,查找處理器

定義了一個通用的處理器:

public interface ExecutorByOpCode {
    String getOpCode();

    /**
     *
     * @param context
     * @return 若是須要跳轉,則返回要跳轉的指令的偏移量;不然返回null
     */
    InstructionExecutionResult execute(InstructionExecutionContext context);
}

而後,我這邊針對各類指令,寫了一堆實現類:

拿一個最簡單的iconst_0舉例:

@Component
public class ExecutorForIConst0 extends BaseExecutorForIConstN implements ExecutorByOpCode{

    @Override
    public String getOpCode() {
        return OpCodeEnum.iconst_0.name();
    }

    @Override
    public InstructionExecutionResult execute(InstructionExecutionContext context) {
        super.execute(context, 0);
        return null;
    }
}

public class BaseExecutorForIConstN {
	// 1 
    public void execute(InstructionExecutionContext context,Integer counter) {
        context.getOperandStack().addLast(counter);
    }
}
  • 1處,將常量0,壓入操做數棧。

每一個字節碼處理器,都註解了@Component,而後在執行引擎類中,注入了所有的處理器:

@Component
@Slf4j
public class MethodExecutionEngine implements InitializingBean {
    ClassInfo classInfo;
	
    // 1
    @Autowired
    private List<ExecutorByOpCode> executorByOpCodes;
  	
  	private Map<String, ExecutorByOpCode> executorByOpCodeMap = new HashMap<>();
	
  // 2
  @Override
    public void afterPropertiesSet() throws Exception {
        if (executorByOpCodes != null) {
            for (ExecutorByOpCode executorByOpCode : executorByOpCodes) {
                executorByOpCodeMap.put(executorByOpCode.getOpCode().toLowerCase(), executorByOpCode);
            }

        }
    }
  • 1處,注入所有的處理器
  • 2處,將處理器寫入map,key:字節碼指令;value:處理器自己。
  • 後續執行引擎,就能夠根據字節碼指令,查找到對應的處理器。

遍歷讀取文件全部行,採用visitor模式回調visitor接口

就是普通的讀文件,寫得比較隨意,讀成了行的集合。

String filepath = "F:\\ownprojects\\all-simple-demo-in-work\\class-bytecode-analyse-engine\\target\\classes\\com\\yn\\sample\\a.txt";
        JavapClassFileParser javapClassFileParser = context.getBean(JavapClassFileParser.class);
        ClassInfo classInfo = javapClassFileParser.parse(filepath);

在parse方法內,代碼以下:

// 1	
		lines = FileReaderUtil.readFile2Lines(filePath);
        if (CollectionUtils.isEmpty(lines)) {
            return null;
        }
		
		// 2
        ClassMethodCodeVisitor classMethodCodeVisitor = null;
        for (int i = 0; i < lines.size(); i++) {
            String currentLine = lines.get(i);
            if (i == 0) {
              ...
  • 1處,讀取文件,獲取所有行

  • 遍歷全部行,這塊寫得比較亂一點,好比,當前行包含了「Constant pool:」時,將當前解析狀態修改成常量池解析開始

    /**
     * 當本行包含Constant pool:時,接下來就是一堆的常量:
     * Constant pool:
     *    #1 = Methodref          #6.#25         //  java/lang/Object."<init>":()V
     *    #2 = Fieldref           #5.#26         //  com/yn/sample/CheckAndSet.f:I
     * 切換狀態到常量池解析開始的狀態
     */
    if (currentLine.contains("Constant pool:")) {
        classConstantPoolInfoVisitor.visitConstantPoolStarted();
        state = ParseStateEnum.CONSTANT_POOL_STARTED.state;
        continue;
    }

    下一次循環,就會進入解析狀態爲常量池解析開始時的邏輯:

    if (state == ParseStateEnum.CONSTANT_POOL_STARTED.state) {
      // 1.
      ConstantPoolItem item = ParseEngineHelper.parseConstantPoolItem(currentLine);
      if (item == null) {
    	// 2.
        classConstantPoolInfoVisitor.visitConstantPoolEnd();
        state = ParseStateEnum.METHOD_INFO_STARTED.state;
        continue;
      } else {
        // 3
        classConstantPoolInfoVisitor.visitConstantPoolItem(item);
        continue;
      }
    }
    • 1處,當前行的格式應該爲,

      #1 = Methodref #6.#26 // java/lang/Object."<init>":()V

      根據正則,解析當前行爲以下結構:

      public class ConstantPoolItem {
          /**
           * 格式如:
           * #1
           */
          private String id;
      
          /**
           * 如:
           * Methodref
           */
          private ConstantPoolItemTypeEnum constantPoolItemTypeEnum;
      
          /**
           * #6.#25
           */
          private String value;
      
          /**
           * 對於value的註釋,由於value字段通常就是對常量池的id引用,
           * javap反編譯後,爲了方便你們閱讀,這裏會顯示爲相應的常量
           */
          private String comment;
      }
    • 2處,若是返回的常量池對象爲null,說明當前常量池解析結束,則修改解析狀態爲:方法解析開始

    • 3處,若是解析出來了常量池對象,則回調visitor接口。

在解析過程當中,會不斷回調咱們的visitor接口,好比:

package com.yn.sample.visitor;

import com.yn.sample.domain.ConstantPoolItem;

import java.util.ArrayList;

public interface ClassConstantPoolInfoVisitor {
    /**
     * 常量池解析開始
     */
    void visitConstantPoolStarted();

    /**
     * 解析到每個常量池對象時,回調本方法
     * @param constantPoolItem
     */
    void visitConstantPoolItem(ConstantPoolItem constantPoolItem);

    /**
     * 常量池解析結束
     */
    void visitConstantPoolEnd();

    /**
     * 獲取最終的常量池對象
     * @return
     */
    ArrayList<ConstantPoolItem> getConstantPoolItemList();
}

總體流程

  1. 讀取文件,獲取字節碼

    package com.yn.sample;
    
    
    @Component
    @ComponentScan("com.yn.sample")
    public class BootStrap {
        public static void main(String[] args) throws Throwable {
            AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(BootStrap.class);
            /**
             * 解析文件
             */
            String filepath = "F:\\ownprojects\\all-simple-demo-in-work\\class-bytecode-analyse-engine\\target\\classes\\com\\yn\\sample\\a.txt";
            JavapClassFileParser javapClassFileParser = context.getBean(JavapClassFileParser.class);
            ClassInfo classInfo = javapClassFileParser.parse(filepath);
    
        }
    }

    字節碼讀取後,存在classInfo中。

  2. 調用CheckAndSet類的實例的checkAndSetF(int)接口,參數爲12,即,調用以下方法:

    public void checkAndSetF(int f) {
            if (f >= 0) {
                this.f = f;
            } else {
                throw new IllegalArgumentException();
            }
        }
  3. 構造本地變量list、操做數棧

    private Object doExecute(Object target, MethodInfo methodInfo,
                             List<ConstantPoolItem> constantPoolItems, List<Object> arguments) throws Throwable {
        List<MethodInstructionVO> instructionVOList = methodInfo.getInstructionVOList();
        /**
         * 構造next字段,將字節碼指令list轉變爲鏈表
         */
        assemblyInstructionList2LinkedList(instructionVOList);
    
        /**
         * 本地變量表,按照從javap中解析出來的:
         *     Code:
         *       stack=1, locals=4, args_size=2
         * 來建立本地變量的堆棧
         */
        Integer localVariablesSize = methodInfo.getMethodCodeStackSizeAndLocalVariablesTableSize().getLocalVariablesSize();
        List<Object> localVariables = constructLocalVariableList(target, arguments, localVariablesSize);
    
        /**
         * 構造指令map,方便後續跳轉指令使用
         * key:指令的sequenceNum
         * value:指令
         */
        HashMap<String, MethodInstructionVO> instructionVOHashMap = new HashMap<>();
        for (MethodInstructionVO vo : instructionVOList) {
            instructionVOHashMap.put(vo.getSequenceNumber(), vo);
        }
    
    
        return null;
    }
  4. 調用執行引擎逐行解釋執行字節碼

    這部分參見前面,已經講過。

總結

源碼放在:

https://gitee.com/ckl111/class-bytecode-analyse-engine

目前沒實現的有:

  1. 方法調用方法,只支持調用單個方法。方法堆棧待實現。
  2. 不少其餘各類指令

目前只能執行下面這個類中的方法,後續遇到其餘字節碼指令,再慢慢加吧:

後續有時間再寫其餘的吧,若是你們有興趣,能夠本身寫。

相關文章
相關標籤/搜索