JVM SandBox源码解析(一):启动时初始化、启动时加载模块、ModuleHttpServlet进行Http路由

前言

上篇JVM SandBox实现原理详解文章中,主要解析了JVM SandBox的核心实现原理,并且对SandBoxClassLoader和ModuleClassLoader做了源码解析,也解释了在用户自定义模块中为何能使用部分sandbox-core中的类。本文主要对JVM SandBox的核心功能进行源码解析,主要包含以下几部分内容:

  • 启动时初始化
  • 启动时加载模块
  • ModuleHttpServlet进行Http路由
  • 增强目标类
  • 模块刷新
  • 模块卸载

1、启动时初始化

1)、脚本分析

Attach方式启动:sh sandbox.sh -p pid

启动脚本sandbox.sh如下:

# the sandbox main function
function main() {

  while getopts "hp:vFfRu:a:A:d:m:I:P:ClSn:X" ARG; do
    case ${ARG} in
    h)
      usage
      exit
      ;;
    p) TARGET_JVM_PID=${OPTARG} ;;
    v) OP_VERSION=1 ;;
    l) OP_MODULE_LIST=1 ;;
    R) OP_MODULE_RESET=1 ;;
    F) OP_MODULE_FORCE_FLUSH=1 ;;
    f) OP_MODULE_FLUSH=1 ;;
    u)
      OP_MODULE_UNLOAD=1
      ARG_MODULE_UNLOAD=${OPTARG}
      ;;
    a)
      OP_MODULE_ACTIVE=1
      ARG_MODULE_ACTIVE=${OPTARG}
      ;;
    A)
      OP_MODULE_FROZEN=1
      ARG_MODULE_FROZEN=${OPTARG}
      ;;
    d)
      OP_DEBUG=1
      ARG_DEBUG=${OPTARG}
      ;;
    m)
      OP_MODULE_DETAIL=1
      ARG_MODULE_DETAIL=${OPTARG}
      ;;
    I) TARGET_SERVER_IP=${OPTARG} ;;
    P) TARGET_SERVER_PORT=${OPTARG} ;;
    C) OP_CONNECT_ONLY=1 ;;
    S) OP_SHUTDOWN=1 ;;
    n)
      OP_NAMESPACE=1
      ARG_NAMESPACE=${OPTARG}
      ;;
    X) set -x ;;
    ?)
      usage
      exit_on_err 1
      ;;
    esac
  done

  reset_for_env
  check_permission

  # reset IP
  [ -z "${TARGET_SERVER_IP}" ] && TARGET_SERVER_IP="${DEFAULT_TARGET_SERVER_IP}"

  # reset PORT
  [ -z "${TARGET_SERVER_PORT}" ] && TARGET_SERVER_PORT=0

  # reset NAMESPACE
  [[ ${OP_NAMESPACE} ]] &&
    TARGET_NAMESPACE=${ARG_NAMESPACE}
  [[ -z ${TARGET_NAMESPACE} ]] &&
    TARGET_NAMESPACE=${DEFAULT_NAMESPACE}

  if [[ ${OP_CONNECT_ONLY} ]]; then
    [[ 0 -eq ${TARGET_SERVER_PORT} ]] &&
      exit_on_err 1 "server appoint PORT (-P) was missing"
    SANDBOX_SERVER_NETWORK="${TARGET_SERVER_IP};${TARGET_SERVER_PORT}"
  else
    # -p was missing
    [[ -z ${TARGET_JVM_PID} ]] &&
      exit_on_err 1 "PID (-p) was missing."
    attach_jvm
  fi

	...省略代码...

  # default
  sandbox_curl "sandbox-info/version"
  exit

}

在执行sandbox.sh的时候,先执行reset_for_env方法,再执行attach_jvm方法

reset_for_env方法

