基于JavaAgent的Mock和回放系统

主要解决的问题

  • 依赖的第三方系统不稳定等情况,影响开发和测试流程
  • UI测试,压力测试等去除IO操作

目前市场上集几种Mock方案以及分析

  1. 在代码中Coding代码逻辑。
  2. 使用网络代理,将服务代理到指定服务器(JVM Proxy参数)。
  3. 修改注册中心,将相应的服务地址修改到Mock服务。
  4. 使用JavaAgent 修改字节码,将相应的IO的地方修改到某些地方。

分析

现有的主流Mock方案有上面的列出几种,尽管可以达到Mock的目的,但是存在和现有业务代码耦合性大,功能匮乏,对mock掌控力能力弱等问题。此时,基于JavaAgent修改Class文件字节码的方式的优势就显得比较突出了,可以和应用完全解偶,完全对用户无感知的进行一些逻辑修改。

JavaAgent

JavaAgent技术是在JDK5增加,JDK6完善AttachApi 的一项可以控制JVM加载Class文件行为的一系列API。在JVM层依赖于JVMTI,JVMTI是一系列可以描述和JVM虚拟机执行状态的API,有兴趣的同学可以去了解下在Hotspot的具体实现。

JavaAgent类的加载有两种方式:

  1. JVM未启动,可以通过premain方式进行加载。
  2. JVM已经启动,可以通过Attach的方式通过agentmain方式加载。

Class类文件加载机制

Java 从代码文件到程序运行主要由三个阶段组成。

  1. 编译阶段。将Java,Groovy,Kotlin等文件编译成Class格式文件,此阶段依赖于javac,kotlinc等工具。
  2. 加载阶段。将Class文件加载进JVM,以Class对象的形式存储存储在JVM,JDK7存储在方法区,JDK8存储在Metaspace区。
  3. 解释执行阶段。这个阶段为程序的运行阶段,JVM执行方式为解释执行。为了提高运行效率,之后添加JIT热点代码的缓存来提高代码执行效率,近期Spring尝试一项已经GraalVM的项目,可以将Class文件编译成操作系统平台的二进制代码,从而从根本上解决JVM类语言JIT过久,导致应用短时间内CPU利用率上升,应用拒绝服务的情况。

类加载阶段需要经过下面几个阶段

  • =》 加载
  • =》 验证(验证Class文件格式正确性)
  • =》 准备(给静态变量区等分配空间)
  • =》 解析(将符号引用转化为直接引用)
  • =》 初始化(执行静态属性 -> 静态方法块 -> 属性 -> 构造方法的初始化)
  • =》 使用
  • =》 卸载(将类定义信息从虚拟机中卸载,主要发生在JVM关闭期间)

JavaAgent操作执行在类加载阶段,在类加载进JVM生成Class对象之前,修改Class文件的定义。

系统加载机制以及Mock加载方案

基于JavaAgent的Mock和回放系统_第1张图片

  • 通过Agent对IO Point进行修改。通过反射调用的方式对Mocker对象进行调用。
  • 通过自定义Class类路径来进行对相关类对象和Job,Schedule进行加载。
  • JavaAgent负责对自定义类加载器加载的对象和WebappClassloader应用类加载器加载的对象进行粘合。

Mocker对象

  • Mocker对象主要封装判断Mock的可用性,返回Mock报文等整个。

基于JavaAgent的Mock和回放系统_第2张图片

  • =》 初始化Mocker对象;
  • =》 反射注入相关属性对象;
  • =》 反射调用construct构造相关参数,生成Service唯一Key,并将Service信息和本地相同Key的Service信息进行比对,如果缓存中已经存在,则使用缓存中的配置信息;如果给不存在则将服务信息添加到本地缓存,并且不断于服务端信息进行同步;
  • =》 在指定真实调用逻辑前面来调用available来判断是否可以调用。如果返回false则执行真实调用,否则进入Mock逻辑;
  • =》 调用 execute逻辑,返回Mock报文,如果是异步则封装成异步对象;

