JVM-SANDBOX(沙箱)实现了一种在不重启、不侵入目标JVM应用的AOP解决方案。

沙箱的特性

  1. 无侵入:目标应用无需重启也无需感知沙箱的存在
  2. 类隔离:沙箱以及沙箱的模块不会和目标应用的类相互干扰
  3. 可插拔:沙箱以及沙箱的模块可以随时加载和卸载,不会在目标应用留下痕迹
  4. 多租户:目标应用可以同时挂载不同租户下的沙箱并独立控制
  5. 高兼容:支持JDK[6,11]

沙箱常见应用场景

  • 线上故障定位
  • 线上系统流控
  • 线上故障模拟
  • 方法请求录制和结果回放
  • 动态日志打印
  • 安全信息监测和脱敏

1.1 AOP

在介绍 JVM SandBox 之前,我们先来回顾一下 AOP 技术。

AOP(面向切面编程,Aspect Oriented Programming)技术已被业界广泛应用,其思想是面向业务处理过程的某个步骤或阶段进行编程,这个步骤或阶段被称为切面,其目的是降低业务逻辑的各部分之间的耦合,常见的 AOP 实现基本原理有两种:代理和行为注入。

1)代理模式
在代理模式下,我们会创建一个代理对象来代理原对象的行为,代理对象拥有原对象行为执行的控制权,在这种模式下,我们基于代理对象在原对象行为执行的前后插入代码来实现 AOP。

图 2-1 代理模式

2)行为注入模式
在行为注入模式下,我们不会创建一个新的对象,而是修改原对象,在原对象行为的执行前后注入代码来实现 AOP。

图 2-2 行为注入模式

1.2 JVM SandBox

JVM SandBox 是阿里开源的一款 JVM 平台非侵入式运行期 AOP 解决方案,本质上是一种 AOP 落地形式。

为什么不采用 Spring AOP 方案呢?Spring AOP 方案的痛点在于不是所有业务代码都托管在 Spring 容器中,而且更底层的中间件代码、三方包代码无法纳入到回归测试范围,更糟糕的是测试框架会引入自身所依赖的类库,经常与业务代码的类库产生冲突,因此,JVM SandBox 应运而生。

JVM SandBox 本身是基于插件化的设计思想,允许用于以“模块”的方式基于 JVM SandBox 提供的 AOP 能力开发新的功能。基于 JVM SandBox,我们不需要关心如何在 JVM 层实现 AOP 的技术细节,只需要通过 JVM SandBox 提供的编程结构告诉“沙箱”,我们希望对哪些类哪些方法进行 AOP,在切面点做什么即可,JVM SandBox 模块功能编写起来非常简单。下面是一个示例模块代码:

@MetaInfServices(Module.class)  
@Information(id = "my-sandbox-module")// 模块名  
public class MySandBoxModule implements Module {  
    private Logger LOG = Logger.getLogger(MySandBoxModule.class.getName());  
    @Resource  
    private ModuleEventWatcher moduleEventWatcher;  
  
    @Command("addLog")// 模块命令名  
    public void addLog() {  
        new EventWatchBuilder(moduleEventWatcher)  
                .onClass("com.float.lu.DealGroupService")// 想要对 DealGroupService 这个类进行切面  
                .onBehavior("loadDealGroup")// 想要对上面类的 loadDealGroup 方法进行切面  
                .onWatch(new AdviceListener() {  
                    @Override  
                    protected void before(Advice advice) throws Throwable {  
                        LOG.info(" 方法名: " + advice.getBehavior().getName());// 在方法执行前打印方法的名字  
                    }  
                });  
    }  
}

如上面代码所示,通过简单常规的编码即可实现对某个类的某个方法进行切面,不需要对底层技术有了解即可上手。上面的模块被 JVM SandBox 加载和初始化之后便可以被使用了。比如,只需要告诉 JVM SandBox 我们要执行 my-sandbox-module 这个模块的 addLog 这个方法,我们编写的功能的调用就会被注入到目标地方。

我们知道Java对象的行为(函数,方法)是存储在方法区的,从下图可以看到,方法区的数据是由类加载器把编译好的class文件加载到jvm方法区的。所以我们可以得出简单思路是:
1. 在对应类Java代码中新增日志代码,并重新编译得到新的class文件。
2. 让jvm重新加载这个类的class文件到方法区

