您现在的位置是:首页 > 文章详情

Jvm-Sandbox源码分析--启动简析

日期:2019-08-31点击:533

前言

1.工作原因,使用jvm-sandbox比较多,遂进行源码分析,做到知己知彼,个人能力有限,如有错误,欢迎指正。
2.关于jvm-sandbox 是什么,如何安装相关环境,可移步 官方文档
3.源码分析基于jvm-sandbox 最新的master代码,tag-1.2.1。
4.暂定计划通过启动简析,加载模块,刷新模块,卸载模块,激活模块等方面入手,通过几篇文章覆盖jvm-sandbox关键流程。

启动

attach方式启动

sh sandbox/bin/sandbox.sh -p pid

脚本分析

简单看一下启动脚本sandbox.sh

# the sandbox main function function main() { check_permission 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 # 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方法

reset_for_env() { #使用默认环境变量 JAVA_HOME # use the env JAVA_HOME for default [[ ! -z ${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="$(\ ps aux\ |grep ${TARGET_JVM_PID}\ |grep java\ |awk '{print $11}'\ |xargs ls -l\ |awk '{if($1~/^l/){print $11}else{print $9}}'\ |sed 's/\/bin\/java//g'\ )" [[ ! -x "${SANDBOX_JAVA_HOME}" ]] \ && exit_on_err 1 "permission denied, ${SANDBOX_JAVA_HOME} is not accessible! please set JAVA_HOME" [[ ! -x "${SANDBOX_JAVA_HOME}/bin/java" ]] \ && exit_on_err 1 "permission denied, ${SANDBOX_JAVA_HOME}/bin/java is not executable!" #判断 JVM 版本 # check the jvm version, we need 6+ local JAVA_VERSION=$("${SANDBOX_JAVA_HOME}/bin/java" -version 2>&1|awk -F '"' '/version/&&$2>"1.5"{print $2}') [[ -z ${JAVA_VERSION} ]] \ && exit_on_err 1 "illegal java version: ${JAVA_VERSION}, please make sure target java process: ${TARGET_JVM_PID} run int JDK[6,11]" #若 ${JAVA_HOME}/lib/tools.jar 存在,则通过 -Xbootclasspath/a 这个配置,将它加入 classpath 末尾,为执行 attach_jvm 方法做准备 [[ -f "${SANDBOX_JAVA_HOME}"/lib/tools.jar ]] \ && SANDBOX_JVM_OPS="${SANDBOX_JVM_OPS} -Xbootclasspath/a:${SANDBOX_JAVA_HOME}/lib/tools.jar" }

关键步骤:

  • 1.使用默认环境变量 JAVA_HOME
  • 2.或者通过TARGET_JVM_PID查找 设置sandbox环境变量
  • 3.判断 JVM 版本是否符合要求
  • 4.若 ${JAVA_HOME}/lib/tools.jar 存在,则通过 -Xbootclasspath/a 这个配置,将它加入 classpath 末尾,为执行 attach_jvm 方法做准备

然后再执行attach_jvm方法

# attach sandbox to target JVM # return : attach jvm local info function attach_jvm() { # got an token local token=`date |head|cksum|sed 's/ //g'` # attach target jvm # 通过java -jar 命令启动 sandbox-core.jar 并传递参数 1. TARGET_JVM_PID 2. sandbox-agent.jar 3. 启动要用到的数据信息 "${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." }

关键步骤:

  • 通过java -jar 命令启动 sandbox-core.jar 并传递参数 1. TARGET_JVM_PID 2. sandbox-agent.jar 3. 启动要用到的数据信息

代码分析

我们来看sandbox-core 这个moudle

在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这个类的main方法
关键步骤:

  • 1.attach pid
  • 2.load sandbox-angent.jar
/** * 内核启动程序 * * @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) { //通过vm类 加载sandbox-agent.jar vmObj.loadAgent(agentJarPath, cfg); } } finally { if (null != vmObj) { vmObj.detach(); } } }

我们可以看到在attach pid 之后加载了sandbox-agent.jar
接下来我们看一下sandbox-agent.jar
和sandbox-core.jar的pom文件类似,agent这个模块也通过maven插件配置了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>

接下来我们看一下AgentLauncher
通过attach pid方式执行,会调用这个类中的agentmain方法,
如果大家不明白为何通过maven插件配置后,便会和指定类中的某个方法进行关联,可以参考一下这篇文章这篇文章

 /** * 动态加载 * * @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) ); }

关键步骤:

  • 1.组装通过脚本命令 attach_jvm()方法传过来的参数featureString
home=/Users/zhengmaoshao/sandbox/bin/..;token=341948577048;server.ip=0.0.0.0;server.port=0;namespace=default
  • 2.writeAttachResult() 方法是写了一些数据到/Users/zhengmaoshao/.sandbox.token 这个文件
default;226298528348;0.0.0.0;55060

java agent方式启动

在应用服务启动脚本中添加:

java -javaagent:/yourpath/sandbox/lib/sandbox-agent.jar

通过javaagent方式启动会调用AgentLauncher类中的premain方法

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

到这一步我们可以很清楚的看到,不管是通过attach pid的方式还是通过javaagent的方式进行启动,最终都会执行install这个方法,

install方法做了什么事情。

1.启动类加载器加载sandbox-spy.jar
SANDBOX_SPY_JAR_PATH=/Users/zhengmaoshao/sandbox/bin/../lib/sandbox-spy.jar

// 将Spy注入到BootstrapClassLoader inst.appendToBootstrapClassLoaderSearch(new JarFile(new File( getSandboxSpyJarPath(getSandboxHome(featureMap)) // SANDBOX_SPY_JAR_PATH )));

2.构造自定义的类加载器,实现代码隔离
SANDBOX_CORE_JAR_PATH=/Users/zhengmaoshao/sandbox/bin/../lib/sandbox-core.jar

// 构造自定义的类加载器,尽量减少Sandbox对现有工程的侵蚀 final ClassLoader sandboxClassLoader = loadOrDefineClassLoader( namespace, getSandboxCoreJarPath(getSandboxHome(featureMap)) // SANDBOX_CORE_JAR_PATH );

3.实例化sandbox-core.jar中的CoreConfigure内核启动配置类
CoreConfigure内核启动配置类内容:
image

 // 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);

4.获取sandbox-core.jar中的ProxyCoreServer对象实例,注意这里真正被实例化的其实JettyCoreServer

// CoreServer类定义 final Class<?> classOfProxyServer = sandboxClassLoader.loadClass(CLASS_OF_PROXY_CORE_SERVER); // 获取CoreServer单例 final Object objectOfProxyServer = classOfProxyServer .getMethod("getInstance") .invoke(null); ...省略代码... //ProxyCoreServer public static CoreServer getInstance() { try { return new ProxyCoreServer( (CoreServer) classOfCoreServerImpl .getMethod("getInstance") .invoke(null) ); } catch (Throwable cause) { throw new RuntimeException(cause); } } ...省略代码... //JettyCoreServer /** * 单例 * * @return CoreServer单例 */ public static CoreServer getInstance() { if (null == coreServer) { synchronized (CoreServer.class) { if (null == coreServer) { coreServer = new JettyCoreServer(); } } } return coreServer; }

5.调用JettyCoreServer bind方法开始进入启动httpServer流程

// 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; } } 

接下来我们看一下启动httpServer过程中都做了什么事情

  • 1.初始化logback日志框架
//初始化logback日志框架 LogbackUtils.init( cfg.getNamespace(), cfg.getCfgLibPath() + File.separator + "sandbox-logback.xml" );
  • 2.创建一个沙箱对象

JvmSandbox构造方法中的关键步骤:
2.1获取事件处理类实例
2.2初始化模块管理实例

  • 2.2.1这里面通过new DefaultProviderManager(cfg)对默认服务提供管理器实现进行实例化。
    主要是创建了一个针对服务提供库sandbox-mgr-provider.jar的ClassLoader,sandbox-mgr-provider中的类通过JAVA SPI的方式实现可扩展性
  • 2.2.2 初始化模块目录,包括/Users/zhengmaoshao/sandbox/bin/../module文件夹中系统模块和/Users/zhengmaoshao/.sandbox-module文件夹中的用户自定义模块
    2.3初始化Spy类
public JvmSandbox(final CoreConfigure cfg, final Instrumentation inst) { //获取事件处理类实例 EventListenerHandlers.getSingleton(); this.cfg = cfg; //初始化模块管理实例 this.coreModuleManager = new DefaultCoreModuleManager( cfg, inst, new DefaultLoadedClassDataSource(inst, cfg.isEnableUnsafe()), new DefaultProviderManager(cfg) ); //初始化Spy类 init(); } 

3.初始化Jetty's ContextHandler,启动httpServer

//JettyCoreServer 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)); if (httpServer.getThreadPool() instanceof QueuedThreadPool) { final QueuedThreadPool qtp = (QueuedThreadPool) httpServer.getThreadPool(); qtp.setName("sandbox-jetty-qtp-" + qtp.hashCode()); } } //初始化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(jvmSandbox.getCoreModuleManager())), pathSpec ); httpServer.setHandler(context); } //最终启动httpServer httpServer.start(); 

最后初始化加载所有的模块,详情后续分析

 // 初始化加载所有的模块 后续分析 try { jvmSandbox.getCoreModuleManager().reset(); } catch (Throwable cause) { logger.warn("reset occur error when initializing.", cause); } //这里校验httpServer是否启动成功 final InetSocketAddress local = getLocal(); logger.info("initialized server. actual bind to {}:{}", local.getHostName(), local.getPort() );

启动了一个jetty服务之后,后续我们的加载,卸载,等命令操作都会通过http请求的方式进行。

原文链接:https://yq.aliyun.com/articles/716768
关注公众号

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。

持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。

转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。

文章评论

共有0条评论来说两句吧...

文章二维码

扫描即可查看该文章

点击排行

推荐阅读

最新文章