JVM-Sandbox 啟動(dòng)有兩種方式:ATTACH
和AGENT
誊抛。
AGENT 方式的入口
必須和服務(wù)一起啟動(dòng)度气,需要修改服務(wù)的啟動(dòng)命令陵霉,如:
java -javaagent:${HOME}/sandbox/lib/sandbox-agent.jar=server.port=8820\;server.ip=0.0.0.0 \
-jar ${HOME}/.sandbox-module/repeater-bootstrap.jar
通過(guò)sandbox-agent.jar
的pom文件汁展;或者通過(guò)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
已創(chuàng)建: META-INF/
已解壓: META-INF/MANIFEST.MF
已創(chuàng)建: com/
已創(chuàng)建: com/alibaba/
已創(chuàng)建: com/alibaba/jvm/
已創(chuàng)建: com/alibaba/jvm/sandbox/
已創(chuàng)建: 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
通過(guò)上述兩種方式我們可以找到程序入口類是AgentLauncher
寸五,這種方式啟動(dòng)是調(diào)用的premain
方法:
/**
* 啟動(dòng)加載
*
* @param featureString 啟動(dòng)參數(shù)
* [namespace,prop]
* @param inst inst
*/
public static void premain(String featureString, Instrumentation inst) {
System.out.println("Sandbox 以Agent方式啟動(dòng)");
LAUNCH_MODE = LAUNCH_MODE_AGENT;
install(toFeatureMap(featureString), inst);
}
這里的核心是install(toFeatureMap(featureString), inst);
方法梳凛,主要作用是在當(dāng)前JVM安裝jvm-sandbox
。
ATTACH 方式的入口
即插即用的啟動(dòng)模式播歼,可以在不重啟目標(biāo)JVM的情況下完成沙箱的植入伶跷。原理和GREYS掰读、BTrace類似秘狞,利用了JVM的Attach機(jī)制實(shí)現(xiàn),它是調(diào)用的agentmain
方法蹈集。
這種方式加載稍微復(fù)雜一點(diǎn)烁试,他的啟動(dòng)入口是執(zhí)行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}'
的作用是用來(lái)找到當(dāng)前jvm程序的進(jìn)程號(hào)拢肆,轉(zhuǎn)換過(guò)來(lái)的命令是:./sandbox.sh -p 67672
减响。
深入sandbox.sh
這個(gè)腳本可以發(fā)現(xiàn)其核心是attach_jvm
函數(shù)。:
# 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
函數(shù)首先會(huì)生成一個(gè)帶時(shí)間戳信息的唯一token郭怪,然后使用java命令啟動(dòng)sandbox代理支示,最后獲取attach結(jié)果的網(wǎng)絡(luò)信息。核心是執(zhí)行了java -jar sandbox-cor.jar
然后傳了三個(gè)參數(shù)鄙才,簡(jiǎn)化后的關(guān)鍵信息如下:
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"
通過(guò)上述sandbox-agent.jar
查詢?nèi)肟陬惖姆绞剿毯瑁梢哉业?code>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
方法通過(guò)解析接口傳過(guò)來(lái)的三個(gè)參數(shù)嘴纺,三個(gè)參數(shù)分別為:
- targetJvmPid(PID(JVM進(jìn)程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栽渴。
最后通過(guò)獲取到的信息調(diào)用VirtualMachine.attach()
方法,通過(guò)attach
來(lái)執(zhí)行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實(shí)現(xiàn)原理可以參考如下資料:
- Java Attach機(jī)制簡(jiǎn)介: https://blog.csdn.net/u013332124/article/details/88362317
- Java Attach源碼解析:https://www.cnblogs.com/Jack-Blog/p/15026267.html
通過(guò)上述方法就調(diào)用了sandbox-agent.jar
中的AgentLauncher
類的agentmain
方法:
/**
* 動(dòng)態(tài)加載
*
* @param featureString 啟動(dòng)參數(shù)
* [namespace,token,ip,port,prop]
* @param inst inst
*/
public static void agentmain(String featureString, Instrumentation inst) {
System.out.println("Sandbox 以ATTACH方式啟動(dòng)");
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)
方法。
由此可見(jiàn)ATTACH
和AGENT
兩種啟動(dòng)方式最后都是調(diào)用的 install(featureMap, inst)
子方法來(lái)完成sandbox的加載场梆。
Agent 初始化過(guò)程(install方法)
在 install 方法中完成對(duì) agent 的初始化墅冷,在初始化的過(guò)程中使用到了自定義的 SandboxClassLoader 對(duì)沙箱類進(jìn)行加載,ModuleJarClassLoader 對(duì)./modele
辙谜、~/.sanbox-modele
目錄中module俺榆。jar進(jìn)行加載,實(shí)現(xiàn)沙箱內(nèi)部類與業(yè)務(wù)類隔離装哆。
sandbox-agent.jar
中的AgentLauncher
類的install
方法源碼如下:
/**
* 在當(dāng)前JVM安裝jvm-sandbox
*
* @param featureMap 啟動(dòng)參數(shù)配置
* @param inst inst
* @return 服務(wù)器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
)));
// 構(gòu)造自定義的類加載器罐脊,盡量減少Sandbox對(duì)現(xiàn)有工程的侵蝕
final ClassLoader sandboxClassLoader = loadOrDefineClassLoader(
namespace,
getSandboxCoreJarPath(home)
// SANDBOX_CORE_JAR_PATH
);
// CoreConfigure類定義
final Class<?> classOfConfigure = sandboxClassLoader.loadClass(CLASS_OF_CORE_CONFIGURE);
// 反序列化成CoreConfigure類實(shí)例
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);
// 如果未綁定,則需要綁定一個(gè)地址
if (!isBind) {
try {
classOfProxyServer
.getMethod("bind", classOfConfigure, Instrumentation.class)
.invoke(objectOfProxyServer, objectOfCoreConfigure, inst);
} catch (Throwable t) {
classOfProxyServer.getMethod("destroy").invoke(objectOfProxyServer);
throw t;
}
}
// 返回服務(wù)器綁定的地址
return (InetSocketAddress) classOfProxyServer
.getMethod("getLocal")
.invoke(objectOfProxyServer);
} catch (Throwable cause) {
throw new RuntimeException("sandbox attach failed.", cause);
}
}
核心流程:
- 通過(guò)
Instrumentation
調(diào)用BootstrapClassLoader
去加載sandbox-spy.jar
定嗓,sandbox-spy.jar
的主要作用是完成目標(biāo)JVM和sandbox的通訊。 - 創(chuàng)建
SandboxClassLoader
萍桌,并通過(guò)該ClassLoader去加載sandbox-core.jar
宵溅。 - 通過(guò)
SandboxClassLoader
去加載CoreConfigure
類,然后將所有沙箱配置映射賦值到該類的實(shí)例上炎。 - 通過(guò)
SandboxClassLoader
去加載ProxyCoreServer
類恃逻,并獲得一個(gè)JettyCoreServer
實(shí)例。 - 然后調(diào)用
JettyCoreServer
的bind
方法完成Spy的初始化(SpyUtils.init(cfg.getNamespace());
)藕施、HTTP 服務(wù)的初始化和啟動(dòng)寇损、通過(guò)ModuleJarClassLoader
加載所有mudule
(jvmSandbox.getCoreModuleManager().reset();
)。 - 最后裳食,返回代理核心服務(wù)器
JettyCoreServer
的服務(wù)器綁定的地址矛市。
Spy 間諜類
install
方法首先會(huì)通過(guò)Instrumentation
實(shí)例將 sandbox-spy.jar
添加到 BootstrapClassLoader
的搜索范圍內(nèi)。
// 將Spy注入到BootstrapClassLoader
inst.appendToBootstrapClassLoaderSearch(new JarFile(new File(
getSandboxSpyJarPath(home)
// SANDBOX_SPY_JAR_PATH
)));
使用BootstrapClassLoader去加載spy的最要目的應(yīng)該是保證Spy能增強(qiáng)所有的類诲祸,包括JDK自帶的一些類浊吏,Spy的主要作用是完成目標(biāo)JVM和sandbox間的通訊,sandbox
會(huì)將方法的執(zhí)行分為三個(gè)階段BEFORE
(方法執(zhí)行前)救氯、RETURN
(方法返回)和 THROWS
(方法異常) 三個(gè)環(huán)節(jié)找田。
// BEFORE
try {
/*
* do something...
*/
// RETURN
return;
} catch (Throwable cause) {
// THROWS
}
基于BEFORE
、RETURN
和THROWS
三個(gè)環(huán)節(jié)事件分離着憨,沙箱的模塊可以完成很多類AOP的操作墩衙。
- 可以感知和改變方法調(diào)用的入?yún)?/li>
- 可以感知和改變方法調(diào)用返回值和拋出的異常
- 可以改變方法執(zhí)行的流程
- 在方法體執(zhí)行之前直接返回自定義結(jié)果對(duì)象,原有方法代碼將不會(huì)被執(zhí)行
- 在方法體返回之前重新構(gòu)造新的結(jié)果對(duì)象享扔,甚至可以改變?yōu)閽伋霎惓?/li>
- 在方法體拋出異常之后重新拋出新的異常底桂,甚至可以改變?yōu)檎7祷?/li>
要完成這些動(dòng)作都是依賴Spy暴露出來(lái)的xxxOnBefore()
、xxxOnReturn()
和xxxOnThrows()
等鉤子函數(shù)來(lái)完成通訊惧眠,下圖是官網(wǎng)提供的圖:
為了更加直觀的看到代碼增強(qiáng)后的效果籽懦,我在我的服務(wù)里面新寫(xiě)了下面一個(gè)測(cè)試類,并通過(guò)自帶的debug-trace
模塊來(lái)查看spyEnhance
方法的執(zhí)行耗時(shí)氛魁,然后反編譯JVM中內(nèi)存中的class文件查看效果暮顺。
原始TestService類:
@Service("testService")
public class TestService {
/**
* 未增強(qiáng)方法
*
* @param name
* @return
*/
public String notEnhance(String name) {
System.out.println("notEnhance");
return "1";
}
/**
* 增強(qiáng)方法
*
* @return
*/
public String spyEnhance() {
System.out.println("spyEnhance");
return "1";
}
}
通過(guò)命令監(jiān)聽(tīng)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 內(nèi)存中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);
}
}
通過(guò)反編譯的代碼我們可以看出,通過(guò)注入的Spy.spyMethodOnBefore()
方法作為sandbox
的入口秀存,然后調(diào)用了sandbox
的事件分發(fā)處理器EventListener.onEvent()
方法捶码,sandbox
通過(guò)Spy
完成了目標(biāo)JVM和sandbox
的通訊,打開(kāi)了鏈接兩個(gè)世界的大門(mén)或链。
sandbox
增強(qiáng)器EventEnhancer
內(nèi)部提供了輸出增強(qiáng)類的方法惫恼,是否輸出增強(qiáng)代碼的開(kāi)關(guān)isDumpClass=true
需要手動(dòng)開(kāi)啟,開(kāi)關(guān)打開(kāi)后澳盐,在自己項(xiàng)目的sandbox-class-dump
目錄可以查看到被增強(qiáng)的所有類祈纯,源碼如下:
private static final boolean isDumpClass = true;
/*
* dump class to file
* 用于代碼調(diào)試
*/
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());
// 創(chuàng)建類所在的包路徑
if (!classPath.mkdirs()
&& !classPath.exists()) {
logger.warn("create dump classpath={} failed.", classPath);
return data;
}
// 將類字節(jié)碼寫(xiě)入文件
try {
writeByteArrayToFile(dumpClassFile, data);
logger.info("dump {} to {} success.", className, dumpClassFile);
} catch (IOException e) {
logger.warn("dump {} to {} failed.", className, dumpClassFile, e);
}
return data;
}
核心類的加載
命令執(zhí)行原理
sandbox
完成啟動(dòng)后令宿,后續(xù)的所有命令的執(zhí)行其實(shí)是直接訪問(wèn)的HTTP服務(wù)器,比如 ./sandbox.sh -p 7640 -l
命令腕窥,最后執(zhí)行的是 curl 命令粒没,翻譯過(guò)來(lái)是:curl -N -s "http://10.242.232.9:8820/sandbox/default/module/http/sandbox-module-mgr/list"
。