Java Agent+Javassist實現零侵入mock

前言

最早接觸“零侵入”一詞,源於筆者參加美團舉辦的測試技術沙龍活動。活動上,去哪兒網的童鞋介紹其自主研發的接口自動化測試框架Qunit時,提到了一項關鍵技術:零侵入切面技術,該技術方案最大優點是:無需修改代碼實現mock功能,舉例說明如下。
假如被測接口裏面調用了第三方接口,由於第三方接口的不確定性,對於某些測試場景(比如請求超時、特定錯誤碼測試等),測試人員往往需要開發人員添加mock來配合測試,這種工作效率相對來說是比較低的,而且也不利於自動化測試的開展。



零侵入技術把mock主動權交接給測試人員管理,無需開發再去修改代碼、部署測試環境等一系列動作。測試人員只需根據具體的測試場景編寫對應三方接口的mock腳本,啓動mock服務即可。通過靈活編寫mock腳本,我們可以覆蓋各種特殊的測試場景。
比如需要在系統測試環境mock上圖的“第三方接口1”,讓其返回超時。測試人員只需編寫mock1腳本,啓動mock服務,請求“被測試接口”時即可觸發調用mock server,而非真實接口“第三方接口1”,整個過程並沒有修改被測接口任何代碼。



同理,如果想同時mock“第三方接口1”和“第三方接口2”,只需再編寫一個mock2腳本,以此類推。

零侵入實現原理


Java程序運行時,必須經過編譯和運行兩個步驟。首先將後綴名爲.java的源文件進行編譯,最終生成.class的字節碼文件,然後將字節碼文件加載到內存進行解析執行。零侵入技術要做的就是在.class文件被加載前,對其進行修改,以達到我們的目的。字節碼修改工具有ASM、Javassist等,接下來筆者將基於Java Agent+Javassist來實現一個簡單的零侵入mock測試場景,對於更復雜的應用場景,有興趣的童鞋可深入專研。

Java Agent介紹

JavaAgent 是運行在 main方法之前的攔截器,其內定的方法名是premain,也就是說先執行premain方法,然後再執行main方法。通過增加premain方法,即可實現一個JavaAgent。

Javassist介紹

Javassist是一個開源的分析、編輯和創建Java字節碼的類庫。關於java字節碼的處理,目前有很多工具,如bcel,asm。不過這些都需要直接跟虛擬機指令打交道。如果你不想了解虛擬機指令,可以採用javassist。javassist是jboss的一個子項目,其主要的優點在於簡單,而且快速。直接使用java編碼的形式,而不需要了解虛擬機指令,就能動態改變類的結構,或者動態生成類。

案例

發短信接口sendMsg調用了第三方接口toSendSmsBySingle,下面通過零侵入的方式實現第三方接口返回指定的響應報文。

1、編寫agent

pom.xml配置

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>JavaAgent</groupId>
    <artifactId>javaAgent</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.20.0-GA</version>
        </dependency>
    </dependencies>

</project>

編寫premain方法邏輯。

import java.lang.instrument.Instrumentation;

public class MyAgent {

    public static void premain(String agentOps, Instrumentation inst) {
        System.out.println("=========premain方法執行========");
        //System.out.println(agentOps);
        // 添加Transformer
        inst.addTransformer(new ClassFileTransformerImp());
    }
}

編寫ClassFileTransformer的實現ClassFileTransformerImp,主要功能是使用javassist來修改字節碼文件,在第40行通過插入“url = http://localhost:8187/v1/toSendSmsBySingle;”來改變代碼中url的值,從而請求mockserver,其中localhost:8187爲下文提到的mockserver地址。

import javassist.*;

import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;

