JVM-Sandbox 启动有两种方式:ATTACH和AGENT。
AGENT 方式的入口
必须和服务一起启动,需要修改服务的启动命令,如:
java -javaagent:${HOME}/sandbox/lib/sandbox-agent.jar=server.port=8820;server.ip=0.0.0.0
-jar ${HOME}/.sandbox-module/repeater-bootstrap.jar
通过sandbox-agent.jar的pom文件;或者通过jar -xvf sandbox-agent.jar解压jar包,查看META-INF目录下的MANIFEST.MF文件我们可以获得程序入口。
pom 方式查看入口
<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>
解压jar包方式查看入口
admin@wangyuhao lib % jar -xvf sandbox-agent.jar
已创建: META-INF/
已解压: META-INF/MANIFEST.MF
已创建: com/
已创建: com/alibaba/
已创建: com/alibaba/jvm/
已创建: com/alibaba/jvm/sandbox/
已创建: com/alibaba/jvm/sandbox/agent/
已解压: com/alibaba/jvm/sandbox/agent/SandboxClassLoader.class
已解压: com/alibaba/jvm/sandbox/agent/AgentLauncher.class
admin@wangyuhao lib % cat META-INF/MANIFEST.MF
Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Created-By: Apache Maven
Built-By: admin
Build-Jdk: 1.8.0_281
Agent-Class: com.alibaba.jvm.sandbox.agent.AgentLauncher
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Premain-Class: com.alibaba.jvm.sandbox.agent.AgentLauncher
通过上述两种方式我们可以找到程序入口类是AgentLauncher,这种方式启动是调用的premain方法:
/**
* 启动加载
*
* @param featureString 启动参数
* [namespace,prop]
* @param inst inst
*/
public static void premain(String featureString, Instrumentation inst) {
System.out.println("Sandbox 以Agent方式启动");
LAUNCH_MODE = LAUNCH_MODE_AGENT;
install(toFeatureMap(featureString), inst);
}
这里的核心是install(toFeatureMap(featureString), inst);方法,主要作用是在当前JVM安装jvm-sandbox。
ATTACH 方式的入口
即插即用的启动模式,可以在不重启目标JVM的情况下完成沙箱的植入。原理和GREYS、BTrace类似,利用了JVM的Attach机制实现,它是调用的agentmain方法。
这种方式加载稍微复杂一点,他的启动入口是执行sandbox命令,如:
./sandbox.sh -p `ps -ef | grep java | grep com.alibaba.repeater.console.start.Application | grep -v grep | awk {print $2} `
ps -ef | grep java | grep com.alibaba.repeater.console.start.Application | grep -v grep | awk {print $2} 的作用是用来找到当前jvm程序的进程号,转换过来的命令是:./sandbox.sh -p 67672。
深入sandbox.sh这个脚本可以发现其核心是attach_jvm函数。:
# attach sandbox to target JVM
# return : attach jvm local info
function attach_jvm() {
# got an token
local token
token="$(date | head | cksum | sed s/ //g )"
# attach target jvm
"${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."
}
attach_jvm函数第一会生成一个带时间戳信息的唯一token,然后使用java命令启动sandbox代理,最后获取attach结果的网络信息。核心是执行了java -jar sandbox-cor.jar然后传了三个参数,简化后的关键信息如下:
java -jar /home/admin/sandbox/lib/sandbox-core.jar 76092 "/home/admin/sandbox/lib/sandbox-agent.jar" "home=/home/admin/sandbox;token=2019091703032893;server.ip=0.0.0.0;server.port=12345;namespace=default"
通过上述sandbox-agent.jar 查询入口类的方式,可以找到sandbox-cor.jar的入口类为:CoreLauncher,入口即为该类的main方法。
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);
}
}
main方法通过解析接口传过来的三个参数,三个参数分别为:
- targetJvmPid(PID(JVM进程ID)):76092。
- agentJarPath(agent.jar全路径):/home/admin/sandbox/lib/sandbox-core.jar。
- cfg(配置信息):home=/home/admin/sandbox;token=2019091703032893;server.ip=0.0.0.0;server.port=12345;namespace=default。
最后通过获取到的信息调用VirtualMachine.attach()方法,通过attach来执行agent.jar。
// 加载Agent
private void attachAgent(final String targetJvmPid,
final String agentJarPath,
final String cfg) throws Exception {
VirtualMachine vmObj = null;
try {
vmObj = VirtualMachine.attach(targetJvmPid);
if (vmObj != null) {
vmObj.loadAgent(agentJarPath, cfg);
}
} finally {
if (null != vmObj) {
vmObj.detach();
}
}
}
Attach实现原理可以参考如下资料:
- Java Attach机制简介: https://blog.csdn.net/u013332124/article/details/88362317
- Java Attach源码解析:https://www.cnblogs.com/Jack-Blog/p/15026267.html
通过上述方法就调用了sandbox-agent.jar中的AgentLauncher类的agentmain方法:
/**
* 动态加载
*
* @param featureString 启动参数
* [namespace,token,ip,port,prop]
* @param inst inst
*/
public static void agentmain(String featureString, Instrumentation inst) {
System.out.println("Sandbox 以ATTACH方式启动");
LAUNCH_MODE = LAUNCH_MODE_ATTACH;
final Map<String, String> featureMap = toFeatureMap(featureString);
writeAttachResult(
getNamespace(featureMap),
getToken(featureMap),
install(featureMap, inst)
);
}
CoreLauncher类主要完成了sandbox-agent.jar的代理加载,核心方法还是 install(featureMap, inst)方法。
由此可见ATTACH和AGENT两种启动方式最后都是调用的 install(featureMap, inst)子方法来完成sandbox的加载。
Agent 初始化过程(install方法)
在 install 方法中完成对 agent 的初始化,在初始化的过程中使用到了自定义的 SandboxClassLoader 对沙箱类进行加载,ModuleJarClassLoader 对./modele、~/.sanbox-modele目录中module。jar进行加载,实现沙箱内部类与业务类隔离。
sandbox-agent.jar中的AgentLauncher类的install方法源码如下:
/**
* 在当前JVM安装jvm-sandbox
*
* @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);
// 将Spy注入到BootstrapClassLoader
inst.appendToBootstrapClassLoaderSearch(new JarFile(new File(
getSandboxSpyJarPath(home)
// SANDBOX_SPY_JAR_PATH
)));
// 构造自定义的类加载器,尽量减少Sandbox对现有工程的侵蚀
final ClassLoader sandboxClassLoader = loadOrDefineClassLoader(
namespace,
getSandboxCoreJarPath(home)
// SANDBOX_CORE_JAR_PATH
);
// 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单例
final Object objectOfProxyServer = classOfProxyServer
.getMethod("getInstance")
.invoke(null);
// 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;
}
}
// 返回服务器绑定的地址
return (InetSocketAddress) classOfProxyServer
.getMethod("getLocal")
.invoke(objectOfProxyServer);
} catch (Throwable cause) {
throw new RuntimeException("sandbox attach failed.", cause);
}
}
核心流程:
- 通过
Instrumentation调用BootstrapClassLoader去加载sandbox-spy.jar,sandbox-spy.jar的主要作用是完成目标JVM和sandbox的通讯。 - 创建
SandboxClassLoader,并通过该ClassLoader去加载sandbox-core.jar。 - 通过
SandboxClassLoader去加载CoreConfigure类,然后将所有沙箱配置映射赋值到该类的实例。 - 通过
SandboxClassLoader去加载ProxyCoreServer类,并获得一个JettyCoreServer实例。 - 然后调用
JettyCoreServer的bind方法完成Spy的初始化(SpyUtils.init(cfg.getNamespace());)、HTTP 服务的初始化和启动、通过ModuleJarClassLoader加载所有mudule(jvmSandbox.getCoreModuleManager().reset();)。 - 最后,返回代理核心服务器
JettyCoreServer的服务器绑定的地址。

Spy 间谍类
install方法第一会通过Instrumentation 实例将 sandbox-spy.jar 添加到 BootstrapClassLoader 的搜索范围内。
// 将Spy注入到BootstrapClassLoader
inst.appendToBootstrapClassLoaderSearch(new JarFile(new File(
getSandboxSpyJarPath(home)
// SANDBOX_SPY_JAR_PATH
)));
使用BootstrapClassLoader去加载spy的最要目的应该是保证Spy能增强所有的类,包括JDK自带的一些类,Spy的主要作用是完成目标JVM和sandbox间的通讯,sandbox会将方法的执行分为三个阶段BEFORE(方法执行前)、RETURN (方法返回)和 THROWS(方法异常) 三个环节。
// BEFORE
try {
/*
* do something...
*/
// RETURN
return;
} catch (Throwable cause) {
// THROWS
}
基于BEFORE、RETURN和THROWS三个环节事件分离,沙箱的模块可以完成许多类AOP的操作。
- 可以感知和改变方法调用的入参
- 可以感知和改变方法调用返回值和抛出的异常
- 可以改变方法执行的流程
- 在方法体执行之前直接返回自定义结果对象,原有方法代码将不会被执行
- 在方法体返回之前重新构造新的结果对象,甚至可以改变为抛出异常
- 在方法体抛出异常之后重新抛出新的异常,甚至可以改变为正常返回
要完成这些动作都是依赖Spy暴露出来的xxxOnBefore()、xxxOnReturn()和xxxOnThrows()等钩子函数来完成通讯,下图是官网提供的图:

