JVM Sandbox入门详解

news2025/4/3 15:06:52

一. 概述

在日常开发中,经常会接触到面向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框架实现方案中,有静态编织和动态编织两种。

  1. 静态编织:静态编织发生在字节码生成时根据一定框架的规则提前将AOP字节码插入到目标类和方法中,实现AOP;

  2. 动态编织

    :动态编织则允许在JVM运行过程中完成指定方法的AOP字节码增强.常见的动态编织方案大多采用重命名原有方法,再新建一个同签名的方法来做代理的工作模式来完成AOP的功能(常见的实现方案如CgLib),但这种方式存在一些应用边界:

    • 侵入性:对被代理的目标类需要进行侵入式改造。比如:在Spring中必须是托管于Spring容器中的Bean
    • 固化性:目标代理方法在启动之后即固化,无法重新对一个已有方法进行AOP增强

要解决无侵入的特性需要AOP框架具备 在运行时完成目标方法的增强和替换。在JDK的规范中运行期重定义一个类必须准循以下原则

  1. 不允许新增、修改和删除成员变量
  2. 不允许新增和删除方法
  3. 不允许修改方法签名

JVM-SANDBOX属于基于Instrumentation的动态编织类的AOP框架,通过精心构造了字节码增强逻辑,使得沙箱的模块能在不违反JDK约束情况下实现对目标应用方法的无侵入运行时AOP拦截

从上图中,可以看到一个方法的整个执行周期都被代码“加强”了,能够带来的好处就是你在使用JVM SandBox只需要对于方法的事件进行处理。

// BEFORE
try {

   /*
    * do something...
    */

    // RETURN
    return;

} catch (Throwable cause) {
    // THROWS
}

在沙箱的世界观中,任何一个Java方法的调用都可以分解为BEFORERETURNTHROWS三个环节,由此在三个环节上引申出对应环节的事件探测和流程控制机制。

基于BEFORERETURNTHROWS三个环节事件分离,沙箱的模块可以完成很多类AOP的操作。

  1. 可以感知和改变方法调用的入参
  2. 可以感知和改变方法调用返回值和抛出的异常
  3. 可以改变方法执行的流程
    • 在方法体执行之前直接返回自定义结果对象,原有方法代码将不会被执行
    • 在方法体返回之前重新构造新的结果对象,甚至可以改变为抛出异常
    • 在方法体抛出异常之后重新抛出新的异常,甚至可以改变为正常返回

三. 代码实践

我们还是以 《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入门教程与原理浅谈 - 蛮三刀酱 - 博客园 (cnblogs.com)

Home · alibaba/jvm-sandbox Wiki (github.com)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/577265.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

http请求和响应(包含状态码)+过滤器

目录 一、http协议概述 二、http请求 三、http响应 四、过滤器 一、http协议概述 1.http&#xff1a;超文本传输协议&#xff0c;是用于在网络上传输数据的应用层协议。是互联网上应用最为流行的一种网络协议,用于定义客户端浏览器和服务器之间交换数据的过程&#xff0c;基…

软考A计划-试题模拟含答案解析-卷二

点击跳转专栏>Unity3D特效百例点击跳转专栏>案例项目实战源码点击跳转专栏>游戏脚本-辅助自动化点击跳转专栏>Android控件全解手册点击跳转专栏>Scratch编程案例 &#x1f449;关于作者 专注于Android/Unity和各种游戏开发技巧&#xff0c;以及各种资源分享&am…

当我按下开关,震惊了一众答辩老师,乍一看,啊,就这?

基于机器视觉的爬行机器人&#xff08;毕业设计&#xff09; 零、实现功能一、关于本想法二、理论分析三、3D结构设计四、硬件设计主控板驱动板 五、软件设计控制程序机器视觉APP设计 六、结束语 零、实现功能 实现了爬行机器人的移动控制功能。采用三角步态控制机器人移动&am…

【技术分享】万字长文图文并茂读懂高性能无锁 “B-Tree 改”:Bw-Tree

【技术分享】万字长文图文并茂读懂高性能无锁 “B-Tree 改”&#xff1a;Bw-Tree 原文链接&#xff1a; https://mp.weixin.qq.com/s/I5TphQP__tHn6JoPcP–_w 参考文献可能需要科学上网才能下载。如果你获取不到这几篇论文&#xff0c;可以关注公众号 IT技术小密圈 回复 bw-tre…

类和对象初阶

目录 一、再谈构造函数 1.1 构造函数体赋值 1.2 初始化列表 1.3 注意 1.4 总结 二、拷贝对象时的一些编译器优化 三、static成员 3.1 静态成员变量 3.1.1 引入 3.1.2 特点 3.1.3 区别 3.2 静态成员函数 3.2.1 引入 3.2.2 特点 3.2.3 例题 四、友元 4.1 友元函…

数据结构与算法·第2章【线性表】

线性结构具有以下基本特征&#xff1a; 有唯一的一个被称为首元素&#xff08;或头元素&#xff09;的元素&#xff0c;没有直接前驱&#xff1b;有唯一的一个被称为尾元素&#xff08;或尾节点&#xff09;的元素&#xff0c;没有直接后继。 数据元素之间存在一对一的线性关…

