簡(jiǎn)介
JVM-SANDBOX(沙箱)實(shí)現(xiàn)了一種在不重啟、不侵入目標(biāo)JVM應(yīng)用的AOP解決方案。
沙箱的特性
-
無(wú)侵入:目標(biāo)應(yīng)用無(wú)需重啟也無(wú)需感知沙箱的存在 -
類隔離:沙箱以及沙箱的模塊不會(huì)和目標(biāo)應(yīng)用的類相互干擾 -
可插拔:沙箱以及沙箱的模塊可以隨時(shí)加載和卸載,不會(huì)在目標(biāo)應(yīng)用留下痕跡 -
多租戶:目標(biāo)應(yīng)用可以同時(shí)掛載不同租戶下的沙箱并獨(dú)立控制 -
高兼容:支持JDK[6,11]
沙箱常見(jiàn)應(yīng)用場(chǎng)景
- 線上故障定位
- 線上系統(tǒng)流控
- 線上故障模擬
- 方法請(qǐng)求錄制和結(jié)果回放
- 動(dòng)態(tài)日志打印
- 安全信息監(jiān)測(cè)和脫敏
JVM-SANDBOX還能幫助你做很多很多,取決于你的腦洞有多大了。
實(shí)時(shí)無(wú)侵入AOP框架
在常見(jiàn)的AOP框架實(shí)現(xiàn)方案中,有靜態(tài)編織和動(dòng)態(tài)編織兩種。
- 靜態(tài)編織:靜態(tài)編織發(fā)生在字節(jié)碼生成時(shí)根據(jù)一定框架的規(guī)則提前將AOP字節(jié)碼插入到目標(biāo)類和方法中,實(shí)現(xiàn)AOP;
-
動(dòng)態(tài)編織:動(dòng)態(tài)編織則允許在JVM運(yùn)行過(guò)程中完成指定方法的AOP字節(jié)碼增強(qiáng).常見(jiàn)的動(dòng)態(tài)編織方案大多采用重命名原有方法,再新建一個(gè)同簽名的方法來(lái)做代理的工作模式來(lái)完成AOP的功能(常見(jiàn)的實(shí)現(xiàn)方案如CgLib),但這種方式存在一些應(yīng)用邊界:
- 侵入性:對(duì)被代理的目標(biāo)類需要進(jìn)行侵入式改造。比如:在Spring中必須是托管于Spring容器中的Bean。
- 固化性:目標(biāo)代理方法在啟動(dòng)之后即固化,無(wú)法重新對(duì)一個(gè)已有方法進(jìn)行AOP增強(qiáng)。
要解決無(wú)侵入的特性需要AOP框架具備 在運(yùn)行時(shí)完成目標(biāo)方法的增強(qiáng)和替換。在JDK的規(guī)范中運(yùn)行期重定義一個(gè)類必須準(zhǔn)循以下原則:
- 不允許新增、修改和刪除成員變量
- 不允許新增和刪除方法
- 不允許修改方法簽名
JVM-SANDBOX屬于基于Instrumentation的動(dòng)態(tài)編織類的AOP框架,通過(guò)精心構(gòu)造了字節(jié)碼增強(qiáng)邏輯,使得沙箱的模塊能在不違反JDK約束情況下實(shí)現(xiàn)對(duì)目標(biāo)應(yīng)用方法的無(wú)侵入運(yùn)行時(shí)AOP攔截。
源碼
源碼下載-一定要切換到穩(wěn)定版本:
https://github.com/alibaba/jvm-sandbox
也可以直接下載打包好的版本
https://github.com/alibaba/jvm-sandbox/releases/tag/1.3.3
安裝
源碼包導(dǎo)入開(kāi)發(fā)工具后切換到bin目錄:

- 注釋掉不需要的插件:
<!-- 為了發(fā)布到MAVEN中央倉(cāng)庫(kù)而用的插件 -->
<plugin>
<groupId>org.sonatype.plugins</groupId>
<artifactId>nexus-staging-maven-plugin</artifactId>
<version>1.6.7</version>
<extensions>true</extensions>
<configuration>
<serverId>luanjia-ossrh</serverId>
<nexusUrl>https://oss.sonatype.org/</nexusUrl>
<autoReleaseAfterClose>true</autoReleaseAfterClose>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<version>1.5</version>
<executions>
<execution>
<id>sign-artifacts</id>
<phase>verify</phase>
<goals>
<goal>sign</goal>
</goals>
</execution>
</executions>
</plugin>
- 打包
打包前先將sandbox-packages.sh腳本中執(zhí)行單測(cè)的代碼調(diào)過(guò),修改如下:
# maven package the sandbox
mvn clean cobertura:cobertura package -Dmaven.test.skip=true -f ../pom.xml \
|| exit_on_err 1 "package sandbox failed."
修改完成后執(zhí)行腳本./sandbox-packages.sh,腳本執(zhí)行后會(huì)在工程target目錄下生成安裝好的文件,如圖:

