流量回放repeater的原理分析一:sandbox源码分析

目录

      • 概述
        • 启动
        • 模块加载
        • 增强的完成
      • 参考

概述

想要弄明白sandbox如何实现字节码增强,我们首先得了解Instrumentation。
Instrumentation让我们有能力在jvm启动后进行Class的变型(java5以后)或者在jvm启动后,通过attach的方式加载jar包实现Class的变型(java6以后)。
sandbox-jvm便是根据此原理,实现一套框架,将不同的增强能力模块化管理,使用者只需要按照规范实现模块功能,即可被sandbox-jvm加载,实现字节码能力增强。
了解了上面的能力后,我们来看下sandbox-jvm的框架实现。

sandbox的代码主要分为几个过程:启动、模块加载、类增强实现

启动

上面我们提到,使用Instrumentation进行字节码增强有2种模式(attach模式和java-agent模式),sandbox-jvm的启动也是这2种,入口都在AgentLauncher中,分别对应着agentmain和premain,它们都调用了install方法,以agentmain为例:

public static void agentmain(String featureString, Instrumentation inst) {
     
        LAUNCH_MODE = LAUNCH_MODE_ATTACH;
        final Map<String, String> featureMap = toFeatureMap(featureString);
        writeAttachResult(
                getNamespace(featureMap),
                getToken(featureMap),
                install(featureMap, inst)
        );
    }

install函数的作用是在目标jvm上安装sandbox,创建独立的classloader,通过classloader加载JettyCoreServer.class,并且反射生成实例,建立httpserver监听请求

// CoreServer类定义
final Class<?> classOfProxyServer = sandboxClassLoader.loadClass(CLASS_OF_PROXY_CORE_SERVER);
// 获取CoreServer单例
final Object objectOfProxyServer = classOfProxyServer
		.getMethod("getInstance")
		.invoke(null);
// CoreServer.isBind()
final boolean isBind = (Boolean) classOfProxyServer.getMethod("isBind").invoke(objectOfProxyServer);
// 如果未绑定,则需要绑定一个地址
if (!isBind) {
     
	try {
     
		classOfProxyServer
				.getMethod("bind", classOfConfigure, Instrumentation.class)
				.invoke(objectOfProxyServer, objectOfCoreConfigure, inst);
	} catch (Throwable t) {
     
		classOfProxyServer.getMethod("destroy").invoke(objectOfProxyServer);
		throw t;
	}
}

启动jetty server,监听http请求,并且调用coreModuleManager.reset进行模块的加载,在下面一节介绍。

public synchronized void bind(final CoreConfigure cfg, final Instrumentation inst) throws IOException {
     
        this.cfg = cfg;
        try {
     
            initializer.initProcess(()->{
     
                    logger.info("initializing server. cfg={}", cfg);
                    jvmSandbox = new JvmSandbox(cfg, inst);
                    initHttpServer();
                    initJettyContextHandler();
                    httpServer.start();
                }
            });
            // 初始化加载所有的模块
            try {
     
                jvmSandbox.getCoreModuleManager().reset();
            } catch (Throwable cause) {
     
                logger.warn("reset occur error when initializing.", cause);
            }
            final InetSocketAddress local = getLocal();
            logger.info("initialized server. actual bind to {}:{}",
                    local.getHostName(),
                    local.getPort()
            );
        } catch (Throwable cause) {
     
                     // 对外抛出到目标应用中
            throw new IOException("server bind failed.", cause);
        }
    }

模块加载

模块是什么?sandbox将不同的业务进行模块划分,不同的模块使用不同的classloader进行加载,例如如果我们想实现流量录制,我们可以自定义一个模块通过字节码增强实现流量入口的监听并进行录制,这就是我们后面会介绍的repeater。我们继续回到sandbox-jvm。
先来看下CoreModuleManager.reset() 的工作:
加载过程是先卸载再加载,我们先不看卸载的过程,先来看加载。首先根据cfg(配置存储对象)中的的module包路径配置得到moduleLibDirArray(需要加载的模块路径:系统模块+用户模块), 每个模块独立加载。

for (final File moduleLibDir : moduleLibDirArray) {
     
	// 用户模块加载目录,加载用户模块目录下的所有模块
	// 对模块访问权限进行校验
	if (moduleLibDir.exists() && moduleLibDir.canRead()) {
     
		new ModuleLibLoader(moduleLibDir, cfg.getLaunchMode())
				.load(new InnerModuleJarLoadCallback(), new InnerModuleLoadCallback());
	} 
}  

ModuleJarLoader::load,保存当前的classloader,然后使用moduleJarClassLoader进行加载。

moduleJarClassLoader = new ModuleJarClassLoader(moduleJarFile);
            final ClassLoader preTCL = Thread.currentThread().getContextClassLoader();
            Thread.currentThread().setContextClassLoader(moduleJarClassLoader);
            try {
     
                hasModuleLoadedSuccessFlag = loadingModules(moduleJarClassLoader, mCb);
            } finally {
     
                Thread.currentThread().setContextClassLoader(preTCL);
            }

DefaultCoreModuleManager.InnerModuleLoadCallback::load,该函数是模块加载的核心内容,主要包含以下功能:

  1. 使用模块的类加载器加载模块,处理@Resource注解的相关属性
  2. 模块激活,并且保存到map中