python 实现单链表

链表 链表是一种在存储单元上非连续、非顺序的存储结构。数据元素的逻辑顺序是通过链表中的指针链接次序实现。 链表是由一系列的结点组成&#xff0c;结点可以在运行时动态生成。每个结点包含两部分&#xff1a;数据域与指针域。数据域存储数据元素&#xff0c;指针域存储下一…

Yapi内网部署[CentOS7]

mongo安装 # 下载MongoDB https://www.mongodb.com/try/download/community4.2.24 RedHat/CentOS7.0 tgz# 安装MongoDB mkdir -p /home/jpge/devp-tools/tools cd /home/jpge/devp-tools/tools wget https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-rhel70-4.2.24.tgz…

线性代数:线性方程求解、矩阵的逆、线性组合、线性独立

本文参考www.deeplearningbook.org一书第二章2.3 Identity and Inverse Matrices 2.4 Linear Dependence and Span 本文围绕线性方程求解依次介绍矩阵的逆、线性组合、线性独立等线性代数的基础知识点。 一、线性方程 本文主要围绕求解线性方程展开&#xff0c;我们先把线性…

揭秘Redis持久化原理,探索fork与Copy-on-Write的魔法!

大家好&#xff0c;我是小米&#xff0c;今天我将和大家一起探索Redis持久化原理中的两个关键概念&#xff1a;fork和Copy-on-Write。这两个概念对于理解Redis的数据持久化机制至关重要。让我们一起来揭开这些技术的神秘面纱吧&#xff01; Redis持久化简介 在开始之前&#…

组合数学第四讲

Generating Function&#xff08;生成函数&#xff09; 这里是一个普通生成函数例子&#xff0c;生成函数一般适用于根据递推关系式求出比较复杂的通项公式的 关键点&#xff1a; 1.项可转换成G(x)-,因为生成函数规定是从0到∞的 2.,当|x|<1时&#xff0c;最终可收敛为。这里…

基础算法(六):回溯算法

前言 Hello大家好&#xff0c;停了半个多月算法学习的荔枝又变菜了&#xff0c;最近决定认认真真地重新学习回溯&#xff0c;无意间看到Carl哥的代码随想录&#xff0c;感动之余也是跟着一步步走&#xff0c;后悔上车晚了呜呜呜~~~。之前自己摸索确实有点难受&#xff0c;在这篇…

YUM安装LNMP架构配置命令与搭建论坛

LNMP架构配置命令与搭建论坛 LNMP简介安装须知安装 Nginx配置yum源yum安装nginx并查看版本号开启服务并且设为开机自启 安装 MySQL 5.7 (mariadb)移除软件包下载安装mysql安装MySQL报错问题解决方案&#xff1a; 开启服务并设为开机自启在日志文件中找出root用户的初始密码登录…

数据结构 --- 树

1、二叉树 树是一种非线性的数据结构&#xff0c;它是由n&#xff08;n>0&#xff09;个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树&#xff0c;也就是说它是根朝上&#xff0c;而叶朝下的。 每个结点最多有两棵子树&#xff0c;即二叉…

CAPL(vTESTStudio) - CAPL控制RS232继电器

目录 为什么要使用CAPL控制继电器? 一、RS232继电器选择 二、继电器通信协议

AList 一个支持多种存储的文件列表程序,使用 Gin 和 Solidjs。

一个支持多种存储&#xff0c;支持网页浏览和 WebDAV 的文件列表程序&#xff0c;由 gin 和 Solidjs 驱动。 特点 使用简单 AList 从一开始就设计为易于安装&#xff0c;并且可以在所有平台上使用。 多种存储 AList 支持多个存储提供商&#xff0c;包括本地存储、阿里云盘、O…

大数据治理入门系列:数据治理

在信息经济时代&#xff0c;数据是企业的一大关键资产。为了制定科学、有效、合理的决策&#xff0c;企业需要收集大量的数据并进行各种数据分析&#xff0c;为决策提供依据。在此过程中&#xff0c;收集数据的速度、数据的质量和可靠性、对数据的分析过程、合适的分析工具等&a…

三十四、数学知识——约数(试除法 + 约数个数 + 约数之和 + 欧几里得算法)

约数相关算法主要内容 一、基本思路1、定义2、试除法——求一个数的所有约数3、约数个数4、约数之和5、欧几里得算法——辗转相除法&#xff08;求解最大公约数&#xff09; 二、Java、C语言模板实现三、例题题解 一、基本思路 1、定义 约数&#xff1a; 约数是指一个数&…

利用百度API进行植物识别

植物识别_拍照识别植物-百度AI开放平台百度AI植物识别,支持识别超过2万种通用植物和近8千种花卉&#xff0c;接口返回植物的名称&#xff0c;并获取百科信息&#xff0c;适用于拍照识图类APP中https://ai.baidu.com/tech/imagerecognition/plant 偶然看到的&#xff0c;不过真…