AREX 是一款开源的基于实在申请与数据的自动化回归测试平台,利用 Java Agent 字节码注入技术,通过在生产环境录制和存储申请、应答数据,并在测试环境回放申请和注入 Mock 数据,存储新的应答,实现了主动录制、主动回放、主动比对,为接口回归测试提供便当。
AREX Mock 性能非常弱小,不仅反对各种支流技术框架的主动数据采集和 Mock,还反对了本地工夫、缓存数据以及各种内存数据的采集和 Mock,能够做到在回放时精准还原生产执行时的数据环境,且不会产生脏数据。
这篇文档将从代码实现的角度简略介绍下 AREX 是如何实现在流量回放时主动 Mock 数据的。
示例
让咱们先以一个简略的函数为例,了解⼀下其实现原理。假设咱们有上面⼀个函数,用于将给定的 IP 字符串转换成整型,代码如下:
public Integer parseIp(String ip) { int result = 0; if (checkFormat(ip)) { // 查看IP串是否非法 String[] ipArray = ip.split("\\."); for (int i = 0; i < ipArray.length; i++) { result = result << 8; result += Integer.parseInt(ipArray[i]); } } return result;}
咱们将从两个方面阐明如何实现该函数的流量回放性能:
- ecord(流量采集)
当这个函数被调用时,咱们把对应的申请参数和返回后果保留下来,供前面流量回放应用,代码如下:
if (needRecord()) { // 数据采集,将参数和执⾏后果保留进DB DataService.save("parseIp", ip, result);}
- Replay(流量回放)
在进行流量回放时,就能够用之前采集的数据来主动实现这个函数的 Mock,代码如下:
if (needReplay()) { return DataService.query("parseIp", ip);}
通过查看残缺的代码,咱们能够更好地了解其实现逻辑:
public Integer parseIp(String ip) { if (needReplay()) { // 回放的场景,使⽤采集的数据做为返回后果,也就是 Mock return DataService.query("parseIp", ip); } int result = 0; if (checkFormat(ip)) { String[] ipArray = ip.split("\\."); for (int i = 0; i < ipArray.length; i++) { result = result << 8; result += Integer.parseInt(ipArray[i]); } } if (needRecord()) { // 录制的场景,将参数和执⾏后果保留进到数据库 DataService.save("pareseIp", ip, result); } return result;}
AREX 中的具体实现
AREX 实现的原理相似,不过会更简单⼀些,不须要开发人员手动在业务代码中增加录制和回放的代码。arex-agent
会在利用启动时,在须要的代码块中主动增加相应的代码来实现这个性能。这里以 MyBatis3 的 Query 为例,看看 AREX 中的具体实现。
浏览过 MyBatis 源码的应该都理解,Query 的操作都会收束在 org.apache.ibatis.executor.BaseExecutor
类的 query 办法上(Batch 操作除外),这个办法的签名如下:
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException
这⾥蕴含了执行的 SQL 和参数,函数的后果蕴含了从数据库中查到的数据,显然在这里执行数据采集是适合的,在回放的时候也能够用采集的数据作为后果返回,从而防止理论的数据库操作。看看 AREX 中的代码,为了便于了解,这里做了⼀定的简化,如下:
public class ExecutorInstrumentation extends TypeInstrumentation { @Override protected ElementMatcher<TypeDescription> typeMatcher() { // 须要进行代码注入的类全名 return named("org.apache.ibatis.executor.BaseExecutor"); } @Override public List<MethodInstrumentation> methodAdvices() { // 须要进行代码注入的办法名,因为query办法存在多个重载,所以带上了参数验证 return Collections.singletonList(new MethodInstrumentation( named("query").and(isPublic()) .and(takesArguments(6)) .and(takesArgument(0, named("org.apache.ibatis.mapping.MappedStatement"))) .and(takesArgument(1, Object.class)) .and(takesArgument(5, named("org.apache.ibatis.mapping.BoundSql"))), QueryAdvice.class.getName()) ); } // 注入的代码 public static class QueryAdvice { @Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue.class, suppress = Throwable.class) public static boolean onMethodEnter(@Advice.Argument(0) MappedStatement var1, @Advice.Argument(1) Object var2, @Advice.Argument(5) BoundSql boundSql, @Advice.Local("mockResult") MockResult mockResult) { RepeatedCollectManager.enter(); // 避免嵌套调用导致的数据反复采集 if (ContextManager.needReplay()) { mockResult = InternalExecutor.replay(var1, var2, boundSql, "query"); } return mockResult != null; } @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) public static void onMethodExit(@Advice.Argument(0) MappedStatement var1, @Advice.Argument(1) Object var2, @Advice.Argument(5) BoundSql boundSql, @Advice.Thrown(readOnly = false) Throwable throwable, @Advice.Return(readOnly = false) List<?> result, @Advice.Local("mockResult") MockResult mockResult) { if (!RepeatedCollectManager.exitAndValidate()) { return; } if (mockResult != null) { if (mockResult.getThrowable() != null) { throwable = mockResult.getThrowable(); } else { result = (List<?>) mockResult.getResult(); } return; } if (ContextManager.needRecord()) { InternalExecutor.record(var1, var2, boundSql, result, throwable, "query"); } } }}
其中 QueryAdvice
是须要在 query
办法中注入的代码。通过 onMethodEnter
注入的代码会在办法最开始地地位执行,而 onMethodExit
注入的代码则会在函数返回后果之前执行。
单纯地看这个可能比拟难于了解,咱们把注入代码后的 BaseExecutor
的 query 办法的代码 dump下来进行剖析,如下:
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { MockResult mockResult = null; boolean skipOk; try { RepeatedCollectManager.enter(); if (ContextManager.needReplay()) { mockResult = InternalExecutor.replay(ms, parameter, boundSql, "query"); } skipOk = mockResult != null; } catch (Throwable var28) { var28.printStackTrace(); skipOk = false; } List result; Throwable throwable; if (skipOk) { // 重放的场景,不再执行原来的 query 办法体 result = null; } else { try { // BaseExecutor query 办法的原代码,此处省略,惟一会被调整的就是原办法里 return 的代码,会被批改为将后果赋值给 result result = list; } catch (Throwable var27) { throwable = var27; result = null; } } try { if (mockResult != null) { if (mockResult.getThrowable() != null) { throwable = mockResult.getThrowable(); } else { result = (List)mockResult.getResult(); } } else if (RepeatedCollectManager.exitAndValidate() && ContextManager.needRecord()) { InternalExecutor.record(ms, parameter, boundSql, result, throwable, "query"); } } catch (Throwable var26) { var26.printStackTrace(); } if (throwable != null) { throw throwable; } else { return result; } }
能够看到 onMethodEnter 和 onMethodExit 里的代码被插⼊到了结尾和结尾,再来了解下这段代码:
- 录制的场景
AREX 会判断这次拜访数据是否须要录制(服务收到申请时,AREX 会依据配置的录制频率决定是否对这个申请进行录制,如果判断为须要录制,则这个申请执行过程中所有的内部依赖都会被录制,具体实现细节这里不做介绍了)。录制过程中,AREX 会调用 InternalExecutor.record(ms, parameter, boundSql, result, throwable, "query")
办法,将本次数据库拜访的后果、外围参数等信息存入AREX的数据库中,实现对该数据库拜访的录制。
- 回放的场景
从下面的代码能够看到,当把后面录制的申请再次发送给对应服务时,AREX 会将其视为回放,此时不会再执行原函数的代码了,而是间接返回之前录制下来的后果(包含过后异样的还原),通过调用 InternalExecutor.replay(ms, parameter, boundSql, "query”)
能够获取之前保留的录制数据。
内存数据的 Record\&Replay(动静类)
当然,后面示例的函数是幂等的,对于幂等函数而言,因为每次调用时,其返回后果始终雷同,不会受到内部因素的影响,因而在录制和回放过程中并不需要进行数据的采集和 Mock。
相同,对于非幂等的函数,每次调用的后果可能会受到外部环境的影响,并且执行后果会影响服务输入(例如各种本地缓存,不同的环境数据可能不同,从而影响输入后果)。在这种状况下,AREX 也提供配置动静类这种机制来实现这部分数据的 Record 和 Mock 性能,具体能够在 Setting 子菜单的 Record 配置项中配置:
在这里顺次配置类名、办法名(非必须,不配置的话将会利用于所有有参数和返回值的公共办法)、参数类型(非必须)。配置实现后,arex-agent
将会主动在对应的办法中注入相似下面的 Record\&Replay 代码,从而实现数据的采集和回放时的 Mock 性能。