# reset sandbox work environment
# reset some options for env
reset_for_env() {

	# 使用默认环境变量JAVA_HOME
  # use the env JAVA_HOME for default
  [[ -n "${JAVA_HOME}" ]] &&
    SANDBOX_JAVA_HOME="${JAVA_HOME}"

	# 或者通过TARGET_JVM_PID查找 设置sandbox环境变量
  # use the target JVM for SANDBOX_JAVA_HOME
  [[ -z "${SANDBOX_JAVA_HOME}" ]] &&
    SANDBOX_JAVA_HOME="$(
      lsof -p "${TARGET_JVM_PID}" |
        grep "/bin/java" |
        awk '{print $9}' |
        xargs ls -l |
        awk '{if($1~/^l/){print $11}else{print $9}}' |
        xargs ls -l |
        awk '{if($1~/^l/){print $11}else{print $9}}' |
        sed 's/\/bin\/java//g'
    )"

	# 若${JAVA_HOME}/lib/tools.jar存在,则通过-Xbootclasspath/a这个配置,将它加入classpath末尾,为执行attach_jvm方法做准备
  # append toos.jar to JVM_OPT
  [[ -f "${SANDBOX_JAVA_HOME}"/lib/tools.jar ]] &&
    SANDBOX_JVM_OPS="${SANDBOX_JVM_OPS} -Xbootclasspath/a:${SANDBOX_JAVA_HOME}/lib/tools.jar"

  #fix for windows  shell $HOME diff with user.home
  test -n "${USERPROFILE}" -a -z "$(cat "${SANDBOX_TOKEN_FILE}")" && SANDBOX_TOKEN_FILE=${USERPROFILE}/.sandbox.token

}

attach_jvm方法

# attach sandbox to target JVM
# return : attach jvm local info
function attach_jvm() {

  # got an token
  local token
  token="$(date | head | cksum | sed 's/ //g')"

	# 通过java -jar命令启动sandbox-core.jar并传递参数:TARGET_JVM_PID、sandbox-agent.jar、启动要用到的数据信息
  # attach target jvm
  "${SANDBOX_JAVA_HOME}/bin/java" \
    ${SANDBOX_JVM_OPS} \
    -jar "${SANDBOX_LIB_DIR}/sandbox-core.jar" \
    "${TARGET_JVM_PID}" \
    "${SANDBOX_LIB_DIR}/sandbox-agent.jar" \
    "home=${SANDBOX_HOME_DIR};token=${token};server.ip=${TARGET_SERVER_IP};server.port=${TARGET_SERVER_PORT};namespace=${TARGET_NAMESPACE}" ||
    exit_on_err 1 "attach JVM ${TARGET_JVM_PID} fail."

  # get network from attach result
  SANDBOX_SERVER_NETWORK=$(grep "${token}" "${SANDBOX_TOKEN_FILE}" | grep "${TARGET_NAMESPACE}" | tail -1 | awk -F ";" '{print $3";"$4}')
  [[ -z ${SANDBOX_SERVER_NETWORK} ]] &&
    exit_on_err 1 "attach JVM ${TARGET_JVM_PID} fail, attach lose response."

}

2)、sandbox-core

sandbox-core的pom文件中,通过mainClass指定了这个主函数,java -jar sandbox-core.jar命令会执行这个函数

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <executions>
                    <execution>
                        <goals>
                            <goal>attached</goal>
                        </goals>
                        <phase>package</phase>
                        <configuration>
                            <descriptorRefs>
                                <descriptorRef>jar-with-dependencies</descriptorRef>
                            </descriptorRefs>
                            <archive>
                                <manifest>
                                    <mainClass>com.alibaba.jvm.sandbox.core.CoreLauncher</mainClass>
                                </manifest>
                            </archive>
                        </configuration>
                    </execution>
                </executions>
            </plugin>

CoreLauncher

public class CoreLauncher {

    /**
     * 内核启动程序
     *
     * @param args 参数
     *             [0] : PID
     *             [1] : agent.jar's value
     *             [2] : token
     */
    public static void main(String[] args) {
        try {

            // check args
            if (args.length != 3
                    || StringUtils.isBlank(args[0])
                    || StringUtils.isBlank(args[1])
                    || StringUtils.isBlank(args[2])) {
                throw new IllegalArgumentException("illegal args");
            }

            new CoreLauncher(args[0], args[1], args[2]);
        } catch (Throwable t) {
            t.printStackTrace(System.err);
            System.err.println("sandbox load jvm failed : " + getCauseMessage(t));
            System.exit(-1);
        }
    }
  