// 初始化模块信息
        final CoreModule coreModule = new CoreModule(uniqueId, moduleJarFile, moduleClassLoader, module);
        // 注入@Resource资源
        injectResourceOnLoadIfNecessary(coreModule);
        // 如果模块标记了加载时自动激活,则需要在加载完成之后激活模块
        markActiveOnLoadIfNecessary(coreModule);
        // 注册到模块列表中
        loadedModuleBOMap.put(uniqueId, coreModule);

增强的完成

通过http协议访问对应的模块完成增强(通过cmd本质上也是通过调用http接口),通过访问/sandbox/{namespace}/module/http/*,具体实现在JettyCoreServer::initJettyContextHandler中。

private void initJettyContextHandler() {
     
        final String namespace = cfg.getNamespace();
        final ServletContextHandler context = new ServletContextHandler(NO_SESSIONS);

        final String contextPath = "/sandbox/" + namespace;
        context.setContextPath(contextPath);
        context.setClassLoader(getClass().getClassLoader());

        // web-socket-servlet
        final String wsPathSpec = "/module/websocket/*";
        logger.info("initializing ws-http-handler. path={}", contextPath + wsPathSpec);
        //noinspection deprecation
        context.addServlet(
                new ServletHolder(new WebSocketAcceptorServlet(jvmSandbox.getCoreModuleManager())),
                wsPathSpec
        );

        // module-http-servlet
        final String pathSpec = "/module/http/*";
        logger.info("initializing http-handler. path={}", contextPath + pathSpec);
        context.addServlet(
                new ServletHolder(new ModuleHttpServlet(cfg, jvmSandbox.getCoreModuleManager())),
                pathSpec
        );

        httpServer.setHandler(context);
    }  

在ModuleHttpServlet中实现对http请求的解析以及找到对应的module的对应的方法进行执行,执行的过程就是类增强的过程。
下面就是个例子,onClass指定增强的类,onBehavior指定方法,onWatch通过注册事件监听器,通过触发class的重新加载产生事件触发事件监听器,实现类增强。

new EventWatchBuilder(moduleEventWatcher)
                .onClass(Exception.class)
                .includeBootstrap()
                .onBehavior("")
                .onWatch(new EventListener() {
     
                    @Override
                    public void onEvent(Event event) throws Throwable {
     
                        final BeforeEvent bEvent = (BeforeEvent) event;
                        exLogger.info("{} occur an exception: {}",
                                getJavaClassName(bEvent.target.getClass()),
                                bEvent.target
                        );
                    }
                }, BEFORE);

这种设计,提供了用户很易用的扩展方式,用户只需要将自己想要增强的实现放到EventListener中实现。我们来看下这种机制是如何生效的,核心在于onWatch方法,跟踪下去,看下DefaultModuleEventWatcher::watch的实现。 看这段代码前,先来理解下jvm提供的类增强如何实现:

  1. 实现ClassFileTransformer接口
  2. 将ClassFileTransformer实例添加到Instrumentation,添加的时机有2个,就是agentmain和premain,这也是sandbox的2中工作方式

看以下代码也是这个思路,SandboxClassFileTransformer实现了ClassFileTransformer,并且添加到inst中,根据条件得到匹配的class,增强匹配到的class

        // 给对应的模块追加ClassFileTransformer
        final SandboxClassFileTransformer sandClassFileTransformer = new SandboxClassFileTransformer(
                watchId, coreModule.getUniqueId(), matcher, listener, isEnableUnsafe, eventType, namespace);

        // 注册到CoreModule中
        coreModule.getSandboxClassFileTransformers().add(sandClassFileTransformer);

        //这里addTransformer后,接下来引起的类加载都会经过sandClassFileTransformer
        inst.addTransformer(sandClassFileTransformer, true);

        // 查找需要渲染的类集合
        final List<Class<?>> waitingReTransformClasses = classDataSource.findForReTransform(matcher);
        // 应用JVM
        reTransformClasses(watchId,waitingReTransformClasses, progress);

也就是说SandboxClassFileTransformer封装了EventListener中用户自定义的增强逻辑,来看下SandboxClassFileTransformer的_transform方法实现,获取修改后的字节码byte数组。

final byte[] toByteCodeArray = new EventEnhancer().toByteCodeArray(
                    loader,
                    srcByteCodeArray,
                    behaviorSignCodes,
                    namespace,
                    listenerId,
                    eventTypeArray
            );

EventEnhancer::toByteCodeArray中,实现了字节码的增强:

       // 返回增强后字节码
       final ClassReader cr = new ClassReader(byteCodeArray);
       final ClassWriter cw = createClassWriter(targetClassLoader, cr);
       final int targetClassLoaderObjectID = ObjectIDs.instance.identity(targetClassLoader);
       cr.accept(
               new EventWeaver(
                       ASM7, cw, namespace, listenerId,
                       targetClassLoaderObjectID,
                       cr.getClassName(),
                       signCodes,
                       eventTypeArray
               ),
               EXPAND_FRAMES
       );

进入EventWeaver就可以看到具体字节码修改的细节,这里暂不考虑,后续我们再来分析。

参考

https://wemp.app/posts/1ddacf32-7193-4188-8bdc-7e7ef5a61853?utm_source=bottom-latest-posts

你可能感兴趣的:(java,流量回访,java,运维,测试工程师)