jvm-sandbox源码笔记之模块加载

793 阅读10分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第3天。点击查看活动详情

一、前言

在启动篇中讲了sandbox的启动流程,其中提到了初始化模块,今天具体介绍sandbox是如何加载模块的。

二、加载模块

启动过程中与sandbox模块相关的逻辑第一次出现在sandbox的JvmSandbox类的实例化之中。在创建一个JvmSandbox对象的时候可以看到创建coreMoudleManger的时候new了一个DefaultCoreModuleManager对象,而这个DefaultCoreModuleManager就是我们默认的模块管理器。

public JvmSandbox(final CoreConfigure cfg,
                  final Instrumentation inst) {
    EventListenerHandler.getSingleton();
    this.cfg = cfg;
    this.coreModuleManager = SandboxProtector.instance.protectProxy(CoreModuleManager.class, new DefaultCoreModuleManager(
            cfg,
            inst,
            new DefaultCoreLoadedClassDataSource(inst, cfg.isEnableUnsafe()),
            new DefaultProviderManager(cfg)
    ));

    init();
}

CoreModuleManager实现了CoreModuleManager接口,在这个接口中我门可以看到模块管理器的一些功能,包含激活、卸载模块等

/**
 * 模块管理
 * Created by luanjia on 16/10/4.
 */
public interface CoreModuleManager {

    /**
     * 刷新沙箱模块
     *
     * @param isForce 是否强制刷新
     * @throws ModuleException 模块加载失败
     */
    void flush(boolean isForce) throws ModuleException;

    /**
     * 沙箱重置
     *
     * @return this
     * @throws ModuleException 沙箱重置失败
     */
    CoreModuleManager reset() throws ModuleException;

    /**
     * 激活模块
     *
     * @param coreModule 模块业务对象
     * @throws ModuleException 激活模块失败
     */
    void active(CoreModule coreModule) throws ModuleException;

    /**
     * 冻结模块
     * 模块冻结时候将会失去所有事件的监听
     *
     * @param coreModule              模块业务对象
     * @param isIgnoreModuleException 是否忽略模块异常
     *                                强制冻结模块将会主动忽略冻结失败情况,强行将模块所有的事件监听行为关闭
     * @throws ModuleException 冻结模块失败
     */
    void frozen(CoreModule coreModule, boolean isIgnoreModuleException) throws ModuleException;

    /**
     * 列出所有的模块
     *
     * @return 模块集合
     */
    Collection<CoreModule> list();

    /**
     * 获取模块
     *
     * @param uniqueId 模块ID
     * @return 模块
     */
    CoreModule get(String uniqueId);

    /**
     * 获取模块
     *
     * @param uniqueId 模块ID
     * @return 模块
     * @throws ModuleException 当模块不存在时抛出模块不存在异常
     */
    CoreModule getThrowsExceptionIfNull(String uniqueId) throws ModuleException;

    /**
     * 卸载模块
     *
     * @param coreModule              模块
     * @param isIgnoreModuleException 是否忽略模块异常
     * @return 返回被卸载的模块
     * @throws ModuleException 卸载模块失败
     */
    CoreModule unload(CoreModule coreModule, boolean isIgnoreModuleException) throws ModuleException;

    /**
     * 卸载所有模块
     */
    void unloadAll();

}

在DefaultCoreModuleManager的具体实现中可以看到,DefaultCoreModuleManager的构造函数中初始化了几个参数,最下面的个是初始化模块目录,将配置中传入进来的模块目录记录下来,cfg.getSystemModuleLibPath()获取sandbox的系统模块加载路径,cfg.getUserModuleLibFilesWithCache()将用户目录下的.sandbox-moulde下的jar包路径记录下来,便于后续的加载。我们自己开发的模块jar文件一般就是在用户目录下的.sandbox-moulde

/**
 * 模块模块管理
 *
 * @param cfg             模块核心配置
 * @param inst            inst
 * @param classDataSource 已加载类数据源
 * @param providerManager 服务提供者管理器
 */