public class ClassFileTransformerImp implements ClassFileTransformer {

    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {

        if (className.equals("com.bank.iiacc.adapter.MsgServiceAdapter")) {
            try {
                System.out.println("類名:" + className);
                ClassPool cPool = new ClassPool(true);
                //設置class文件的位置,實際運用時應替換爲相對路徑
                cPool.insertClassPath("D:\\gittest_pro\\iiAccount\\iiAccount-adapter\\target\\classes");
                //獲取該class對象
                CtClass cClass = cPool.get("com.bank.iiacc.adapter.MsgServiceAdapter");
                //獲取到對應的方法
                CtMethod cMethod = cClass.getDeclaredMethod("sendMsg");
                //通過insertAt可引用局部變量。
                cMethod.insertAt(40, "{url = \"http://localhost:8187/v1/toSendSmsBySingle\";}");

                //替換原有的文件,實際運用時應替換爲相對路徑
                cClass.writeFile("D:\\gittest_pro\\iiAccount\\iiAccount-adapter\\target\\classes");
                System.out.println("=======修改完成=========");
            } catch (NotFoundException e) {
                e.printStackTrace();
            } catch (CannotCompileException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return null;
    }
}
2、agent打包

常見的打包技術參考idea打包jar的多種方式,以下介紹其中一種方式。

  • 第1步


  • 第2步


  • 第3步,修改路徑。


  • 第4步
    修改resources目錄下的MANIFEST.MF文件,增加第2、3行內容。
Manifest-Version: 1.0
Premain-Class: MyAgent         //增加第1點的MyAgent類路徑
Can-Redefine-Classes: true     //增加
Class-Path: javassist-3.20.0-GA.jar
Main-Class: 
  • 第5步,點擊ok。


  • 第6步



  • 第7步,build完成後,out目錄下已導出了對應的jar包


3、配置tomcat啓動參數
  • 增加以下啓動參數。
-javaagent:D:\gittest_pro\javaAgent\out\artifacts\javaAgent_jar\javaAgent.jar
  • 啓動tomcat


4、編寫mock腳本

以moutebank舉例,詳情參考筆者另外一篇文章《Mock service之Mountebank入門》

  • main.ejs腳本如下。
{
  "imposters": [
    <% include proxy.ejs %>,
    <% include iiacct.ejs %>
  ]
}
  • iiacc.ejs腳本如下。
{
    "port": 8187,
    "protocol": "http",
    "stubs": [
        <% include toSendSmsBySingle.ejs %>
    ]
}
  • toSendSmsBySingle.ejs腳本如下。
{
    "predicates": [
        {
            "contains": {
                "path": "/v1/toSendSmsBySingle"
            }
        }
    ],
    "responses": [
        {
            "is": {
                "statusCode": 500,
                "headers": {
                    "Server": "Apache-Coyote/1.1",
                    "Content-Type": "text/json;charset=UTF-8",
                    "Content-Length": 298,
                    "Date": "Tue, 05 Sep 2017 06:49:14 GMT",
                    "Connection": "close"
                },
                "body": "{\"data\":{\"errCode\":\"iia-trade-00010\",\"errMsg\":\"商戶不存在8888\"},\"message\":\"業務處理失敗\",\"status\":\"GW-10510\",\"sign\":\"6tbbBajxsMTsql1Gl/VSsI7BHilAvCtA9J0FGiN7+p3Nde7vwZVd9taneNIp4M1zsRhqXXHMFTp67ZFTUItcI8PB4UFnltXomCCW1Jya7dI+hpQilUs2rLQ1WcumGN3GqjWaE472FQbOX2muzcUjJbsMosTo+P0SPawhO5m83Uw=\"}",
                "_mode": "text",
                "_proxyResponseTime": 135
            }
        }
    ]
}
5、啓動mock服務

啓動moutebank。

mb --configfile d:\mountebank_ejs\main.ejs --allowInjection
6、接口請求

發送接口請求



查看MsgServiceAdapter.class文件,可發現java agent確實發揮了作用,url被重新賦值。



查看控制檯日誌,可發現請求第三方接口toSendSmsBySingle時,確實返回了mock的響應報文,並沒有去請求真實的第三方接口。

總結

無論是手工測試,還是自動化測試,零侵入mock技術無疑都有大量的應用場景,但要用好這門技術卻不是一件容易的事,任何技術的應用都是一個循序漸進、挖坑填坑的過程,筆者也在專研中。

相關學習資料

去哪兒自動化測試框架Qunit中的零侵入切面技術應用及分佈式運行平臺
深入理解JVM之Java字節碼(.class)文件詳解
Javassist 操作手冊
Javassist 使用指南(一)
Javassist 使用指南(二)
Javassist 使用指南(三)
Java動態編程之javassist
JAVA AOP編程之:Javassist

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