1、sandbox-core 入口
上一篇我们讲到了sandbox.sh最后执行的是我们熟悉的java -jar命令,用来拉起sandbox-core.jar包。
接着,我们就应该寻找sandbox-core.jar这个jar包的入口文件在哪了,有两种寻找方法:
第一种方法:解压jar包,查看META-INF/MANIFEST.MF
文件,其中Main-Class这个key就指 定了模块入口类的全路径类名
Manifest-Version: 1.0 Archiver-Version: Plexus Archiver Created-By: Apache Maven Built-By: XWT Build-Jdk: 1.8.0_281 Main-Class: com.alibaba.jvm.sandbox.core.CoreLauncher
第二种方法:直接查看源码中的pom.xml文件,其中的maven-build插件就指定了模块的入口,manifest-mainClass表示通过java -jar执行该jar包时,main函数的入口为配置的全路径类名。
... <manifest> <mainClass>com.alibaba.jvm.sandbox.core.CoreLauncher</mainClass> </manifest> ...
ps:上述两种寻找模块入口的方式对于大家写惯了Springboot的同学来说或许有些陌生。其实Springboot也是这么做的,Springboot工程的入口其实并不在我们经常写的@Springboot注解下的main方法,这个方法其实会在真正的入口处被反射调用,详细可以参考这篇文章:你真的了解SpringBoot应用的启动入口么? - 掘金 (juejin.cn)
通过上述分析我们得知,sandbox.sh执行了java -jar后,最终会来到sandbox-core这个module下的com.alibaba.JVM.sandbox.core.CoreLauncher
类的main方法中。
2、CoreLauncher.java分析
CoreLauncher.java
的核心逻辑以及源码如下所示:
main方法首先会对执行参数args进行校验。通过上一篇分析sandbox.sh中我们可以得知,最后执行java -jar时会携带三个参数,他们分别是目标JVM进程的PID、sandbox-agent.jar路径、config配置信息,这三个参数会封装在args参数中。
随后会调用CoreLauncher构造方法透传执行参数至attachAgent方法中。
attachAgent方法就是挂载javaagent的核心方法了。它首先获取目标JVM进程的虚拟机对象VirtualMachine,然后调用该对象的loadAgent方法加载sandbox-agent.jar这个agent jar包。
package com.alibaba.jvm.sandbox.core; import com.sun.tools.attach.VirtualMachine; import org.apache.commons.lang3.StringUtils; import static com.alibaba.jvm.sandbox.core.util.SandboxStringUtils.getCauseMessage; /** * 沙箱内核启动器 * Created by luanjia@taobao.com on 16/10/2. */ public class CoreLauncher { public CoreLauncher(final String targetJvmPid, final String agentJarPath, final String token) throws Exception { // 加载agent attachAgent(targetJvmPid, agentJarPath, token); } /** * 内核启动程序 * @param args 参数: [0]PID [1]agent.jar's value [2]token * sandbox.sh执行后面带了 pid sandbox-agent.jar config 三个参数,这些参数会被传入到main函数的args数组当中 */ public static void main(String[] args) { try { // 检验参数是否符合要求 if (args.length != 3 || StringUtils.isBlank(args[0]) || StringUtils.isBlank(args[1]) || StringUtils.isBlank(args[2])) { throw new IllegalArgumentException("illegal args"); } // 执行CoreLauncher方法,并传递三个参数 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); } } // 加载Agent private void attachAgent(final String targetJvmPid, final String agentJarPath, final String cfg) throws Exception { VirtualMachine vmObj = null; try { // attach目标的JVM vmObj = VirtualMachine.attach(targetJvmPid); if (vmObj != null) { // attach成功后加载代理jar包。 vmObj.loadAgent(agentJarPath, cfg); } } finally { if (null != vmObj) { vmObj.detach(); } } } }
CoreLauncher.java的核心逻辑如上所述,但是为什么这样做就能够在目标JVM还在运行期间挂载其他的jar包呢?
这就不得不说JVM提供给开发人员的Java Instrument这个“杀手级武器”了。
原本准备在第三节讲解Java Instrument相关知识,但这部分内容实在有点多,于是单独抽出一篇说明:【《待补充》】
上面的过程完成了后,目前的启动流程便成了这样:
3、 sandbox-agent入口
第二节聊到了CoreLauncher.java通过VirtualMachine#loadAgent挂载sandbox-agent.jar这个jar包。大家也通过链接的文章了解了Java Instrument相关知识,接下来我们看看sandbox-agent的入口在哪里。(其实sandbox-agent这个module的类非常少,入口很容易就能够找到)
sandbox-agent这个module的pom.xml如下所示(只显示关键信息),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>
Premain-Class标签表示,当前jar包通过在虚拟机参数中指定-javaagent进行挂载时(启动时挂载),入口在AgentLauncher#premain方法中。
Agent-Class标签表示,当前jar包通过VirtualMachine#loadAgent方法挂载时(运行时挂载),入口在AgentLauncher#agentmain方法中。
Sandbox一般都是运行时挂载,因此我们的关注点可以转移到AgentLauncher#agentmain方法上了。
4、AgentLauncher#agentmain
agentmain方法如下所示,其中featureString参数是在加载agent时,通过方法参数cfg传递过来,这个参数其实是sandbox需要的配置文件。
// CoreLauncher.java vmObj.loadAgent(agentJarPath, cfg); /** * 运行时加载 * @param featureString 为 CoreLauncher.java 传递的 cfg 参数 * @param inst inst */ public static void agentmain(String featureString, Instrumentation inst) { // 设置启动模式为 attach LAUNCH_MODE = LAUNCH_MODE_ATTACH; // 解析配置,String->Map final Map<String, String> featureMap = toFeatureMap(featureString); // 获取名称空间 String namespace = getNamespace(featureMap); // 获取token String token = getToken(featureMap); // 安装sandbox的核心方法 InetSocketAddress inetSocketAddress = install(featureMap, inst); // 将安装结果写入到sandbox.token文件当中 writeAttachResult(namespace, token, inetSocketAddress); }
这个方法比较简单,解析完配置参数后就开始安装过程,最后将安装结果append到sandbox.token文件当中。方法中的其他逻辑都比较简单,大家看一下就行了,我们接下来分析最为核心的install方法。
5、AgentLauncher#install
install方法逻辑比较多,这里先总的概括下install方法做了什么,并给出方法的全部代码,后面我们再对其中的细节慢慢分析,这里先注意下该方法传递了inst这个Instrumentation对象。
install方法总的来说完成了以下几件事:
将sandbox-spy这个jar包路径添加到Bootstrap Classloader的加载路径下
初始化自定义的SandboxClassloader,反射加载Sandbox核心对象,实现沙箱类与业务类隔离
启动Sandbox的内置jetty服务器(上一篇说的服务器就是在这个时候加载的),后续业务代码的增强等工作都在该服务器中执行。该服务器也提供了一些接口动态操作沙箱。
返回Jetty服务器绑定信息,install方法执行完成。
完整的代码如下所示:
/** * 在当前JVM安装jvm-sandbox * 注意 install传递了inst这个Instrumentation对象 * * @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); // SANDBOX_SPY_JAR_PATH JarFile spyJarFile = new JarFile(new File(getSandboxSpyJarPath(home))); // 将Spy注入到BootstrapClassLoader,我们需要将spy类的代码增强到目标JVM中 inst.appendToBootstrapClassLoaderSearch(spyJarFile); // 构造自定义的类加载器,尽量减少Sandbox对现有工程的侵蚀 final ClassLoader sandboxClassLoader = loadOrDefineClassLoader(namespace,getSandboxCoreJarPath(home)); // 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单例,这里可以观察 com.alibaba.jvm.sandbox.core.server#getInstance 方法, // ProxyCoreServer只是代理,执行都在JettyCoreServer中 final Object objectOfProxyServer = classOfProxyServer.getMethod("getInstance").invoke(null); // CoreServer.isBind() final boolean isBind = (Boolean) classOfProxyServer.getMethod("isBind").invoke(objectOfProxyServer); // 如果未绑定,则需要绑定一个地址 if (!isBind) { try { // 注意这里绑定的时候传递了 Instrumentation 这个类,这个类主要负责对目标JVM中的代码进行增强 // 现在主要逻辑都在 com.alibaba.jvm.sandbox.core.server.jetty.JettyCoreServer#bind 中了 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); } }
5.1、自定义SandboxClassloader
install最开始的代码都是些配置解析的操作,大家看看就可以了。
我们首先来说下关于classloader相关的代码,就是下面这三行:
// 将sandbox-spy这个jar包保证成JarFile文件 JarFile spyJarFile = new JarFile(new File(getSandboxSpyJarPath(home))); // 将Spy注入到BootstrapClassLoader,我们需要将spy类的代码增强到目标JVM中 inst.appendToBootstrapClassLoaderSearch(spyJarFile); // 构造自定义的类加载器,尽量减少Sandbox对现有工程的侵蚀 final ClassLoader sandboxClassLoader = loadOrDefineClassLoader(namespace,getSandboxCoreJarPath(home));
将sandbox-spy这个jar包注入到BootstrapClassLoader加载路径下,这里提前说下sandbox-spy这个包的作用:我们可以认为这个包里的代码是一系列的模板代码,后期Sandbox会通过ASM框架将这些代码转化成字节码增加到业务代码的字节码当中,实现对业务代码的增强功能。 官方把这个包认为是间谍类,能够埋点在业务代码中,获取业务代码执行过程中的一些信息。
但spy类为什么要被BootstrapClassLoader加载呢?
按理说,埋点在业务代码中,仅需要ApplicationClassloader就完全足够了,后来经过某位大佬的提醒,如果要增强JDK里面的代码,ApplicationClassloader就不够用了,大家都知道,JDK的代码加载是ExtensionClassloader和BootstrapClassLoader。因此,将spy添加到BootstrapClassLoader的类路径,只要不破坏双亲委托机制,都能够被spy类增强,这是一种比较保险的做法。
之后就是初始化SandboxClassLoader,这个自定义类加载器主要负责加载沙箱中的类,它的实现破坏了双亲委托机制,能够保证沙箱中的类与业务代码的类完全隔离。减少Sandbox对现有工程的侵蚀。
经过上面两行代码后,整个工程的Classloader模型大致如下图所示:
5.2、反射加载
5.2.1、反射CoreConfigure对象
完成SandboxClassloader的初始化后,首先加载的是com.alibaba.jvm.sandbox.core.CoreConfigure
这个类,从类名很容易知道该类主要存储了沙箱的核心配置信息。之后利用加载的类反射调用toConfigure()方法,将我们之前设置的配置信息保存在CoreConfigure
类当中,并实例化一个CoreConfigure对象。
// 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);
toConfigure方法比较简单,如下所示。就是调用构造方法,然后解析配置,大家感兴趣的可以深入看看。
// CoreConfigure.java public static CoreConfigure toConfigure(final String featureString, final String propertiesFilePath) { return instance = new CoreConfigure(featureString, propertiesFilePath); } private CoreConfigure(final String featureString, final String propertiesFilePath) { final Map<String, String> featureMap = toFeatureMap(featureString); final Map<String, String> propertiesMap = toPropertiesMap(propertiesFilePath); this.featureMap.putAll(merge(featureMap, propertiesMap)); }
5.2.2、反射CoreServer对象
第二个加载的类是com.alibaba.jvm.sandbox.core.server.ProxyCoreServer
,完成类加载后反射调用getInstance方法获取Server对象。
// CoreServer类定义 final Class<?> classOfProxyServer = sandboxClassLoader.loadClass(CLASS_OF_PROXY_CORE_SERVER); // 反射CoreServer单例,这里可以观察 com.alibaba.jvm.sandbox.core.server#getInstance 方法, // ProxyCoreServer只是代理,执行都在JettyCoreServer中 final Object objectOfProxyServer = classOfProxyServer.getMethod("getInstance").invoke(null);
ProxyCoreServer#getInstance方法如下所示,该方法也是反射调用JettyCoreServer#getInstance方法实例化一个JettyCoreServer。这也能够更好体现ProxyCoreServer命名含义,表名它仅是一个代理Server,具体实现还是由classOfCoreServerImpl指定。
这里作者应该是想让CoreServer的实现更加可扩展些,如果有些同学不想用JettyServer,而是想要使用自己实现的Server,只需要实现CoreServer接口后,替换classOfCoreServerImpl即可。
... <manifest> <mainClass>com.alibaba.jvm.sandbox.core.CoreLauncher</mainClass> </manifest> ...0
获得CoreServer实例对象后,反射调用JettyCoreServer#isBind方法判断服务器是否已经初始化。如果没有初始化,则再反射调用JettyCoreServer#bind方法初始化JettyServer,其中沙箱的初始化也在JettyServer的初始化过程中完成。
我们注意下,在调用JettyCoreServer#bind方法时,我们传递了Instrumentation 这个对象,这个对象是后期字节码重写(增强)的核心对象。
bind方法会在第6小节详细分析,这里暂时先不给出代码了。
... <manifest> <mainClass>com.alibaba.jvm.sandbox.core.CoreLauncher</mainClass> </manifest> ...1
5.3、返回绑定信息
最后也比较简单,反射调用JettyCoreServer#getLocal方法获取服务器ip、port信息并返回。
... <manifest> <mainClass>com.alibaba.jvm.sandbox.core.CoreLauncher</mainClass> </manifest> ...2
6、总结
完成了上述过程后,Sandbox-core的启动与Agent挂载的过程差不多完成了,我们这里简单总结下:
sandbox.sh脚本运行后,会通过java -jar运行CoreLauncher.java中的main函数,main函数会通过attach agent的方式将sandbox-agent挂载到目标JVM上,sandbox-agent会自定义SandboxClassloader实现类隔离,并通过该类加载器反射加载JettyServer,完成后续的操作。
目前启动流程便可以由下图描述:
原文:https://juejin.cn/post/7101675446127263751