public DefaultCoreModuleManager(final CoreConfigure cfg,
                                final Instrumentation inst,
                                final CoreLoadedClassDataSource classDataSource,
                                final ProviderManager providerManager) {
    this.cfg = cfg;
    this.inst = inst;
    this.classDataSource = classDataSource;
    this.providerManager = providerManager;

    // 初始化模块目录
    this.moduleLibDirArray = mergeFileArray(
            StringUtils.isBlank(cfg.getSystemModuleLibPath())
                    ? new File[0]
                    : new File[]{new File(cfg.getSystemModuleLibPath())},
            cfg.getUserModuleLibFilesWithCache()
    );
}

到这一步基本上就是实例化一个jvmSandbox对象,里面引用了一个默认的模块管理器。接下来就是开始加载所有的模块。这一步的开始实在bind方法里的jvmSandbox.getCoreModuleManager().reset()。获取jvmSandbox实例的模块管理器再调用重置沙箱模块方法,代码如下

@Override
public synchronized CoreModuleManager reset() throws ModuleException {

    logger.info("resetting all loaded modules:{}", loadedModuleBOMap.keySet());

    // 1. 强制卸载所有模块
    unloadAll();

    // 2. 加载所有模块
    for (final File moduleLibDir : moduleLibDirArray) {
        // 用户模块加载目录,加载用户模块目录下的所有模块
        // 对模块访问权限进行校验
        if (moduleLibDir.exists() && moduleLibDir.canRead()) {
            new ModuleLibLoader(moduleLibDir, cfg.getLaunchMode())
                    .load(
                            new InnerModuleJarLoadCallback(),
                            new InnerModuleLoadCallback()
                    );
        } else {
            logger.warn("module-lib not access, ignore flush load this lib. path={}", moduleLibDir);
        }
    }

    return this;
}

流程在方法注释中已经写到啦,就是先卸载全部模块再重新加载。sandbox的refresh命令调用的也是这里。
第一步 unloadAll
第二步 加载所有模块

// 2. 加载所有模块
for (final File moduleLibDir : moduleLibDirArray) {
    // 用户模块加载目录,加载用户模块目录下的所有模块
    // 对模块访问权限进行校验
    if (moduleLibDir.exists() && moduleLibDir.canRead()) {
        new ModuleLibLoader(moduleLibDir, cfg.getLaunchMode())
                .load(
                        new InnerModuleJarLoadCallback(),
                        new InnerModuleLoadCallback()
                );
    } else {
        logger.warn("module-lib not access, ignore flush load this lib. path={}", moduleLibDir);
    }
}

对模块的加载调用的是ModuleLibLoader的load方法 --> ModuleJarLoader的load方法。ModuleJarLoader.load代码如下

void load(final ModuleLoadCallback mCb) throws IOException {

    boolean hasModuleLoadedSuccessFlag = false;
    ModuleJarClassLoader moduleJarClassLoader = null;
    logger.info("prepare loading module-jar={};", moduleJarFile);
    try {
        moduleJarClassLoader = new ModuleJarClassLoader(moduleJarFile);

        final ClassLoader preTCL = Thread.currentThread().getContextClassLoader();
        Thread.currentThread().setContextClassLoader(moduleJarClassLoader);

        try {
            hasModuleLoadedSuccessFlag = loadingModules(moduleJarClassLoader, mCb);
        } finally {
            Thread.currentThread().setContextClassLoader(preTCL);
        }

    } finally {
        if (!hasModuleLoadedSuccessFlag
                && null != moduleJarClassLoader) {
            logger.warn("loading module-jar completed, but NONE module loaded, will be close ModuleJarClassLoader. module-jar={};", moduleJarFile);
            moduleJarClassLoader.closeIfPossible();
        }
    }

}

再调用loadingModules方法