    public CoreLauncher(final String targetJvmPid,
                        final String agentJarPath,
                        final String token) throws Exception {

        // 加载agent
        attachAgent(targetJvmPid, agentJarPath, token);

    }
  
    // 加载Agent
    private void attachAgent(final String targetJvmPid,
                             final String agentJarPath,
                             final String cfg) throws Exception {

        VirtualMachine vmObj = null;
        try {
						//attach目标pid
            vmObj = VirtualMachine.attach(targetJvmPid);
            if (vmObj != null) {
              	//加载sandbox-agent.jar
                vmObj.loadAgent(agentJarPath, cfg);
            }

        } finally {
            if (null != vmObj) {
                vmObj.detach();
            }
        }

    }  

3)、sandbox-agent

sandbox-agent的pom文件中,配置了Premain-Class和Agent-Class两个参数,并且都指向AgentLauncher这个类

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <executions>
                    <execution>
                        <goals>
                            <goal>attached</goal>
                        </goals>
                        <phase>package</phase>
                        <configuration>
                            <descriptorRefs>
                                <descriptorRef>jar-with-dependencies</descriptorRef>
                            </descriptorRefs>
                            <archive>
                                <manifestEntries>
                                    <Premain-Class>com.alibaba.jvm.sandbox.agent.AgentLauncher</Premain-Class>
                                    <Agent-Class>com.alibaba.jvm.sandbox.agent.AgentLauncher</Agent-Class>
                                    <Can-Redefine-Classes>true</Can-Redefine-Classes>
                                    <Can-Retransform-Classes>true</Can-Retransform-Classes>
                                </manifestEntries>
                            </archive>
                        </configuration>
                    </execution>
                </executions>
            </plugin>

通过Attach方式执行会调用AgentLauncher的agentmain(String featureString, Instrumentation inst)方法,Agent方式启动会调用AgentLauncher的premain(String featureString, Instrumentation inst)方法

public class AgentLauncher {

    /**
     * 启动加载
     *
     * @param featureString 启动参数
     *                      [namespace,prop]
     * @param inst          inst
     */
    public static void premain(String featureString, Instrumentation inst) {
        LAUNCH_MODE = LAUNCH_MODE_AGENT;
        install(toFeatureMap(featureString), inst);
    }  

    /**
     * 动态加载
     *
     * @param featureString 启动参数
     *                      [namespace,token,ip,port,prop]
     * @param inst          inst
     */
    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)
        );
    }

两种启动方式,都会通过toFeatureMap(final String featureString)方法解析启动传入的参数featureString,然后调用install(final Map<String, String> featureMap, final Instrumentation inst)方法

4)、AgentLauncher的install()方法

public class AgentLauncher {