第一步倒是挺好实现,但是第二步,如何让jvm加载一个已经加载过的类?
答案是“java.lang.instrument.Instrumentation”

instrument 是 JVM 提供的一个可以修改已加载类的类库,专门为 Java 语言编 写的插桩服务提供支持。Instrumentation中有两个方法都可以实现重新替换已经存在的class文件,它们是:redefineClasses 和 retransformClasses。区别是redefineClasses 是自己提供字节码文件替换 掉已存在的 class 文件,retransformClasses 是在已存在的字节码文件上修改后再替换之。它需要依赖 JVMTI的 Attach API 机制实现。JVM TI(JVM TOOL INTERFACE,JVM 工具接口)是 JVM 提供的一套对 JVM 进行操作的工具接口。通过JVMTI,可以实现对 JVM 的多种操作,它通过接口注册 各种事件勾子,在 JVM 事件触发时,同时触发预定义的勾子,以实现对各个 JVM 事件的响应,事件包括类文件加载、异常产生与捕获、线程启动和结束、进入和退 出临界区、成员变量修改、GC开始和结束、方法调用进入和退出、临界区竞争与等 待、VM 启动与退出等等。Instrument 就是一个基于 JVMTI 接口的,以代理方式连接和访问 JVM 的一个 Agent。

JVM SandBox 容器的启动依赖 Java Agent,Java Agent(Java 代理)是 JDK 1.5 之后引入的技术。Agent 就是 JVMTI 的一种实现,Agent 有两种启动方式,一是随 Java 进 程启动而启动;二是运行时载入,通过 attach API,将模块(jar 包)动态地 Attach 到指定进程 id 的 Java 进程内。开发一个 Java Agent 有两种方式,一种是实现一个 premain 方法,但是这种方式实现的 Java Agent 只能在 JVM 启动的时候被加载;另一种是实现一个 agentmain 方法,这种方式实现的 Java Agent 可以在 JVM 启动之后被加载。JVM SandBox Agent 对于这两种方式都有实现,用户可以自行选择使用,因为在 JVM 层这两种方式底层的实现原理大同小异,下面先通过两行代码,来看看基于 agentmain 方式实现的 Java Agent 是如何被加载的:

VirtualMachine vmObj = VirtualMachine.attach(targetJvmPid);//targetJvmPid 为目标 JVM 的进程 ID

vmObj.loadAgent(agentJarPath, cfg);  // agentJarPath 为 agent jar 包的路径,cfg 为传递给 agent 的参数

1.3可插拔

本文理解的 JVM SandBox 可插拔至少有两层含义:一层是 JVM 沙箱本身是可以被插拔的,可被动态地挂载到指定 JVM 进程上和可以被动态地卸载;另一层是 JVM 沙箱内部的模块是可以被插拔的,在沙箱启动期间,被加载的模块可以被动态地启用和卸载。
一个典型的沙箱使用流程如下:

$./sandbox.sh -p 33342 #将沙箱挂载到进程号为 33342 的 JVM 进程上

$./sandbox.sh -p 33342 -d 'my-sandbox-module/addLog' #运行指定模块, 模块功能生效

$./sandbox.sh -p 33342 -S #卸载沙箱

JVM 沙箱可以被动态地挂载到某个正在运行的目标 JVM 进程之上(前提是目标 JVM 没有禁止 attach 功能),沙箱工作完之后还可以被动态地从目标 JVM 进程卸载掉,沙箱被卸载之后,沙箱对对目标 JVM 进程产生的影响会随即消失(这是沙箱的一个重要特性),沙箱工作示意图如下:

图 4-1 沙箱工作示意图

客户端通过 Attach 将沙箱挂载到目标 JVM 进程上,沙箱的启动实际上是依赖 Java Agent,上文已经介绍过,启动之后沙箱会一直维护着 Instrument 对象引用,在沙箱中 Instrument 对象是一个非常重要的角色,它是沙箱访问和操作 JVM 的唯一通道,后续修改字节码和重定义类都要经过 Instrument。另外,沙箱启动之后同时会启动一个内部的 Jetty 服务器,这个服务器用于外部进程和沙箱进行通信,上面看到的./sandbox.sh -p 33342 -d ‘my-sandbox-module/addLog’ 这行代码,实际上就是通过 HTTP 协议来告诉沙箱执行 my-sanbox-module 这个模块的 addLog 这个功能的。