private boolean loadingModules(final ModuleJarClassLoader moduleClassLoader,
                               final ModuleLoadCallback mCb) {

    final Set<String> loadedModuleUniqueIds = new LinkedHashSet<String>();
    final ServiceLoader<Module> moduleServiceLoader = ServiceLoader.load(Module.class, moduleClassLoader);
    final Iterator<Module> moduleIt = moduleServiceLoader.iterator();
    while (moduleIt.hasNext()) {

        final Module module;
        try {
            module = moduleIt.next();
        } catch (Throwable cause) {
            logger.warn("loading module instance failed: instance occur error, will be ignored. module-jar={}", moduleJarFile, cause);
            continue;
        }

        final Class<?> classOfModule = module.getClass();

        // 判断模块是否实现了@Information标记
        if (!classOfModule.isAnnotationPresent(Information.class)) {
            logger.warn("loading module instance failed: not implements @Information, will be ignored. class={};module-jar={};",
                    classOfModule,
                    moduleJarFile
            );
            continue;
        }

        final Information info = classOfModule.getAnnotation(Information.class);
        final String uniqueId = info.id();

        // 判断模块ID是否合法
        if (StringUtils.isBlank(uniqueId)) {
            logger.warn("loading module instance failed: @Information.id is missing, will be ignored. class={};module-jar={};",
                    classOfModule,
                    moduleJarFile
            );
            continue;
        }

        // 判断模块要求的启动模式和容器的启动模式是否匹配
        if (!ArrayUtils.contains(info.mode(), mode)) {
            logger.warn("loading module instance failed: launch-mode is not match module required, will be ignored. module={};launch-mode={};required-mode={};class={};module-jar={};",
                    uniqueId,
                    mode,
                    StringUtils.join(info.mode(), ","),
                    classOfModule,
                    moduleJarFile
            );
            continue;
        }

        try {
            if (null != mCb) {
                mCb.onLoad(uniqueId, classOfModule, module, moduleJarFile, moduleClassLoader);
            }
        } catch (Throwable cause) {
            logger.warn("loading module instance failed: MODULE-LOADER-PROVIDER denied, will be ignored. module={};class={};module-jar={};",
                    uniqueId,
                    classOfModule,
                    moduleJarFile,
                    cause
            );
            continue;
        }

        loadedModuleUniqueIds.add(uniqueId);

    }


    logger.info("loaded module-jar completed, loaded {} module in module-jar={}, modules={}",
            loadedModuleUniqueIds.size(),
            moduleJarFile,
            loadedModuleUniqueIds
    );
    return !loadedModuleUniqueIds.isEmpty();
}

sandbox要求自定义的模块符合SPI规范,所以这里通过ServiceLoader获取到对应的Moudle,再获取我们定义到注解上的id作为模块的唯一标识。再调用模块加载回调InnerModuleLoadCallback的onLoad。 代码如下

@Override
public void onLoad(final String uniqueId,
                   final Class moduleClass,
                   final Module module,
                   final File moduleJarFile,
                   final ModuleJarClassLoader moduleClassLoader) throws Throwable {

    // 如果之前已经加载过了相同ID的模块,则放弃当前模块的加载
    if (loadedModuleBOMap.containsKey(uniqueId)) {
        final CoreModule existedCoreModule = get(uniqueId);
        logger.info("IMLCB: module already loaded, ignore load this module. expected:module={};class={};loader={}|existed:class={};loader={};",
                uniqueId,
                moduleClass, moduleClassLoader,
                existedCoreModule.getModule().getClass().getName(),
                existedCoreModule.getLoader()
        );
        return;
    }

    // 需要经过ModuleLoadingChain的过滤
    providerManager.loading(
            uniqueId,
            moduleClass,
            module,
            moduleJarFile,
            moduleClassLoader
    );

    // 之前没有加载过,这里进行加载
    logger.info("IMLCB: found new module, prepare to load. module={};class={};loader={};",
            uniqueId,
            moduleClass,
            moduleClassLoader
    );

    // 这里进行真正的模块加载
    load(uniqueId, module, moduleJarFile, moduleClassLoader);
}

在这里有一个providerManager是一个服务提供管理器。当真正加载模块之前会调用这个管理器通知管理器进行进一步的处理。比如对jar包进行解密等操作。在sandbox中这个服务提供者管理器都是空实现。用户可以自行实现对加载过程进行定制化处理。
onLoad方法中又调用了load方法进行了真正的加载,将这些数据封装一个CoreModule存放到注册表中(loadedModuleBOMap)。再通知模块加载完成

