JVM Sandbox入门详解
一. 概述
在日常开发中,经常会接触到面向AOP编程的思想,我们通常会使用Spring AOP来做统一的权限认证、异常捕获返回、日志记录等工作。之所以使用Spring AOP来实现上述功能,是因为这些场景本质上来说都是与业务场景挂钩的,但是具有一定的抽象程度,并且绝大多数业务逻辑类都已经被Spring容器托管了。但是这个世界上不是所有的Java应用都接入了Spring框架,接入Spring的应用也不是所有类都会被Spring容器托管,例如很多中间件代码、三方包代码,Java原生代码,都不能被Spring AOP代理到,所以在很多场景下Spring AOP都无法满足AOP编码的需求。
上面是从技术实现出发,说明了Spring AOP的局限性。如果从领域职责出发,像应用指标监控,全链路监控,故障定位,流量回放等与业务无关的场景代码放在业务系统中实现显然并不合适,此时如果有一种更为通用的AOP方式,将通用逻辑与业务逻辑解耦,岂不是美哉。
在 《JavaAgent详解》 中,我们提到了Java自身提供了JVM Instrumentation等功能,允许使用者以通过一系列API完成对JVM的复杂控制,以及字节码增强。但是如果使用原生的Java Agent能力,从技术实现上来说没有太大问题(毕竟越底层的技术越灵活,能够实现的功能越多),但是实现成本和门槛都比较高,开发者需要小心翼翼的操作目标类的字节码,在想要监控的目标方法前后插入对应的业务逻辑。虽然市面上有很多类似于 Byte Buddy 字节码增强库,大大简化了字节码增强的复杂度,但是开发成本和技术门槛都相对很高。而由阿里巴巴开源的 jvm-sandbox 在 Java Instrumentation API 的基础上实现了运行时 AOP 增强的能力,开发者不需要去操作目标类的字节码,即可对目标类进行字节码增强。
是不是看到这里还是不清楚我在讲什么?别急,我举几个典型的 jvm-sandbox 应用场景:
- 流量回放:如何录制线上应用每次接口请求的入参和出参?改动应用代码固然可以,但成本太大,通过jvm-sandbox,可以直接在不修改代码的情况下,直接抓取接口的出入参。
- 安全漏洞热修复:假设某个三方包(例如出名的fastjson)又出现了漏洞,集团内那么多应用,一个个发布新版本修复,漏洞已经造成了大量破坏。通过jvm-sandbox,直接修改替换有漏洞的代码,及时止损。
- 接口故障模拟:想要模拟某个接口超时5s后返回false的情况,jvm-sandbox很轻松就能实现。
- 故障定位:像Arthas类似的功能。
- 接口限流:动态对指定的接口做限流。
- 日志打印
- …
可以看到,借助jvm-sandbox,你可以实现很多之前在业务代码中做不了的事,大大拓展了可操作的范围。
二. 整体架构
本章节不详细讲述JVM SandBox的所有架构设计,只讲其中几个最重要的特性。详细的架构设计可以看原框架代码仓库的Wiki。
2.1 类隔离
很多框架通过破坏双亲委派(我更愿意称之为直系亲属委派)来实现类隔离,SandBox也不例外。它通过自定义的SandboxClassLoader破坏了双亲委派的约定,实现了几个隔离特性:
- 和目标应用的类隔离:不用担心加载沙箱会引起原应用的类污染、冲突。
- 模块之间类隔离:做到模块与模块之间、模块和沙箱之间、模块和应用之间互不干扰。

2.2 无侵入AOP与事件驱动
在常见的AOP框架实现方案中,有静态编织和动态编织两种。
静态编织:静态编织发生在字节码生成时根据一定框架的规则提前将AOP字节码插入到目标类和方法中,实现AOP;
动态编织
:动态编织则允许在JVM运行过程中完成指定方法的AOP字节码增强.常见的动态编织方案大多采用重命名原有方法,再新建一个同签名的方法来做代理的工作模式来完成AOP的功能(常见的实现方案如CgLib),但这种方式存在一些应用边界:
- 侵入性:对被代理的目标类需要进行侵入式改造。比如:在Spring中必须是托管于Spring容器中的Bean
- 固化性:目标代理方法在启动之后即固化,无法重新对一个已有方法进行AOP增强
要解决无侵入的特性需要AOP框架具备 在运行时完成目标方法的增强和替换。在JDK的规范中运行期重定义一个类必须准循以下原则
- 不允许新增、修改和删除成员变量
- 不允许新增和删除方法
- 不允许修改方法签名
JVM-SANDBOX属于基于Instrumentation的动态编织类的AOP框架,通过精心构造了字节码增强逻辑,使得沙箱的模块能在不违反JDK约束情况下实现对目标应用方法的无侵入运行时AOP拦截。

从上图中,可以看到一个方法的整个执行周期都被代码“加强”了,能够带来的好处就是你在使用JVM SandBox只需要对于方法的事件进行处理。
// BEFORE
try {
/*
* do something...
*/
// RETURN
return;
} catch (Throwable cause) {
// THROWS
}
在沙箱的世界观中,任何一个Java方法的调用都可以分解为BEFORE、RETURN和THROWS三个环节,由此在三个环节上引申出对应环节的事件探测和流程控制机制。
基于BEFORE、RETURN和THROWS三个环节事件分离,沙箱的模块可以完成很多类AOP的操作。
- 可以感知和改变方法调用的入参
- 可以感知和改变方法调用返回值和抛出的异常
- 可以改变方法执行的流程
- 在方法体执行之前直接返回自定义结果对象,原有方法代码将不会被执行
- 在方法体返回之前重新构造新的结果对象,甚至可以改变为抛出异常
- 在方法体抛出异常之后重新抛出新的异常,甚至可以改变为正常返回
三. 代码实践
我们还是以 《JavaAgent详解》 中的案例为例,首先定义 Person 类:
package cn.bigcoder.demo.agenttest;
import java.util.Random;
public class Person {
public String test() {
System.out.println("执行测试方法");
try {
Thread.sleep(new Random().nextInt(1000));
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return "I'm ok";
}
}
该类中有一个 test 方法,会在执行时随机停顿一段时间,模拟业务代码中的无法预测的执行时间,定义Main方法不断调用 Person.test 方法,模拟不间断的业务请求:
package cn.bigcoder.demo.agenttest;
import java.util.Scanner;
/**
* @author: bigcoder
**/
public class AgentTest {
public static void main(String[] args) {
while (true) {
Person person = new Person();
person.test();
}
}
}
我们通过jvm-sandbox实现 Person.test 方法执行耗时的监控打印。
3.1 新建sandbox模块工程
首先新建Maven工程,假设用的是MAVEN,这里通过将parent指向sandbox-module-starter来简化我们的配置工作
<parent>
<groupId>com.alibaba.jvm.sandbox</groupId>
<artifactId>sandbox-module-starter</artifactId>
<version>1.4.0</version>
</parent>
3.2 编写模块代码
package cn.bigcoder.demo.sandbox;
import com.alibaba.jvm.sandbox.api.Information;
import com.alibaba.jvm.sandbox.api.Module;
import com.alibaba.jvm.sandbox.api.ModuleLifecycle;
import com.alibaba.jvm.sandbox.api.annotation.Command;
import com.alibaba.jvm.sandbox.api.listener.ext.Advice;
import com.alibaba.jvm.sandbox.api.listener.ext.AdviceListener;
import com.alibaba.jvm.sandbox.api.listener.ext.EventWatchBuilder;
import com.alibaba.jvm.sandbox.api.resource.ModuleEventWatcher;
import org.kohsuke.MetaInfServices;
import javax.annotation.Resource;
/**
* @author: Jindong.Tian
* @date: 2023-05-27
**/
@MetaInfServices(Module.class)
@Information(id = "person-test-monitor")
public class PersonTimeMonitorModule implements Module {
@Resource
private ModuleEventWatcher moduleEventWatcher;
@Command("monitorExecuteTime")
public void monitorExecuteTime() {
new EventWatchBuilder(moduleEventWatcher)
// 增强 cn.bigcoder.demo.agenttest.Person 类
.onClass("cn.bigcoder.demo.agenttest.Person")
// 增强 cn.bigcoder.demo.agenttest.Person 类的test方法
.onBehavior("test")
.onWatch(new AdviceListener() {
@Override
protected void before(Advice advice) throws Throwable {
// 获取执行开始时间
advice.attach(System.currentTimeMillis());
}
@Override
public void afterReturning(Advice advice) throws Throwable {
// 在方法调用后计算方法耗时,并打印出来
long startTime = (long) advice.attachment();
long endTime = System.currentTimeMillis();
String className = advice.getBehavior().getDeclaringClass().getName();
String methodName = advice.getBehavior().getName();
System.out.println(className + "." + methodName + " executed in " + (endTime - startTime) + " ms");
}
});
}
}
3.3 Maven构建
$ mvn clean package
3.4 下载并安装沙箱
下载并安装最新版本沙箱:
下载地址:https://ompc.oss.aliyuncs.com/jvm-sandbox/release/sandbox-stable-bin.zip
执行安装
unzip sandbox-stable-bin.zip
cd sandbox
将打好的包复制到用户模块目录下:
cp ./jvm-sandbox-demo/target/jvm-sandbox-demo-1.0-SNAPSHOT.jar /home/bigcoder/.opt/sandbox
3.5 启动目标类
启动 AgentTest.main,此时代码一直输出:
执行测试方法
执行测试方法
执行测试方法
执行测试方法
...
3.6 启动沙箱,并激活模块
➜ sandbox jps -l
2241 cn.bigcoder.demo.agenttest.AgentTest
2264 jdk.jcmd/sun.tools.jps.Jps
➜ sandbox ./bin/sandbox.sh -p 2241 -d 'person-test-monitor/monitorExecuteTime'
使用 jps 命令查看目标进程ID,然后启动沙箱,-p 参数指定目标进程 pid,-d 参数指定要激活的的模块。
此时,我们可以看到控制台开始打印方法执行耗时了:

3.7 卸载沙箱
➜ sandbox ./bin/sandbox.sh -p 2241 -S
jvm-sandbox[default] shutdown finished.
此时,控制台又恢复了往日的平静,不再输出方法耗时:

3.8 agent方式增强
在 3.6 中我们是以attch方式增强已运行的JVM进程,有些时候我们需要沙箱工作在应用代码加载之前,或者一次性渲染大量的类、加载大量的模块,此时如果用ATTACH方式加载,可能会引起目标JVM的卡顿或停顿(GC),这就需要用到AGENT的启动方式。
假设SANDBOX被安装在了/homt/bigcoder/.opt/sandbox,需要在JVM启动参数中增加上 -javaagent:/homt/bigcoder/.opt/sandbox/lib/sandbox-agent.jar
四. 模块的生命周期
模块生命周期类型有模块加载、模块卸载、模块激活、模块冻结、模块加载完成五个状态。

- 模块加载:创建ClassLoader,完成模块的加载
- 模块卸载:模块增强的类会重新load,去掉增强的字节码
- 模块激活:模块被激活后,模块所增强的类将会被激活,所有
com.alibaba.jvm.sandbox.api.listener.EventListener将开始收到对应的事件 - 模块冻结:模块被冻结后,模块所持有的所有
com.alibaba.jvm.sandbox.api.listener.EventListener将被静默,无法收到对应的事件。需要注意的是,模块冻结后虽然不再收到相关事件,但沙箱给对应类织入的增强代码仍然还在。 - 模块加载完成:模块加载已经完成,这个状态是为了做日志处理,本身不会影响模块变更行为
模块可以通过实现com.alibaba.jvm.sandbox.api.ModuleLifecycle接口,对模块生命周期进行控制,接口中的方法:

- onLoad:模块开始加载之前调用
- onUnload:模块开始卸载之前调用
- onActive:模块被激活之前调用,抛出异常将会是阻止模块被激活的唯一方式
- onFrozen:模块被冻结之前调用,抛出异常将会是阻止模块被冻结的唯一方式
五. 小结
本文从 Spring AOP 的局限性出发,探讨了 jvm-sandbox 使用场景。如果使用 Java Instrumentation API 增强目标方法,理论上来说也能实现目标类增强,但是字节码修改的门槛过高。而 jvm-sandbox 提供了运行时AOP增强的能力,虽然底层仍然基于 Java Instrumentation API,但是程序员不再需要关心字节码增强的细节,就能完成对原有方法的逻辑增强。
我们引入了 《JavaAgent详解》 文章中相同的例子,讲解了如何使用 jvm-sandbox 去增强 Person.test 方法,从而统计方法执行耗时。该例只是一个简单的入门案例,如果对jvm-sandbox 感兴趣,可以学习官方沙箱分发包中自带的实用工具的例子./example/sandbox-debug-module.jar,代码在沙箱的sandbox-debug-module模块:
| 例子 | 例子说明 |
|---|---|
| DebugWatchModule.java | 模仿GREYS的watch命令 |
| DebugTraceModule.java | 模仿GREYES的trace命令 |
| DebugRalphModule.java | 无敌破坏王,故障注入(延时、熔断、并发限流、TPS限流) |
| LogExceptionModule.java | 记录下你的应用都发生了哪些异常 $HOME/logs/sandbox/debug/exception-monitor.log |
| LogServletAccessModule.java | 记录下你的应用的HTTP服务请求 $HOME/logs/sandbox/debug/servlet-access.log |
本文参考至:
JVM Sandbox入门详解的更多相关文章
- Linq之旅:Linq入门详解(Linq to Objects)
示例代码下载:Linq之旅:Linq入门详解(Linq to Objects) 本博文详细介绍 .NET 3.5 中引入的重要功能:Language Integrated Query(LINQ,语言集 ...
- SQL注入攻防入门详解
=============安全性篇目录============== 本文转载 毕业开始从事winfrm到今年转到 web ,在码农届已经足足混了快接近3年了,但是对安全方面的知识依旧薄弱,事实上是没机 ...
- SQL注入攻防入门详解(2)
SQL注入攻防入门详解 =============安全性篇目录============== 毕业开始从事winfrm到今年转到 web ,在码农届已经足足混了快接近3年了,但是对安全方面的知识依旧薄弱 ...
- Quartz 入门详解
Quartz是OpenSymphony开源组织在Job scheduling领域又一个开源项目,它可以与J2EE与J2SE应用程序相结合也可以单独使用.Quartz可以用来创建简单或为运行十个,百个, ...
- Redis快速入门详解
Redis入门详解 Redis简介 Redis安装 Redis配置 Redis数据类型 Redis功能 持久化 主从复制 事务支持 发布订阅 管道 虚拟内存 Redis性能 Redis部署 Redis ...
- [转]SQL注入攻防入门详解
原文地址:http://www.cnblogs.com/heyuquan/archive/2012/10/31/2748577.html =============安全性篇目录============ ...
- [置顶]
xamarin android toolbar(踩坑完全入门详解)
网上关于toolbar的教程有很多,很多新手,在使用toolbar的时候踩坑实在太多了,不好好总结一下,实在浪费.如果你想学习toolbar,你肯定会去去搜索androd toolbar,既然你能看到 ...
- 转:JAVAWEB开发之权限管理(二)——shiro入门详解以及使用方法、shiro认证与shiro授权
原文地址:JAVAWEB开发之权限管理(二)——shiro入门详解以及使用方法.shiro认证与shiro授权 以下是部分内容,具体见原文. shiro介绍 什么是shiro shiro是Apache ...
- webpack入门详解
webpack入门详解(基于webpack 3.5.4 2017-8-22) webpack常用命令: webpack --display-error-details //执行打包 webpa ...
- Scala 入门详解
Scala 入门详解 基本语法 Scala 与 Java 的最大区别是:Scala 语句末尾的分号 ; 是可选的 Scala 程序是对象的集合,通过调用彼此的方法来实现消息传递.类,对象,方法,实例变 ...
随机推荐
- java知识点查漏补缺-- 2020513
重载和重写 方法重载(overload): 必须是同一个类 方法名(也可以叫函数)一样 参数类型不一样或参数数量不一样 方法的重写(override)两同两小一大原则: 方法名相同,参数类型相同 子类 ...
- 最简编译CockroachDB 21.2
编译CockroachDB比较麻烦,尤其是从git下载代码编译还需要关联项目的下载,本文整理从官网下载代码的编译过程,非常简单,几乎没有异常,供大家参考. 编译CockroachDB 21.2 1.安 ...
- 基于 EventBridge 构建数据库应用集成
简介:本文重点介绍 EventBridge 的新特性:数据库 Sink 事件目标. 作者:赵海 引言 事件总线 EventBridge 是阿里云提供的一款无服务器事件总线服务,支持将阿里云服务.自定 ...
- 简单、有效、全面的Kubernetes监控方案
简介:近年来,Kubernetes作为众多公司云原生改造的首选容器化编排平台,越来越多的开发和运维工作都围绕Kubernetes展开,保证Kubernetes的稳定性和可用性是最基础的需求,而这其中 ...
- EDA 事件驱动架构与 EventBridge 二三事
简介: 事件驱动型架构 (EDA) 方兴未艾,作为一种 Serverless 化的应用概念对云原生架构具有着深远影响.当我们讨论到一个具体架构时,首当其冲的是它的发展是否具有技术先进性.这里从我们熟 ...
- Metasploit 实现木马生成、捆绑及免杀
简介: 在渗透测试的过程中,避免不了使用到社会工程学的方式来诱骗对方运行我们的木马或者点击我们准备好的恶意链接.木马的捆绑在社会工程学中是我们经常使用的手段,而为了躲避杀毒软件的查杀,我们又不得不对 ...
- [GPT] 数据分析工具可以使用机器学习技术来预测未来趋势和提供数据可视化?
数据分析工具使用机器学习技术来预测未来趋势和提供数据可视化是靠谱的. 机器学习算法可以通过对历史数据的学习来发现数据中的模式和趋势,并利用这些模式和趋势来预测未来的趋势.这种方法已经被广泛应用于许 ...
- WPF 使用 Dispatcher 的 InvokeAsync 和 BeginInvoke 的异常处理差别
一般认为 WPF 的 Dispatcher 的 InvokeAsync 方法是 BeginInvoke 方法的平替方法和升级版,接近在任何情况下都应该在业务层使用 InvokeAsync 方法代替 B ...
- dotnet 在 UOS 国产系统上安装 dotnet sdk 的方法
本文告诉大家如何在 UOS 国产系统上安装 dotnet sdk 的方法 使用的 UOS 是 UOS 20 x64 版本,这个系统版本是基于 debian 10 的,可以使用 debian 10 的方 ...
- 魔方OA 数据字典
https://gitee.com/mojocube/mc-oa/blob/master/Data/%E6%95%B0%E6%8D%AE%E5%BA%93%E8%84%9A%E6%9C%AC.sql ...