	/**
     * 在当前JVM安装jvm-sandbox
     *
     * @param featureMap 启动参数配置
     * @param inst       inst
     * @return 服务器IP:PORT
     */
    private static synchronized InetSocketAddress install(final Map<String, String> featureMap,
                                                          final Instrumentation inst) {

        final String namespace = getNamespace(featureMap);
        final String propertiesFilePath = getPropertiesFilePath(featureMap);
        final String coreFeatureString = toFeatureString(featureMap);

        try {
            final String home = getSandboxHome(featureMap);
            // 将Spy注入到BootstrapClassLoader
            inst.appendToBootstrapClassLoaderSearch(new JarFile(new File(
                    getSandboxSpyJarPath(home)
                    // SANDBOX_SPY_JAR_PATH
            )));

            // 构造SandboxClassLoader,尽量减少Sandbox对现有工程的侵蚀
            final ClassLoader sandboxClassLoader = loadOrDefineClassLoader(
                    namespace,
                    getSandboxCoreJarPath(home)
                    // SANDBOX_CORE_JAR_PATH
            );

            // CoreConfigure类定义
            final Class<?> classOfConfigure = sandboxClassLoader.loadClass(CLASS_OF_CORE_CONFIGURE);

            // 反序列化成CoreConfigure类实例
            final Object objectOfCoreConfigure = classOfConfigure.getMethod("toConfigure", String.class, String.class)
                    .invoke(null, coreFeatureString, propertiesFilePath);

            // CoreServer类定义
            final Class<?> classOfProxyServer = sandboxClassLoader.loadClass(CLASS_OF_PROXY_CORE_SERVER);

            // 获取CoreServer单例,真正被实例化的是JettyCoreServer
            final Object objectOfProxyServer = classOfProxyServer
                    .getMethod("getInstance")
                    .invoke(null);

            // CoreServer.isBind()
            final boolean isBind = (Boolean) classOfProxyServer.getMethod("isBind").invoke(objectOfProxyServer);


            // 调用JettyCoreServer的bind方法开始进入启动HttpServer流程
            if (!isBind) {
                try {
                    classOfProxyServer
                            .getMethod("bind", classOfConfigure, Instrumentation.class)
                            .invoke(objectOfProxyServer, objectOfCoreConfigure, inst);
                } catch (Throwable t) {
                    classOfProxyServer.getMethod("destroy").invoke(objectOfProxyServer);
                    throw t;
                }

            }

            // 返回服务器绑定的地址
            return (InetSocketAddress) classOfProxyServer
                    .getMethod("getLocal")
                    .invoke(objectOfProxyServer);


        } catch (Throwable cause) {
            throw new RuntimeException("sandbox attach failed.", cause);
        }

    }

install()方法核心流程如下:

  1. BootstrapClassLoader加载sandbox-spy.jar
  2. 构造SandboxClassLoader
  3. 实例化sandbox-core.jar中的CoreConfigure内核启动配置类
  4. 实例化sandbox-core.jar中的ProxyCoreServer,这里真正被实例化的是JettyCoreServer
  5. 调用JettyCoreServer的bind()方法开始进入启动HttpServer流程

5)、JettyCoreServer的bind()方法

public class JettyCoreServer implements CoreServer {
  
    @Override
    public synchronized void bind(final CoreConfigure cfg, final Instrumentation inst) throws IOException {
        this.cfg = cfg;
        try {
            initializer.initProcess(new Initializer.Processor() {
                @Override
                public void process() throws Throwable {
                  	// 初始化logback
                    LogbackUtils.init(
                            cfg.getNamespace(),
                            cfg.getCfgLibPath() + File.separator + "sandbox-logback.xml"
                    );
                    logger.info("initializing server. cfg={}", cfg);
                  	// 实例化JvmSandbox
                    jvmSandbox = new JvmSandbox(cfg, inst);
                  	// 初始化HttpServer
                    initHttpServer();
                  	// 初始化ContextHandler
                    initJettyContextHandler();
                  	// 启动HttpServer
                    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) {

            // 这里会抛出到目标应用层,所以在这里留下错误信息
            logger.warn("initialize server failed.", cause);

            // 对外抛出到目标应用中
            throw new IOException("server bind failed.", cause);
        }

        logger.info("{} bind success.", this);
    }  

bind()方法核心流程如下:

  1. 初始化logback
  2. 实例化JvmSandbox
  3. 初始化HttpServer和ContextHandler
  4. 启动HttpServer
  5. 初始化加载所有的模块

实例化JvmSandbox

public class JvmSandbox {

		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)
        ));
		// 初始化Spy类
        init();
    }

初始化HttpServer和ContextHandler

public class JettyCoreServer implements CoreServer {