/**
 * 加载并注册模块
 * <p>1. 如果模块已经存在则返回已经加载过的模块</p>
 * <p>2. 如果模块不存在,则进行常规加载</p>
 * <p>3. 如果模块初始化失败,则抛出异常</p>
 *
 * @param uniqueId          模块ID
 * @param module            模块对象
 * @param moduleJarFile     模块所在JAR文件
 * @param moduleClassLoader 负责加载模块的ClassLoader
 * @throws ModuleException 加载模块失败
 */
private synchronized void load(final String uniqueId,
                               final Module module,
                               final File moduleJarFile,
                               final ModuleJarClassLoader moduleClassLoader) throws ModuleException {

    if (loadedModuleBOMap.containsKey(uniqueId)) {
        logger.debug("module already loaded. module={};", uniqueId);
        return;
    }

    logger.info("loading module, module={};class={};module-jar={};",
            uniqueId,
            module.getClass().getName(),
            moduleJarFile
    );

    // 初始化模块信息
    final CoreModule coreModule = new CoreModule(uniqueId, moduleJarFile, moduleClassLoader, module);

    // 注入@Resource资源
    injectResourceOnLoadIfNecessary(coreModule);

    callAndFireModuleLifeCycle(coreModule, MODULE_LOAD);

    // 设置为已经加载
    coreModule.markLoaded(true);

    // 如果模块标记了加载时自动激活,则需要在加载完成之后激活模块
    markActiveOnLoadIfNecessary(coreModule);

    // 注册到模块列表中
    loadedModuleBOMap.put(uniqueId, coreModule);

    // 通知生命周期,模块加载完成
    callAndFireModuleLifeCycle(coreModule, MODULE_LOAD_COMPLETED);

}

其中用户自定义的模块可以实现ModuleLifecycle接口对模块的生命周期进行监听。

//注入@Resource资源是做什么 为什么需要设置模块是否被加载 markActiveOnLoadIfNecessary做什么的
上面代码中的load方法真正加载模块,分为以下几个步骤
1.初始化模块信息
2.注入@Resource资源
通过writeField 对模块中添加了了@Resource注解并且引用了了LoadedClassDataSource/ModuleEventWatcher/ModuleController/ModuleManager/ConfigInfo/EventMonitor几个类进行依赖注入
3.生命周期通知模块开始加载
4.模块状态标记已加载
模块在加载时默认标记为已加载,当我们对某个模块执行卸载命令时,模块会被标记为未被加载。其实当执行卸载操作的时候,模块已经在注册列表中被删除,执行list命令的时候不会查到被卸载的模块,这个状态标识也没有真正被使用到
5.如果设置了加载时激活则进行激活
如果模块没有设置为加载时激活(默认为true),那么模块加载完成后为冻结状态,需要执行actice命令进行激活
6.将模块添加到注册表
7.通知生命周期,模块加载完成

至此模块加载算是真正完成啦

三、模块卸载

在模块管理模块中ModuleMgrModule的unload方法接收到卸载模块请求,调用模块管理器unload达到卸载模块 unload代码

/**
 * 卸载并删除注册模块
 * <p>1. 如果模块原本就不存在,则幂等此次操作</p>
 * <p>2. 如果模块存在则尝试进行卸载</p>
 * <p>3. 卸载模块之前会尝试冻结该模块</p>
 *
 * @param coreModule              等待被卸载的模块
 * @param isIgnoreModuleException 是否忽略模块异常
 * @throws ModuleException 卸载模块失败
 */
