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 [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相关知识,但这部分内容实在有点多,于是单独抽出一篇说明:【《待补充》】
上面的过程完成了后,目前的启动流程便成了这样:
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即可。
// 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,完成后续的操作。
目前启动流程便可以由下图描述: