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
- 点赞
- 收藏
- 关注作者
评论(0)