1.4

sandbox的代码主要分为几个过程:启动、模块加载、类增强实现

启动

上面我们提到,使用Instrumentation进行字节码增强有2种模式(attach模式和java-agent模式),sandbox-jvm的启动有这2种方式,入口都在AgentLauncher中,分别对应着agentmain和premain,它们都调用了install方法,以agentmain为例

public static void agentmain(String featureString, Instrumentation inst) {LAUNCH_MODE = LAUNCH_MODE_ATTACH;final Map<String, String> featureMap = toFeatureMap(featureString);writeAttachResult(getNamespace(featureMap),getToken(featureMap),install(featureMap, inst));}

install函数的作用是在目标jvm上安装sandbox,创建独立的classloader,通过classloader加载JettyCoreServer.class,并且反射生成实例,建立httpserver监听请求

// 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;}
}

启动jetty server,监听http请求,并且调用coreModuleManager.reset进行模块的加载,在下面一节介绍。

public synchronized void bind(final CoreConfigure cfg, final Instrumentation inst) throws IOException {this.cfg = cfg;try {initializer.initProcess(()->{logger.info("initializing server. cfg={}", cfg);jvmSandbox = new JvmSandbox(cfg, inst);initHttpServer();initJettyContextHandler();httpServer.start();}});// 初始化加载所有的模块try {jvmSandbox.getCoreModuleManager().reset();} catch (Throwable cause) {logger.warn("reset occur error when initializing.", cause);}final InetSocketAddress local = getLocal();logger.info("initialized server. actual bind to {}:{}",local.getHostName(),local.getPort());} catch (Throwable cause) {// 对外抛出到目标应用中throw new IOException("server bind failed.", cause);}}

模块加载

模块是什么?sandbox将不同的业务进行模块划分,不同的模块使用不同的classloader进行加载,例如如果我们想实现流量录制,我们可以自定义一个模块通过字节码增强实现流量入口的监听并进行录制,这就是我们后面会介绍的repeater。
先来看下CoreModuleManager.reset() 的工作:
加载过程是先卸载再加载,首先根据cfg(配置存储对象)中的的module包路径配置得到moduleLibDirArray(需要加载的模块路径:系统模块+用户模块), 每个模块独立加载。

for (final File moduleLibDir : moduleLibDirArray) {// 用户模块加载目录,加载用户模块目录下的所有模块// 对模块访问权限进行校验if (moduleLibDir.exists() && moduleLibDir.canRead()) {new ModuleLibLoader(moduleLibDir, cfg.getLaunchMode()).load(new InnerModuleJarLoadCallback(), new InnerModuleLoadCallback());}
}

通过阅读sandbox.sh,主要功能分为两个:

  1. attach目标JVM
  2. 发送HTTP请求,控制jvm-sandbox的运行

由于我需要在远程控制JVM-Sandbox,,而attach必须在目标主机上运行,所以我主要关注如何发送HTTP请求。

HTTP请求的格式如下:

http://${host}:${port}/sandbox/${namespace}/module/http/${module-name}/${command-name}?V1=K1&...

${host} 为JVM-Sandbox运行的节点

${port} 为JVM-Sadnbox监听的端口

${namespace} 默认为default,用于区分不同的JVM-Sandbox

${module-name} 是实现Module时,@Information注解的值

${command-name} 是定义Module方法是,@Command注解的值

利用上述URL,我们就可以远程操控JVM-Sandbox了。

参考

https://blog.csdn.net/weixin_37512224/article/details/108226345

JVM SandBox简要介绍相关推荐

  1. JVM SandBox 的技术原理与应用分析

    原文:https://www.infoq.cn/article/TSY4lGjvSfwEuXEBW*Gp 一.前言 在开始之前,我们先来模拟一下以下的场景: 小李:"小明,你的接口没有返回数 ...

  2. AOP—JVM SandBox—快速上手

    原文作者:stingfire 原文地址:深入学习jvm-sandbox(安装&快速上手) 目录 一.安装 1. 下载 2. 运行安装脚本 3. 阅读安装脚本 3.1 首先是定义安装目录变量 3 ...

  3. AOP—JVM SandBox—底层原理解析

    原文作者:陆晨 原文地址:JVM SandBox 的技术原理与应用分析 目录 一.前言 二.JVM SandBox 简介 2.1 AOP 2.2 JVM SandBox 三.JVM 核心技术 3.1 ...

  4. Jvm sandbox mock机制实践

    一.背景 Jvm sandbox沙箱机制,是一种实现不重启.无侵入改变目标应用返回值的面向切面编程解决方案.测试方面来说,对于RPC接口.HTTP接口都适用.如果需要开发一个比较全面的mock平台,不 ...

  5. 阿里JVM SANDBOX原理

    一.前言 在开始之前,我们先来模拟一下以下的场景: 小李:"小明,你的接口没有返回数据,麻烦帮忙看一下?" 小明:"我这边的数据也是从别人的服务器中拿到的,但是我不确定是 ...

  6. Thymeleaf简要介绍

    Thymeleaf简要介绍 1 Thymeleaf是什么 2 模板模式 3 方言:标准方言 4. URL 5.表达式基本对象 6. Thymeleaf中的特殊字符转义 (th:utext与th:tex ...

  7. Chromium插件(Plugin)机制简要介绍和学习计划

    在Chromium中,除了可以使用Extension增强浏览器功能,还可以使用Plugin.两者最大区别是前者用JS开发,后者用C/C++开发.这意味着Plugin以Native Code运行,在性能 ...

  8. Hadoop学习笔记一 简要介绍

    Hadoop学习笔记一 简要介绍 这里先大致介绍一下Hadoop.     本文大部分内容都是从官网Hadoop上来的.其中有一篇介绍HDFS的pdf文档,里面对Hadoop介绍的比较全面了.我的这一 ...

  9. 数据结构的简要介绍:图形如何工作

    by Michael Olorunnisola 通过Michael Olorunnisola 数据结构的简要介绍:图形如何工作 (A Gentle Introduction to Data Struc ...

最新文章

  1. shell获取时间戳
  2. 为什么Kafka中的分区数只能增加不能减少?
  3. 服务器2008操作系统漏洞,【操作系统安全漏洞 】解决CVE-2017-11780:Microsoft Windows SMB Server远程代码执行漏洞...
  4. php封装webservice_PHP实现WebService的简单示例和实现步骤
  5. cfiledialog指定位置和大小_GDamp;T | 位置度公差的理解过程
  6. 【基础操作】线性基详解
  7. java获取jtable的路径,Java如何在JTable组件中获取选定的单元格?
  8. 切记!构造函数里面别一定不要初始化其他类,踩过坑的都知道
  9. leetcode617. 合并二叉树(dfs)
  10. Nginx相关 解决nginx反向代理后页面上的js/css文件无法加载
  11. qt中生成并读取配置文件Ini
  12. HDU4857 逃生【拓扑排序】
  13. ibatis.net:尽可能的使用匿名类型替换 Hashtable
  14. 2021-09-02 网安实验-文件修复-CTF中的压缩包
  15. uint8_t范围_uint8_t / uint16_t / uint32_t /uint64_t数据类型详解
  16. javaSE之多线程vip插队
  17. Higgs全球区块链投融资交流会(香港站)成功举办,路演项目备受瞩目
  18. springboot+VUE整合websocket
  19. BIOS设置光盘启动(上.Award bios)
  20. 克鲁斯卡尔(Kruskal)算法

热门文章

  1. 大专程序员毕业五年税后18K,想进BAT,网友:吃shi都赶不上热的
  2. BaseRepository接口
  3. 大数据技术应用 第1章Oracle11g简介
  4. 1.Excel vba开发-处理空格数据
  5. markdown入门3--数学符号希腊字母等
  6. 架设属于自己的TeamSpeak2 Server服务器(转)
  7. 看看,创专利、买专利、护专利可享哪些优惠
  8. WINDOW -- 华硕升级主板BIOS版本
  9. 从零开始前端学习[17]:overflow超出是否隐藏的使用方式
  10. cocos2dx中精灵点击事件处理的两种方式——Sprite和ImageView