下面是一段可以用来进行MockDubbo接口的Demo

 StringBuilder nivInsertCode = new StringBuilder();
            nivInsertCode.append("public org.apache.dubbo.rpc.Result invoke(org.apache.dubbo.rpc.Invocation invocation) throws org.apache.dubbo.rpc.RpcException {\r\n")
                    .append("Object __mocker = com.snake.agent.util.Reflection.createInstance(\"com.snake.mocker.Mocker\",\"").append(snkCp).append("\");\r\n")
                    .append("{\r\n")
                    .append("com.snake.agent.util.Reflection.setField(__mocker,\"respClazzS\",((org.apache.dubbo.rpc.RpcInvocation)$1).getReturnType().getName());\r\n")
                    .append("com.snake.agent.util.Reflection.setField(__mocker,\"clazzS\",this.invoker.getInterface().getName());\r\n")
                    .append("com.snake.agent.util.Reflection.setField(__mocker,\"operation\",$1.getMethodName());\r\n")
                    .append("com.snake.agent.util.Reflection.setField(__mocker,\"request\",$1.getArguments()[0]);\r\n")
                    .append("com.snake.agent.util.Reflection.setField(__mocker,\"reqClazzS\",invocation.getParameterTypes()[0].getName());\r\n")
                    .append("com.snake.agent.util.Reflection.setField(__mocker,\"async\",new Boolean(com.alibaba.dubbo.rpc.support.RpcUtils.isAsync(this.invoker.getUrl(), $1)));\r\n")
                    .append("com.snake.agent.util.Reflection.setField(__mocker,\"isListenableFuture\",new Boolean(false));\r\n")
                    .append("com.snake.agent.util.Reflection.setField(__mocker,\"serviceId\",((org.apache.dubbo.registry.integration.RegistryDirectory) this.directory).getUrl().getParameter(\"serviceId\"));\r\n")
                    .append("com.snake.agent.util.Reflection.setField(__mocker,\"serviceTypeS\",\"CDUBBO\");\r\n")
                    .append("com.snake.agent.util.Reflection.setField(__mocker,\"namespace\",null);\r\n")
                    .append("com.snake.agent.util.Reflection.setField(__mocker,\"serviceName\",null);\r\n")
                    .append("   com.snake.agent.util.Reflection.invokeMethod(__mocker,\"construct\");\r\n")
                    .append("   if(new Boolean(true).equals(com.snake.agent.util.Reflection.invokeMethod(__mocker,\"available\"))){\r\n")
                    .append("       return new org.apache.dubbo.rpc.AppResponse(com.snake.agent.util.Reflection.invokeMethod(__mocker,\"executed\"));\r\n")
                    .append("   }\r\n")
                    .append("}\r\n")
                    .append("   Object _r = _invoke($$);")
                    .append("{\r\n")
                    .append("   if(new Boolean(true).equals(com.snake.agent.util.Reflection.invokeMethod(__mocker,\"recording\"))){\r\n")
                    .append("      com.snake.agent.util.Reflection.invokeMethod(__mocker,\"record\",((org.apache.dubbo.rpc.Result)_r).getValue());\r\n")
                    .append("   }\r\n")
                    .append("   return _r;")
                    .append("}\r\n")
                    .append("}\r\n");
            CtMethod niv = CtMethod.make(nivInsertCode.toString(), ctClass);

针对每一个Mocker对象在construct过程中,会根据ClassName,MethodName,ParamTypesName生成一个唯一的方法签名,作为改IO接口的唯一性标识。
并且将Key和服务信息cache本地ConfCacheManager。

SyncServiceSchedule

Sync ServiceSchedule会在项目启动过程中启动运行, 主要负责将本地ConfCacheManager中的Service信息发送到服务端,并且同步相关service服务端配置信息到客户端本地。包括异步获取报文的版本信息,Mock开发录制开关等配置信息。

PullDataJob

PullDataJob主要负责获取Mock报文。主要由同步获取和异步获取两种。

  • 同步。在available阶段获取报文到本地,在execute阶段反序列化报文并返回。
  • 异步。异步模式下,会将需要同步的Service信息添加到本地的队列当中。每次SyncService信息的时候会去异步获取Mock报文Cache的本地,在execute阶段进行反序列化并返回。

MessageSender

MessageSender主要负责发送录制的报文,生成的SpanMessage,代码执行分析等信息。

本模块参照Skywalking data-carrier实现了一套本地异步发送队列(data-wharf),将需要发送的消息事先发送到data-wharf,然后通过Consumer线程不断消费队列里面的消息。实现消息的发送和外部IO有一层Cache,避免由于第三方组件不稳定导致的应用堵塞。

服务端

服务端主要负责用于一些操作和数据的存储工作

  1. MockService和Mock报文的配置;
  2. 流量回放HBase和Mongo存储的管理;
  3. 录制的报文的消费、存储等;
  4. 暴露一系列给第三方调用的API;

结语

通过JavaAgent实现的Mock系统,很好的实现了Mock逻辑和应用逻辑的剥离,使接入方能够更加方便的接入系统。系统开发者可以对使用方应用具有更加完整的掌控能力,并且可以实现无感的报文录制,报文场景的回放等经常使用的自动化功能。并且对后期的功能规划起到了一个很大的扩容,可以更方便的接入压力测试,性能监控等功能。并且可以和自研开发的UT框架实现无缝嵌合,实现SOA等的Mock报文获取

你可能感兴趣的:(中间件,Java,架构)