		private void initHttpServer() {

        final String serverIp = cfg.getServerIp();
        final int serverPort = cfg.getServerPort();

        // 如果IP:PORT已经被占用,则无法继续被绑定
        // 这里说明下为什么要这么无聊加个这个判断,让Jetty的Server.bind()抛出异常不是更好么?
        // 比较郁闷的是,如果这个端口的绑定是"SO_REUSEADDR"端口可重用的模式,那么这个server是能正常启动,但无法正常工作的
        // 所以这里必须先主动检查一次端口占用情况,当然了,这里也会存在一定的并发问题,BUT,我认为这种概率事件我可以选择暂时忽略
        if (isPortInUsing(serverIp, serverPort)) {
            throw new IllegalStateException(format("address[%s:%s] already in using, server bind failed.",
                    serverIp,
                    serverPort
            ));
        }

        httpServer = new Server(new InetSocketAddress(serverIp, serverPort));
        QueuedThreadPool qtp = new QueuedThreadPool();
        // jetty线程设置为daemon,防止应用启动失败进程无法正常退出
        qtp.setDaemon(true);
        qtp.setName("sandbox-jetty-qtp-" + qtp.hashCode());
        httpServer.setThreadPool(qtp);
    }

    /*
     * 初始化Jetty's ContextHandler
     */
    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);
    }

启动了一个Jetty服务之后,后续对模块的加载、卸载、激活、冻结等命令操作都会通过Http请求的方式进行

2、启动时加载模块

JettyCoreServer的bind(final CoreConfigure cfg, final Instrumentation inst)方法会调用jvmSandbox.getCoreModuleManager().reset()初始化加载所有的模块,实际上调用了DefaultCoreModuleManager的reset()方法

public class DefaultCoreModuleManager implements CoreModuleManager {
  
