首页>>后端>>java->JvmSandbox原理分析02

JvmSandbox原理分析02

时间:2023-12-06 本站 点击:0

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


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:/java/15945.html