• 整合JVM-SANDBOX与VMTOOL,实现支持OGNL的增强自定义MOCK


    背景

    最近在公司开发基于JVM的“流量录制回放的工具”,简单理解是把生产环境用户的请求响应包括过程中的含状态子组件,把整个过程入参和返回值录制下来,到测试环境一一回放Mock,达到测试接口子调用覆盖率,上线前不同代码版本运行情况对比等目的,提供更多信息帮助测试和开发同学提高系统健壮性。市面同行的类似方案

    简单流程:

    拦截JVM的方案,使用了阿里的 jvm-sandbox , 原理是attach(或agent形式启动)到运行中的JVM,利用Java提供的InstrumentationApi,加载自定义模块,关注对应的拦截点,重新编织字节码到运行中的程序里。

    系统的其他部分,也引入了同样是阿里的Arthas Java工具,主要用到一些trace方法,反编译代码等功能。

    问题

    就工程复杂度来看,线上录制相对复杂度低,主要是系统间的调用,稳定性(过程中不影响正常业务),在运行一段时间考虑如何把流量逐一落盘存储即可。而到线下回放Mock的过程,则会遇到各种的问题,例如机器性能的差异, 组件不完整,网络问题等,其中最大的问题是,和录制环境状态的差异

    尽管目前工具拦截了的子调用组件(都是状态相关)已覆盖了日常大部分应用,但实际开发同学实施代码有各种不同的习惯与个例,回放时即时同样的代码,依然会导致有各种执行结果,例如:

    1. 子调用组件录制不全
    2. 直接使用本地缓存例如 ConcurrentHashMap
    3. 线上录制的一些时间参数与回放时不一致
    4. 调用一些非状态相关的ABTest SDK导致走向不同的代码路径
    5. 直接代码里写死与环境绑定的逻辑
    6. 配置中心不一致

    ....

    目前对回放结果的调优,参数/响应用了JsonPath的方式去忽略噪音,提高了命中率

    例如:

    1. // 入参例子
    2. // 录制到时间是昨天, 回放时把第一个参数忽略不加入匹配
    3. userMapper.selectListGreaterThanDateAndNicknameEq(new Date(), "jas");
    4. // 响应例子
    5. // 调用下游系统,例如个性化推荐,每次返回的结果长度不一样, 影响到下一个调用的入参或最终接口实际响应结果
    6. otherRecommandService.getRecommandList(userId)

    而其他通过忽略匹配规则没办法匹配到的情况,则使用需要使用自定义Mock的方式,去抹平系统环境状态差异

    例如如下这种情况,只要在测试环境回放时把envService.isProductionmock成true:

    1. if(envService.isProduction()) {
    2. // 执行正式环境代码...
    3. } else {
    4. // 执行dev环境代码...
    5. }

    例如如下这种情况,如果能把getUsermock成返回指定数据库里的某个用户:

    1. // 自实现缓存
    2. private static Map<String, User> userCache = new HashMap<>();
    3. private User getUser(String userId) {
    4. return userCache.get(userId);
    5. }

    如能达成以上的情况,则可大大方便测试的同学利用流量,在安全的测试环境里做各种链路的测试。在拦截方法方面,sandbox已可达到目的,而自定义Mock返回部分,结合到易上手与成熟度,本人则选择了OGNL去作为Mock返回值的方案,原因有下:

    1. 成熟的Apage项目, 在MyBatis等大库里均有使用 链接
    2. 上手非常简单,与系统的结合和验证都方便,所有语法两三分钟看完,对白盒测试的同学不成问题
    3. 返回值丰富, 基础类型/数组/Map/复杂JAVA对象/私有属性/静态变量等等等,都可以返回
    4. 过程运算支持变量,这可轻松地复用原有系统的基础逻辑

    简单演示:

    1. // 返回数组
    2. Ognl.getValue("{1,3,4,5}")
    3. // 返回Map
    4. Ognl.getValue("#{\"name\": \"JAS\"}")
    5. // map中带复杂对象变量
    6. Ognl.getValue("#user=new User(),#{\"user\": #user}")
    7. // 访问静态类静态
    8. Ognl.getValue("@ContextFactory@traceId")

    对于第4点,Ognl支持复用业务系统业务逻辑返回,我司大多业务系统都使用到Spring框架,熟悉Spring的朋友都知道,框架支持已注入的方式去获取ApplicationContext类的对象,只要获取到该对象,我们可以调用任意Bean的方法, 例如各种数据库mapperredisTemplate各种业务Service 都不在话下。

    1. // Spring原来获取ApplicationContext的方式, 通过Aop注入
    2. @Component
    3. public class SpringContextUtil implements ApplicationContextAware {
    4. private static ApplicationContext applicationContext;
    5. @Override
    6. public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
    7. SpringContextUtil.applicationContext = applicationContext;
    8. }
    9. }

    但实际我们的视觉是在Sandbox的classLoader里,这和业务运行的classLoader可理解是一个平衡宇宙,即时在sandbox中拦截到例如某个service的方法,但我们也没办法去获取到实例化好的ApplicationContext对象

    1. // sandbox的注入代码示例
    2. new EventWatchBuilder(moduleEventWatcher)
    3. .onClass("com.jasjojo.UserService") // 拦截类
    4. .onBehavior("getUser") //拦截方法
    5. .onWatch((event)-> {
    6. if(event.Type.equals(event.Before)) {
    7. event.getAdvice().getTarget() //获取到拦截的对象,但没办法从这个对象获得到SpringContext
    8. }
    9. })

    下面有请决方案ArthasVmTool

    vmtool 利用JVMTI接口,实现查询内存对象,强制 GC 等功能。

    我们可以用官方例子attach一下正在执行的Spring容器, 遍可以获得对象

    1. # 获取到执行中的ApplicationContext对象,后面也是ognl表达式
    2. $ vmtool --action getInstances --classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader --className org.springframework.context.ApplicationContext --express 'instances[0].getBeanDefinitionNames()'

    这一原理是调用了JAVA提供的JVMTI接口,但调用该接口需要用到C或C++, 并且如果使用该接口, 则需要考虑跨平台编译(兼容Mac/Windows/Linux), 运行时增加了复杂性,通过查看VmTool的源码,见到C相关核心代码如下

    1. // 对应同名nativejava方法
    2. extern "C"
    3. JNIEXPORT jobjectArray JNICALL
    4. Java_arthas_VmTool_getInstances0(JNIEnv *env, jclass thisClass, jclass klass, jint limit) {
    5. jlong tag = getTag();
    6. limitCounter.init(limit);
    7. jvmtiError error = jvmti->IterateOverInstancesOfClass(klass, JVMTI_HEAP_OBJECT_EITHER,
    8. HeapObjectCallback, &tag);
    9. if (error) {
    10. printf("ERROR: JVMTI IterateOverInstancesOfClass failed!%u\n", error);
    11. return NULL;
    12. }
    13. jint count = 0;
    14. jobject *instances;
    15. error = jvmti->GetObjectsWithTags(1, &tag, &count, &instances, NULL);
    16. if (error) {
    17. printf("ERROR: JVMTI GetObjectsWithTags failed!%u\n", error);
    18. return NULL;
    19. }
    20. jobjectArray array = env->NewObjectArray(count, klass, NULL);
    21. //添加元素到数组
    22. for (int i = 0; i < count; i++) {
    23. env->SetObjectArrayElement(array, i, instances[i]);
    24. }
    25. jvmti->Deallocate(reinterpret_cast<unsigned char *>(instances));
    26. return array;
    27. }

    简单理解是,只要在运行时,传入想要获取实例对象的Class,变回返回该Class在内存里所有已实例化的对象。自然我们在sandbox的拦截方法过程里,可以获取平衡宇宙业务classLoader里的Class引用,但更好的方法应该是sandbox-agent在初始化onLoad时,就尝试去vmtool获取有没有spinrg的ApplicationContext对象,这样可以提早作一些预判规则,并缓存起对象引用, 要达到这一点,我们只需要修改sandbox源码,让sandbox往下暴露Instrumentation对象即可。整体整合方案流程如下:

    各部分核心代码如下:

    1. // 修改Sandbox源码, 暴露Instrumentation
    2. public interface ModuleEventWatcher {
    3. //...
    4. // 其他实现类都继承这个方法暴露Instrumentation对象
    5. Instrumentation getInst();
    6. }
    7. // 通过instrumentation获取所有class
    8. public static Set<ClassLoader> getAllClassLoader(Instrumentation inst) {
    9. Set<ClassLoader> classLoaderSet = new HashSet<ClassLoader>();
    10. for (Class<?> clazz : inst.getAllLoadedClasses()) {
    11. ClassLoader classLoader = clazz.getClassLoader();
    12. if (classLoader != null) {
    13. classLoaderSet.add(classLoader);
    14. }
    15. }
    16. return classLoaderSet;
    17. }
    18. // VmTool通过Class去获取Spring ApplicationContext对象
    19. // libPath是编译好对应当前系统的c++库位置, 可参考VmtoolCommand,后续工程打包部分详谈
    20. public static synchronized VmTool getInstance(String libPath) {
    21. if (instance != null) {
    22. return instance;
    23. }
    24. if (libPath == null) {
    25. System.loadLibrary(JNI_LIBRARY_NAME);
    26. } else {
    27. System.load(libPath);
    28. }
    29. instance = new VmTool();
    30. return instance;
    31. }
    32. // 调用的native方法, 返回的范型就是对象列表
    33. private static synchronized native <T> T[] getInstances0(Class<T> klass, int limit);
    34. //Ognl获得Spring对象后的上下文设置,#spring就是一个可用的变量,指向ApplicationContext对象
    35. ClassResolver resolver = new ClassLoaderClassResolver(targetClassLoader);
    36. Map<String, Object> springMap = new HashMap();
    37. springMap.put("spring", springContext); // #spring对象
    38. OgnlContext ognlContext = new OgnlContext( new DefaultMemberAccess(true), resolver, new DefaultTypeConverter(), springMap);
    39. // 如此便可调用到userMapper的查询方法
    40. Object users = Ognl.getValue("#spring.getBean(\"userMapper\".selectList()[0])", ognlContext);

    最后的工程部分一些细节:

    1. 我当前时间2022年8月拉sandbox master最新代码,修改好jvm-sandbox打包编译运行,可能会遇到java.lang.NoClassDefFoundError: Could not initialize class com.alibaba.jvm.sandbox.core.enhance.weaver.SingleEventFactory, 参考pr#Comment
    2. vmtool引用的c++库,可以用arthas编译好的版本, 也可以用gcc自己编译支持苹果arm cpu等
    3. 修改源码的方式,像这种非pr的改动,运行稳定后,最好提交到私有仓, 同时维持两个remote分支。未稳定经大量测试时,可build成jar暂时放到resources lib方式去引用
    4. ognl和vmtool会在比较底层去暴露许多系统的调用,只能用在测试环境!!!!!

    最终大概效果:

  • 相关阅读:
    FL Studio水果2023版本更新下载汉化教程
    2023年【北京市安全员-A证】考试报名及北京市安全员-A证考试资料
    虚拟网络适配器的实现
    Spring的SmartLifecycle可以没用过,但没听过就不好了! - 第517篇
    json 相关
    JS 事件
    备战蓝桥杯,那你一定得打这场免费且有现金奖励的算法双周赛!
    Resultf风格接口
    使用阿里云国际版应该避免哪些操作?
    layui2.9.7-入门初学
  • 原文地址:https://blog.csdn.net/LBWNB_Java/article/details/126698628