Java Agent+Javassist实现零侵入mock

前言

最早接触“零侵入”一词,源于笔者参加美团举办的测试技术沙龙活动。活动上,去哪儿网的童鞋介绍其自主研发的接口自动化测试框架Qunit时,提到了一项关键技术:零侵入切面技术,该技术方案最大优点是:无需修改代码实现mock功能,举例说明如下。
假如被测接口里面调用了第三方接口,由于第三方接口的不确定性,对于某些测试场景(比如请求超时、特定错误码测试等),测试人员往往需要开发人员添加mock来配合测试,这种工作效率相对来说是比较低的,而且也不利于自动化测试的开展。


真实接口请求

零侵入技术把mock主动权交接给测试人员管理,无需开发再去修改代码、部署测试环境等一系列动作。测试人员只需根据具体的测试场景编写对应三方接口的mock脚本,启动mock服务即可。通过灵活编写mock脚本,我们可以覆盖各种特殊的测试场景。
比如需要在系统测试环境mock上图的“第三方接口1”,让其返回超时。测试人员只需编写mock1脚本,启动mock服务,请求“被测试接口”时即可触发调用mock server,而非真实接口“第三方接口1”,整个过程并没有修改被测接口任何代码。


零侵入mock

同理,如果想同时mock“第三方接口1”和“第三方接口2”,只需再编写一个mock2脚本,以此类推。
零侵入mock

零侵入实现原理

java运行原理(ps:此图网上找的)

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步


    file-project structure
  • 第2步


    add jar
  • 第3步,修改路径。


    修改为resource目录
  • 第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。


    导出jar包
  • 第6步


    build-build artifacts

    build
  • 第7步,build完成后,out目录下已导出了对应的jar包


    javaAgent.jar

除了上述的打包方式,亦可通过pom配置自动打包。

<?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>com.demo.javassist</groupId>
    <artifactId>javassist</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <springframework.version>4.3.8.RELEASE</springframework.version>
    </properties>

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

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>${springframework.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>${springframework.version}</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>

            <plugin>
                <artifactId>maven-assembly-plugin</artifactId>
                <configuration>
                    <archive>
                        <manifestEntries>
                            <Premain-Class>MyAgent</Premain-Class>
                        </manifestEntries>
                    </archive>
                    <appendAssemblyId>false</appendAssemblyId>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>

                </configuration>
                <executions>
                    <execution>
                        <id>make-assembly</id>
                        <phase>package</phase>
                        <goals>
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>
自动打包

双击package后,target目录下生成打包文件。

3、配置tomcat启动参数
  • 增加以下启动参数。
-javaagent:D:\gittest_pro\javaAgent\out\artifacts\javaAgent_jar\javaAgent.jar
tomcat启动参数
  • 启动tomcat


    启动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、接口请求

发送接口请求


http请求

查看MsgServiceAdapter.class文件,可发现java agent确实发挥了作用,url被重新赋值。


class字节码修改成功

查看控制台日志,可发现请求第三方接口toSendSmsBySingle时,确实返回了mock的响应报文,并没有去请求真实的第三方接口。
mock成功

总结

无论是手工测试,还是自动化测试,零侵入mock技术无疑都有大量的应用场景,但要用好这门技术却不是一件容易的事,任何技术的应用都是一个循序渐进、挖坑填坑的过程,笔者也在专研中。

相关学习资料

去哪儿自动化测试框架Qunit中的零侵入切面技术应用及分布式运行平台
深入理解JVM之Java字节码(.class)文件详解
Javassist 操作手册
Javassist 使用指南(一)
Javassist 使用指南(二)
Javassist 使用指南(三)
Java动态编程之javassist
JAVA AOP编程之:Javassist

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345