JvmSandbox原理分析02-JVM AOP初探:JavaAgent

1、sandbox-core 入口

上一篇我们讲到了sandbox.sh最后执行的是我们熟悉的java -jar命令,用来拉起sandbox-core.jar包。

image-20220417172353759.png

接着,我们就应该寻找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 [email protected] 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相关知识,但这部分内容实在有点多,于是单独抽出一篇说明:【《待补充》】

上面的过程完成了后,目前的启动流程便成了这样:

image-20220419232536715.png

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模型大致如下图所示:

image-1.png

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即可。

 // ProxyCoreServer.java
 private final static Class<? extends CoreServer> classOfCoreServerImpl = JettyCoreServer.class;
 public static CoreServer getInstance() {
   try {
       return new ProxyCoreServer(
               (CoreServer) classOfCoreServerImpl.getMethod("getInstance").invoke(null)
       );
   } catch (Throwable cause) {
       throw new RuntimeException(cause);
   }
 }
复制代码

获得CoreServer实例对象后,反射调用JettyCoreServer#isBind方法判断服务器是否已经初始化。如果没有初始化,则再反射调用JettyCoreServer#bind方法初始化JettyServer,其中沙箱的初始化也在JettyServer的初始化过程中完成

我们注意下,在调用JettyCoreServer#bind方法时,我们传递了Instrumentation 这个对象,这个对象是后期字节码重写(增强)的核心对象

bind方法会在第6小节详细分析,这里暂时先不给出代码了。

 // 反射调用isBind判断服务器是否初始化
 final boolean isBind = (Boolean) classOfProxyServer.getMethod("isBind").invoke(objectOfProxyServer);
 ​
 // 如果未绑定,则需要绑定一个地址
 if (!isBind) {
     try {
         // 反射调用com.alibaba.jvm.sandbox.core.server.jetty.JettyCoreServer#bind,初始化JettyServer和沙箱Sandbox
         classOfProxyServer
                 .getMethod("bind", classOfConfigure, Instrumentation.class)
                 .invoke(objectOfProxyServer, objectOfCoreConfigure, inst);
     } catch (Throwable t) {
         classOfProxyServer.getMethod("destroy").invoke(objectOfProxyServer);
         throw t;
     }
 }
复制代码

5.3、返回绑定信息

最后也比较简单,反射调用JettyCoreServer#getLocal方法获取服务器ip、port信息并返回。

 // 返回服务器绑定的地址
 return (InetSocketAddress) classOfProxyServer.getMethod("getLocal").invoke(objectOfProxyServer);
 ​
 // JettyCoreServer.java
 @Override
 public InetSocketAddress getLocal() throws IOException {
     if (!isBind() || null == httpServer) {
         throw new IOException("server was not bind yet.");
     }
     SelectChannelConnector scc = null;
     final Connector[] connectorArray = httpServer.getConnectors();
     if (null != connectorArray) {
         for (final Connector connector : connectorArray) {
             if (connector instanceof SelectChannelConnector) {
                 scc = (SelectChannelConnector) connector;
                 break;
             }
         }
     }
     if (null == scc) {
         throw new IllegalStateException("not found SelectChannelConnector");
     }
     return new InetSocketAddress(scc.getHost(), scc.getLocalPort());
 }
复制代码

6、总结

完成了上述过程后,Sandbox-core的启动与Agent挂载的过程差不多完成了,我们这里简单总结下:

sandbox.sh脚本运行后,会通过java -jar运行CoreLauncher.java中的main函数,main函数会通过attach agent的方式将sandbox-agent挂载到目标JVM上,sandbox-agent会自定义SandboxClassloader实现类隔离,并通过该类加载器反射加载JettyServer,完成后续的操作。

目前启动流程便可以由下图描述:

image-1650382056612.png

猜你喜欢

转载自juejin.im/post/7101675446127263751