sandbox-stable-bin.zip就是生成好的安裝文件,如果需要在哪里安裝直接將這個(gè)包在那個(gè)服務(wù)器解壓即可。
腳本會(huì)將打包后的文件拷貝到安裝目錄:
# copy jar to TARGET_DIR
cp ../sandbox-core/target/sandbox-core-*-jar-with-dependencies.jar ${SANDBOX_TARGET_DIR}/lib/sandbox-core.jar \
&& cp ../sandbox-agent/target/sandbox-agent-*-jar-with-dependencies.jar ${SANDBOX_TARGET_DIR}/lib/sandbox-agent.jar \
&& cp ../sandbox-spy/target/sandbox-spy-*-jar-with-dependencies.jar ${SANDBOX_TARGET_DIR}/lib/sandbox-spy.jar \
&& cp sandbox-logback.xml ${SANDBOX_TARGET_DIR}/cfg/sandbox-logback.xml \
&& cp sandbox.properties ${SANDBOX_TARGET_DIR}/cfg/sandbox.properties \
&& cp sandbox.sh ${SANDBOX_TARGET_DIR}/bin/sandbox.sh \
&& cp install-local.sh ${SANDBOX_TARGET_DIR}/install-local.sh
目錄結(jié)構(gòu)
./sandbox/
+--bin/
| +--sandbox.sh
|
+--cfg/
| +--sandbox-logback.xml
| +--sandbox.properties
| `--version
|
+--lib/
| +--sandbox-agent.jar
| +--sandbox-core.jar
| `--sandbox-spy.jar
|
+--provider/
|. `--sandbox-mgr-provider.jar
|
`--module/
`--sandbox-mgr-module.jar
詳細(xì)說(shuō)明可以參考官網(wǎng):https://github.com/alibaba/jvm-sandbox/wiki/CONFIG
安裝
方式一直接拷貝打包好的安裝文件:
直接將上面生成的包sandbox-stable-bin.zip拷貝到安裝目錄,直接解壓即可。
方式二直接網(wǎng)上下載release包:
# 下載最新版本的JVM-SANDBOX
wget http://ompc.oss-cn-hangzhou.aliyuncs.com/jvm-sandbox/release/sandbox-stable-bin.zip
# 解壓
unzip sandbox-stable-bin.zip
方式三執(zhí)行install-local腳本。
不能直接執(zhí)行工程bin目錄下的install-local.sh腳本,需要執(zhí)行我們上面打包好的target中的腳本,如圖:

執(zhí)行安裝腳本(本地調(diào)試用這種方式):
cd ../target/sandbox
./install-local.sh -p /Users/admin/Documents/
-p:表示安裝目錄,默認(rèn)安裝在${HOME}/.opt目錄下。
啟動(dòng)
sandbox啟動(dòng)有兩種方式:ATTACH和AGENT。
ATTACH方式啟動(dòng)
即插即用的啟動(dòng)模式,可以在不重啟目標(biāo)JVM的情況下完成沙箱的植入。原理和GREYS、BTrace類似,利用了JVM的Attach機(jī)制實(shí)現(xiàn)。
假設(shè)目標(biāo)JVM進(jìn)程號(hào)為'33342',進(jìn)入沙箱執(zhí)行腳本
cd {安裝目錄}/sandbox/bin
掛載目標(biāo)應(yīng)用
./sandbox.sh -p 33342
掛載成功后會(huì)提示
./sandbox.sh -p 33342
NAMESPACE : default
VERSION : 1.2.0
MODE : ATTACH
SERVER_ADDR : 0.0.0.0
SERVER_PORT : 55756
UNSAFE_SUPPORT : ENABLE
SANDBOX_HOME : /Users/vlinux/opt/sandbox
SYSTEM_MODULE_LIB : /Users/vlinux/opt/sandbox/module
USER_MODULE_LIB : ~/.sandbox-module;
SYSTEM_PROVIDER_LIB : /Users/vlinux/opt/sandbox/provider
EVENT_POOL_SUPPORT : DISABLE
卸載沙箱
./sandbox.sh -p 33342 -S
jvm-sandbox[default] shutdown finished.
./sandbox.sh命令參數(shù)詳解: https://github.com/alibaba/jvm-sandbox/wiki/CONFIG
AGENT方式啟動(dòng)
有些時(shí)候我們需要沙箱工作在應(yīng)用代碼加載之前,或者一次性渲染大量的類、加載大量的模塊,此時(shí)如果用ATTACH方式加載,可能會(huì)引起目標(biāo)JVM的卡頓或停頓(GC),這就需要啟用到AGENT的啟動(dòng)方式。
假設(shè)SANDBOX被安裝在了/Users/luanjia/pe/sandbox,需要在JVM啟動(dòng)參數(shù)中增加上
-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8000 -javaagent:/Users/luanjia/pe/sandbox/lib/sandbox-agent.jar
這樣沙箱將會(huì)伴隨著JVM啟動(dòng)而主動(dòng)啟動(dòng)并加載對(duì)應(yīng)的沙箱模塊。
對(duì)比Agent和ATTACH,Agent(premain)是JDK5就提供的,主要提供JVM啟動(dòng)時(shí)的代理加載功能,靈活性和易用性欠佳;ATTACH(agentmain)則是JDK6提出的,能夠支持JVM運(yùn)行時(shí)的代理啟動(dòng),靈活性和易用性好,給線上調(diào)試等提供了底層能力支持。
調(diào)試
sandbox的調(diào)試需要使用遠(yuǎn)程調(diào)試,在目標(biāo)應(yīng)用啟動(dòng)時(shí)加上遠(yuǎn)程調(diào)試參數(shù)(目標(biāo)應(yīng)用必須以非DEBUG模式啟動(dòng)):
-Xdebug
-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8000

使用IDEA的遠(yuǎn)程調(diào)試功能,并加上如下配置:
-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5050

sandbox以ATTACH方式啟動(dòng)即可:
./sandbox.sh -p `ps -ef | grep java | grep 'com.alibaba.repeater.console.start.Application' | grep -v grep | awk '{print $2}'`

日志
Sandbox的日志
sandbox的日志配置是在cfg下的sandbox-logback.xml文件。這個(gè)文件會(huì)在沙箱啟動(dòng)時(shí)被加載,通過(guò)配置我們可以找到日志文件位置為${user.home}/logs/sandbox/sandbox.log.%d{yyyy-MM-dd}。
<?xml version="1.0" encoding="UTF-8" ?>
<configuration scan="true" scanPeriod="10000">
<appender name="SANDBOX-FILE-APPENDER" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${user.home}/logs/sandbox/sandbox.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>${user.home}/logs/sandbox/sandbox.log.%d{yyyy-MM-dd}</FileNamePattern>
<MaxHistory>30</MaxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} %SANDBOX_NAMESPACE %-5level %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<root level="info">
<appender-ref ref="SANDBOX-FILE-APPENDER"/>
</root>
</configuration>
module 日志
module本身是一個(gè)單獨(dú)項(xiàng)目(jar),所以module的日志是由項(xiàng)目本身控制,以源碼中的sandbox-debug-module為例,他其實(shí)是一個(gè)module編寫的是示例工程,他的日志可以直接查看項(xiàng)目中的logback.xml文件。
<?xml version="1.0" encoding="UTF-8" ?>
<configuration scan="true" scanPeriod="10000">
<!-- DEBUG模塊:Servlet監(jiān)控日志 -->
<logger name="DEBUG-SERVLET-ACCESS" level="INFO">
<appender class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${user.home}/logs/sandbox/debug/servlet-access.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>${user.home}/logs/sandbox/debug/servlet-access.log.%d{yyyy-MM-dd}</FileNamePattern>
<MaxHistory>30</MaxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
</logger>
<!-- DEBUG模塊:Exception監(jiān)控日志 -->
<logger name="DEBUG-EXCEPTION-LOGGER" level="INFO">
<appender class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${user.home}/logs/sandbox/debug/exception-monitor.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>${user.home}/logs/sandbox/debug/exception-monitor.log.%d{yyyy-MM-dd}</FileNamePattern>
<MaxHistory>30</MaxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
</logger>
<!-- DEBUG模塊:JDBC監(jiān)控日志 -->
<logger name="DEBUG-JDBC-LOGGER" level="INFO">
<appender class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${user.home}/logs/sandbox/debug/jdbc-monitor.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>${user.home}/logs/sandbox/debug/jdbc-monitor.log.%d{yyyy-MM-dd}</FileNamePattern>
<MaxHistory>30</MaxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
</logger>
<!-- DEBUG模塊:JDBC監(jiān)控日志 -->
<logger name="DEBUG-SPRING-LOGGER" level="INFO">
<appender class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${user.home}/logs/sandbox/debug/spring-monitor.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>${user.home}/logs/sandbox/debug/spring-monitor.log.%d{yyyy-MM-dd}</FileNamePattern>
<MaxHistory>30</MaxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
</logger>
<!-- DEBUG模塊默認(rèn)日志 -->
<root level="INFO">
<appender class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${user.home}/logs/sandbox/debug/debug.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>${user.home}/logs/sandbox/debug/debug.log.%d{yyyy-MM-dd}</FileNamePattern>
<MaxHistory>30</MaxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
</root>
<!-- DEBUG模塊:LifeCycle監(jiān)控日志 -->
<logger name="DEBUG-LIFECYCLE-LOGGER" level="INFO">
<appender class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${user.home}/logs/sandbox/debug/lifecycle-monitor.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>${user.home}/logs/sandbox/debug/lifecycle-monitor.log.%d{yyyy-MM-dd}</FileNamePattern>
<MaxHistory>30</MaxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
</logger>
</configuration>