    @Override
    public synchronized CoreModuleManager reset() throws ModuleException {

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

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

        // 加载所有模块
        for (final File moduleLibDir : moduleLibDirArray) {
            // 用户模块加载目录,加载用户模块目录下的所有模块
            // 对模块访问权限进行校验
            if (moduleLibDir.exists() && moduleLibDir.canRead()) {
              	// 初始化模块目录加载器 传入模块lib目录和加载模式(默认加载模式就是attach)
                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;
    }
class ModuleLibLoader {

    /**
     * 加载Module
     *
     * @param mjCb 模块文件加载回调
     * @param mCb  模块加载回掉
     */
    void load(final ModuleJarLoadCallback mjCb,
              final ModuleJarLoader.ModuleLoadCallback mCb) {

        // 开始逐条加载
        for (final File moduleJarFile : listModuleJarFileInLib()) {
            try {
              	// 模块文件加载回调
                mjCb.onLoad(moduleJarFile);
              	// 模块加载
                new ModuleJarLoader(moduleJarFile, mode).load(mCb);
            } catch (Throwable cause) {
                logger.warn("loading module-jar occur error! module-jar={};", moduleJarFile, cause);
            }
        }

    }

1)、模块文件加载回调

public class DefaultCoreModuleManager implements CoreModuleManager {

	/**
     * 用户模块文件加载回调
     */
    final private class InnerModuleJarLoadCallback implements ModuleJarLoadCallback {
        @Override
        public void onLoad(File moduleJarFile) throws Throwable {
            providerManager.loading(moduleJarFile);
        }
    }

最终会通过模块Jar文件加载链ModuleJarLoadingChain去加载文件。目前来看实现类都是空的,没有起到什么作用

public interface ModuleJarLoadingChain {

    /**
     * 加载模块Jar文件 <br>
     * <p>
     * 1. 可以在这个实现中对目标期待加载的模块Jar文件进行解密,签名验证等操作<br>
     * 2. 如果判定加载失败,可以通过抛出异常的形式中断加载,sandbox将会跳过此模块Jar文件的加载<br>
     * 3. 整个模块文件的加载为一个链式的加载过程<br>
     * </p>
     *
     * @param moduleJarFile 期待被加载模块Jar文件
     * @throws Throwable 模块文件加载异常
     */
    void loading(File moduleJarFile) throws Throwable;

}

2)、模块加载过程

class ModuleJarLoader {

    void load(final ModuleLoadCallback mCb) throws IOException {

        boolean hasModuleLoadedSuccessFlag = false;
        ModuleJarClassLoader moduleJarClassLoader = null;
        logger.info("prepare loading module-jar={};", moduleJarFile);
        try {
          	// 构造模块类加载器ModuleJarClassLoader
            moduleJarClassLoader = new ModuleJarClassLoader(moduleJarFile);

          	// 将当前线程的类加载器从SandboxClassLoader设置成ModuleJarClassLoader
            final ClassLoader preTCL = Thread.currentThread().getContextClassLoader();
            Thread.currentThread().setContextClassLoader(moduleJarClassLoader);

            try {
              	// 加载模块
                hasModuleLoadedSuccessFlag = loadingModules(moduleJarClassLoader, mCb);
            } finally {
              	// 将当前线程的类加载器从ModuleJarClassLoader设置成SandboxClassLoader
                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();
            }
        }

    }

ModuleJarLoader的load()方法核心流程如下:

  1. 构造模块类加载器ModuleJarClassLoader
  2. 将当前线程的类加载器从SandboxClassLoader设置成ModuleJarClassLoader
  3. 加载模块
  4. 将当前线程的类加载器从ModuleJarClassLoader设置成SandboxClassLoader
class ModuleJarLoader {
    
    private boolean loadingModules(final ModuleJarClassLoader moduleClassLoader,
                                   final ModuleLoadCallback mCb) {

        final Set<String> loadedModuleUniqueIds = new LinkedHashSet<String>();
      	// ServiceLoader遍历META-INF/services目录下com.alibaba.jvm.sandbox.api.Module文件中的所有类,并实例化返回(SPI)
        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();
    }

ModuleJarLoader的loadingModules()方法核心流程如下:

  1. ServiceLoader遍历META-INF/services目录下com.alibaba.jvm.sandbox.api.Module文件中的所有类,并实例化返回(SPI)
  2. 触发模块加载回调

以sandbox-mgr-module为例,源码中并没有META-INF/services这个目录,但是每个Module类都标注了@MetaInfServices(Module.class)注解,标注了该注解打包后会自动帮我们生成META-INF/services下的文件

在这里插入图片描述

调用ModuleLoadCallbackonLoad()方法触发模块加载回调,代码如下:

public class DefaultCoreModuleManager implements CoreModuleManager {

    /**
     * 用户模块加载回调
     */
    final private class InnerModuleLoadCallback implements ModuleJarLoader.ModuleLoadCallback {
        @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);
        }
    }

真正的模块加载调用load()方法,代码如下:

public class DefaultCoreModuleManager implements CoreModuleManager {

	/**
     * 加载并注册模块
     * <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);

    }

DefaultCoreModuleManager的load()方法核心流程如下:

  1. 实例化模块信息
  2. 注入@Resource资源
  3. 如果模块标记了加载时自动激活,则需要在加载完成之后激活模块
  4. 注册到模块列表中
  5. 通知生命周期,模块加载完成

3、ModuleHttpServlet进行Http路由

ModuleHttpServlet是在启动时,JettyCoreServer类中通过initJettyContextHandler()方法加入到Jetty上下文的

public class JettyCoreServer implements CoreServer {

		private void initHttpServer() {

        final String serverIp = cfg.getServerIp();
        final int serverPort = cfg.getServerPort();

        // 如果IP:PORT已经被占用,则无法继续被绑定
        // 这里说明下为什么要这么无聊加个这个判断,让Jetty的Server.bind()抛出异常不是更好么?
        // 比较郁闷的是,如果这个端口的绑定是"SO_REUSEADDR"端口可重用的模式,那么这个server是能正常启动,但无法正常工作的
        // 所以这里必须先主动检查一次端口占用情况,当然了,这里也会存在一定的并发问题,BUT,我认为这种概率事件我可以选择暂时忽略
        if (isPortInUsing(serverIp, serverPort)) {
            throw new IllegalStateException(format("address[%s:%s] already in using, server bind failed.",
                    serverIp,
                    serverPort
            ));
        }

        httpServer = new Server(new InetSocketAddress(serverIp, serverPort));
        QueuedThreadPool qtp = new QueuedThreadPool();
        // jetty线程设置为daemon,防止应用启动失败进程无法正常退出
        qtp.setDaemon(true);
        qtp.setName("sandbox-jetty-qtp-" + qtp.hashCode());
        httpServer.setThreadPool(qtp);
    }

    /*
     * 初始化Jetty's ContextHandler
     */
    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请求都会进入doMethod()方法:

public class ModuleHttpServlet extends HttpServlet {   

	@Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setCharacterEncoding(cfg.getServerCharset().name());
        doMethod(req, resp, Http.Method.GET);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setCharacterEncoding(cfg.getServerCharset().name());
        doMethod(req, resp, Http.Method.POST);
    }

    private void doMethod(final HttpServletRequest req,
                          final HttpServletResponse resp,
                          final Http.Method expectHttpMethod) throws ServletException, IOException {

        // 获取请求路径
        final String path = req.getPathInfo();

        // 解析请求路径,获取模块ID
        final String uniqueId = parseUniqueId(path);
        if (StringUtils.isBlank(uniqueId)) {
            logger.warn("path={} is not matched any module.", path);
            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        // 根据模块ID获取模块
        final CoreModule coreModule = coreModuleManager.get(uniqueId);
        if (null == coreModule) {
            logger.warn("path={} is matched module {}, but not existed.", path, uniqueId);
            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        // 查找标注@Command的方法,匹配对应的方法
        final Method method = matchingModuleMethod(
                path,
                expectHttpMethod,
                uniqueId,
                coreModule.getModule().getClass()
        );
        if (null == method) {
            logger.warn("path={} is not matched any method in module {}",
                    path,
                    uniqueId
            );
            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
            return;
        } else {
            logger.debug("path={} is matched method {} in module {}", path, method.getName(), uniqueId);
        }

        // 自动释放I/O资源
        final List<Closeable> autoCloseResources = coreModule.append(new ReleaseResource<List<Closeable>>(new ArrayList<Closeable>()) {
            @Override
            public void release() {
                final List<Closeable> closeables = get();
                if (CollectionUtils.isEmpty(closeables)) {
                    return;
                }
                for (final Closeable closeable : get()) {
                    if (closeable instanceof Flushable) {
                        try {
                            ((Flushable) closeable).flush();
                        } catch (Exception cause) {
                            logger.warn("path={} flush I/O occur error!", path, cause);
                        }
                    }
                    IOUtils.closeQuietly(closeable);
                }
            }
        });

        // 生成方法调用参数
        final Object[] parameterObjectArray = generateParameterObjectArray(autoCloseResources, method, req, resp);

        final boolean isAccessible = method.isAccessible();
      	// 使用ModuleJarClassLoader invoke目标模块的方法
        final ClassLoader oriThreadContextClassLoader = Thread.currentThread().getContextClassLoader();
        try {
            method.setAccessible(true);
            Thread.currentThread().setContextClassLoader(coreModule.getLoader());
            method.invoke(coreModule.getModule(), parameterObjectArray);
            logger.debug("path={} invoke module {} method {} success.", path, uniqueId, method.getName());
        } catch (IllegalAccessException iae) {
            logger.warn("path={} invoke module {} method {} occur access denied.", path, uniqueId, method.getName(), iae);
            throw new ServletException(iae);
        } catch (InvocationTargetException ite) {
            logger.warn("path={} invoke module {} method {} occur error.", path, uniqueId, method.getName(), ite.getTargetException());
            final Throwable targetCause = ite.getTargetException();
            if (targetCause instanceof ServletException) {
                throw (ServletException) targetCause;
            }
            if (targetCause instanceof IOException) {
                throw (IOException) targetCause;
            }
            throw new ServletException(targetCause);
        } finally {
            Thread.currentThread().setContextClassLoader(oriThreadContextClassLoader);
            method.setAccessible(isAccessible);
            coreModule.release(autoCloseResources);
        }

    }

ModuleHttpServlet的doMethod()方法核心流程如下:

  1. 获取请求路径
  2. 解析请求路径,获取模块ID
  3. 根据模块ID获取模块
  4. 查找标注@Command的方法,匹配对应的方法
  5. 生成方法调用参数
  6. 使用ModuleJarClassLoader invoke目标模块的方法
参与评论 您还未登录,请先 登录 后发表或查看评论
相关推荐

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:数字20 设计师:CSDN官方博客 返回首页

打赏作者

邋遢的流浪剑客

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值