为了更加直观的看到代码增强后的效果,我在我的服务里面新写了下面一个测试类,并通过自带的debug-trace模块来查看spyEnhance方法的执行耗时,然后反编译JVM中内存中的class文件查看效果。
原始TestService类:
@Service("testService")
public class TestService {
/**
* 未增强方法
*
* @param name
* @return
*/
public String notEnhance(String name) {
System.out.println("notEnhance");
return "1";
}
/**
* 增强方法
*
* @return
*/
public String spyEnhance() {
System.out.println("spyEnhance");
return "1";
}
}
通过命令监听spyEnhance方法:
./sandbox.sh -p `ps -ef | grep java | grep com.alibaba.repeater.console.start.Application | grep -v grep | awk {print $2} ` -d debug-trace/trace?class=com.alibaba.repeater.console.service.impl.TestService&method=spyEnhance
然后反编译JVM 内存中TestClass类:
ClassLoader:
+-sun.misc.Launcher$AppClassLoader@18b4aac2
+-sun.misc.Launcher$ExtClassLoader@3ec300f1
Location:
/Users/admin/Documents/workspace/jvm-sandbox-repeater/repeater-console/repeater-console-service/target/classes/
/*
* Decompiled with CFR.
*
* Could not load the following classes:
* java.com.alibaba.jvm.sandbox.spy.Spy
* java.com.alibaba.jvm.sandbox.spy.Spy$Ret
*/
package com.alibaba.repeater.console.service.impl;
import java.com.alibaba.jvm.sandbox.spy.Spy;
import org.springframework.stereotype.Service;
@Service(value="testService")
public class TestService {
public String notEnhance(String name) {
/*21*/ System.out.println("notEnhance");
/*22*/ return "1";
}
/*
* Enabled aggressive block sorting
* Enabled unnecessary exception pruning
* Enabled aggressive exception aggregation
*/
public String spyEnhance() {
try {
Spy.Ret ret = Spy.spyMethodOnBefore((Object[])new Object[0], (String)"default", (int)1006, (int)1004, (String)"com.alibaba.repeater.console.service.impl.TestService", (String)"spyEnhance", (String)"()Ljava/lang/String;", (Object)this);
int n = ret.state;
if (n == 1) return (String)ret.respond;
if (n == 2) {
Spy.Ret ret2;
throw (Throwable)ret2.respond;
}
Spy.spyMethodOnCallBefore((int)31, (String)"java.io.PrintStream", (String)"println", (String)"(Ljava/lang/String;)V", (String)"default", (int)1006);
try {
System.out.println("spyEnhance");
}
catch (Throwable throwable) {
Spy.spyMethodOnCallThrows((String)throwable.getClass().getName(), (String)"default", (int)1006);
throw throwable;
}
Spy.spyMethodOnCallReturn((String)"default", (int)1006);
/*32*/ Spy.Ret ret3 = Spy.spyMethodOnReturn((Object)"1", (String)"default", (int)1006);
int n2 = ret3.state;
if (n2 == 1) return (String)ret3.respond;
if (n2 == 2) Spy.Ret ret4;
throw (Throwable)ret4.respond;
return "1";
}
catch (Throwable throwable) {
Throwable throwable2 = throwable;
Spy.Ret ret = Spy.spyMethodOnThrows((Throwable)throwable2, (String)"default", (int)1006);
int n = ret.state;
if (n == 1) return (String)ret.respond;
if (n == 2) throw (Throwable)ret.respond;
throw throwable2;
}
}
}
Spy.spyMethodOnBefore源码如下:
public static Ret spyMethodOnBefore(final Object[] argumentArray,
final String namespace,
final int listenerId,
final int targetClassLoaderObjectID,
final String javaClassName,
final String javaMethodName,
final String javaMethodDesc,
final Object target) throws Throwable {
final Thread thread = Thread.currentThread();
if (selfCallBarrier.isEnter(thread)) {
return Ret.RET_NONE;
}
final SelfCallBarrier.Node node = selfCallBarrier.enter(thread);
try {
final SpyHandler spyHandler = namespaceSpyHandlerMap.get(namespace);
if (null == spyHandler) {
return Ret.RET_NONE;
}
return spyHandler.handleOnBefore(
listenerId, targetClassLoaderObjectID, argumentArray,
javaClassName,
javaMethodName,
javaMethodDesc,
target
);
} catch (Throwable cause) {
handleException(cause);
return Ret.RET_NONE;
} finally {
selfCallBarrier.exit(thread, node);
}
}
通过反编译的代码我们可以看出,通过注入的Spy.spyMethodOnBefore()方法作为sandbox的入口,然后调用了sandbox的事件分发处理器EventListener.onEvent()方法,sandbox通过Spy完成了目标JVM和sandbox的通讯,打开了链接两个世界的大门。
sandbox增强器EventEnhancer内部提供了输出增强类的方法,是否输出增强代码的开关isDumpClass=true需要手动开启,开关打开后,在自己项目的sandbox-class-dump目录可以查看到被增强的所有类,源码如下:
private static final boolean isDumpClass = true;
/*
* dump class to file
* 用于代码调试
*/
private static byte[] dumpClassIfNecessary(String className, byte[] data) {
if (!isDumpClass) {
return data;
}
final File dumpClassFile = new File("./sandbox-class-dump/" + className + ".class");
final File classPath = new File(dumpClassFile.getParent());
// 创建类所在的包路径
if (!classPath.mkdirs()
&& !classPath.exists()) {
logger.warn("create dump classpath={} failed.", classPath);
return data;
}
// 将类字节码写入文件
try {
writeByteArrayToFile(dumpClassFile, data);
logger.info("dump {} to {} success.", className, dumpClassFile);
} catch (IOException e) {
logger.warn("dump {} to {} failed.", className, dumpClassFile, e);
}
return data;
}
核心类的加载
命令执行原理
sandbox完成启动后,后续的所有命令的执行实则是直接访问的HTTP服务器,列如 ./sandbox.sh -p 7640 -l命令,最后执行的是 curl 命令,翻译过来是:curl -N -s "http://10.242.232.9:8820/sandbox/default/module/http/sandbox-module-mgr/list"。





![[C++探索之旅] 第一部分第十一课:小练习,猜单词 - 鹿快](https://img.lukuai.com/blogimg/20251015/da217e2245754101b3d2ef80869e9de2.jpg)










暂无评论内容