@Override
public synchronized CoreModule unload(final CoreModule coreModule,
                                      final boolean isIgnoreModuleException) throws ModuleException {

    if (!coreModule.isLoaded()) {
        logger.debug("module already unLoaded. module={};", coreModule.getUniqueId());
        return coreModule;
    }

    logger.info("unloading module, module={};class={};",
            coreModule.getUniqueId(),
            coreModule.getModule().getClass().getName()
    );

    // 尝试冻结模块
    frozen(coreModule, isIgnoreModuleException);

    // 通知生命周期
    try {
        callAndFireModuleLifeCycle(coreModule, MODULE_UNLOAD);
    } catch (ModuleException meCause) {
        if (isIgnoreModuleException) {
            logger.warn("unload module occur error, ignored. module={};class={};code={};",
                    meCause.getUniqueId(),
                    coreModule.getModule().getClass().getName(),
                    meCause.getErrorCode(),
                    meCause
            );
        } else {
            throw meCause;
        }
    }

    // 从模块注册表中删除
    loadedModuleBOMap.remove(coreModule.getUniqueId());
    // 标记模块为:已卸载
    coreModule.markLoaded(false);
    // 释放所有可释放资源
    coreModule.releaseAll();
    // 尝试关闭ClassLoader
    closeModuleJarClassLoaderIfNecessary(coreModule.getLoader());

    return coreModule;
}

unload操作分为以下几个流程
1. 冻结模块
为了 取消事件处理器对事件处理,否则监听逻辑一直存在,增强还会生效
2. 执行卸载模块的钩子函数
通知生命周期模块模块开始进行unload,如果这里出现异常可能会导致终止卸载,将不会继续后面的流程
3. 将模块在注册列表中移除
这里维护了一个ConcurrentHashMap
4. 模块标记为卸载
5. 释放资源
将需要被释放的资源定义为弱饮用。事件观察者(包括删除SandboxClassFileTransformer相关)、流(模块管理模块接受请求用于返回的流)等。
6. 关闭classloader
文件句柄等

四、Sandbox守护者

在模块加载的过程中,为模块注入ModuleEventWatcher的时候使用到了一个Sandbox守护者。在实例化一个jvmSandbox容器的时候也用到了这个方法,名为# Sandbox守护者,用来守护接口定义的方法,使得sandbox操作的事件不被响应
实例化sandbox容器

this.coreModuleManager = SandboxProtector.instance.protectProxy(CoreModuleManager.class, new DefaultCoreModuleManager(
        cfg,
        inst,
        new DefaultCoreLoadedClassDataSource(inst, cfg.isEnableUnsafe()),
        new DefaultProviderManager(cfg)
));

注入ModuleEventWatcher

// ModuleEventWatcher对象注入
else if (ModuleEventWatcher.class.isAssignableFrom(fieldType)) {
    final ModuleEventWatcher moduleEventWatcher = coreModule.append(
            new ReleaseResource<ModuleEventWatcher>(
                    SandboxProtector.instance.protectProxy(
                            ModuleEventWatcher.class,
                            new DefaultModuleEventWatcher(inst, classDataSource, coreModule, cfg.isEnableUnsafe(), cfg.getNamespace())
                    )
            ) 
 .....

protectProxy代码

/**
 * 守护接口定义的所有方法
 *
 * @param protectTargetInterface 保护目标接口类型
 * @param protectTarget          保护目标接口实现
 * @param <T>                    接口类型
 * @return 被保护的目标接口实现
 */
public <T> T protectProxy(final Class<T> protectTargetInterface,
                          final T protectTarget) {
    return (T) Proxy.newProxyInstance(getClass().getClassLoader(), new Class<?>[]{protectTargetInterface}, new InvocationHandler() {

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            final int enterReferenceCount = enterProtecting();
            try {
                return method.invoke(protectTarget, args);
            } finally {
                final int exitReferenceCount = exitProtecting();
                assert enterReferenceCount == exitReferenceCount;
                if (enterReferenceCount != exitReferenceCount) {
                    logger.warn("thread:{} exit protecting with error!, expect:{} actual:{}",
                            Thread.currentThread(),
                            enterReferenceCount,
                            exitReferenceCount
                    );
                }
            }
        }

    });

开始没有搞懂这个方法的作用和场景。直到看到了github上有人提交的isse #250得到了一些提示。因为在jvm-sandbox加载模块的时候,会涉及到许多的流的开关、字符串的处理等java基本操作,如果我们设置的是这些由java基础类的监听则会导致在加载这一步骤就被需要处理一些增强逻辑,这些不是我们希望看到的,会增加一些性能损耗,所以在加载模块的时候增加这个保护方法使得程序可以判断此时的事件是不是由sandbox触发的,从而对该事件不进行处理。