一. 概述

在日常开发中,经常会接触到面向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)

JVM Sandbox入门详解的更多相关文章

  1. Linq之旅:Linq入门详解(Linq to Objects)

    示例代码下载:Linq之旅:Linq入门详解(Linq to Objects) 本博文详细介绍 .NET 3.5 中引入的重要功能:Language Integrated Query(LINQ,语言集 ...

  2. SQL注入攻防入门详解

    =============安全性篇目录============== 本文转载 毕业开始从事winfrm到今年转到 web ,在码农届已经足足混了快接近3年了,但是对安全方面的知识依旧薄弱,事实上是没机 ...

  3. SQL注入攻防入门详解(2)

    SQL注入攻防入门详解 =============安全性篇目录============== 毕业开始从事winfrm到今年转到 web ,在码农届已经足足混了快接近3年了,但是对安全方面的知识依旧薄弱 ...

  4. Quartz 入门详解

    Quartz是OpenSymphony开源组织在Job scheduling领域又一个开源项目,它可以与J2EE与J2SE应用程序相结合也可以单独使用.Quartz可以用来创建简单或为运行十个,百个, ...

  5. Redis快速入门详解

    Redis入门详解 Redis简介 Redis安装 Redis配置 Redis数据类型 Redis功能 持久化 主从复制 事务支持 发布订阅 管道 虚拟内存 Redis性能 Redis部署 Redis ...

  6. [转]SQL注入攻防入门详解

    原文地址:http://www.cnblogs.com/heyuquan/archive/2012/10/31/2748577.html =============安全性篇目录============ ...

  7. [置顶] xamarin android toolbar(踩坑完全入门详解)

    网上关于toolbar的教程有很多,很多新手,在使用toolbar的时候踩坑实在太多了,不好好总结一下,实在浪费.如果你想学习toolbar,你肯定会去去搜索androd toolbar,既然你能看到 ...

  8. 转:JAVAWEB开发之权限管理(二)——shiro入门详解以及使用方法、shiro认证与shiro授权

    原文地址:JAVAWEB开发之权限管理(二)——shiro入门详解以及使用方法.shiro认证与shiro授权 以下是部分内容,具体见原文. shiro介绍 什么是shiro shiro是Apache ...

  9. webpack入门详解

    webpack入门详解(基于webpack 3.5.4  2017-8-22) webpack常用命令: webpack --display-error-details    //执行打包 webpa ...

  10. Scala 入门详解

    Scala 入门详解 基本语法 Scala 与 Java 的最大区别是:Scala 语句末尾的分号 ; 是可选的 Scala 程序是对象的集合,通过调用彼此的方法来实现消息传递.类,对象,方法,实例变 ...

随机推荐

  1. openGauss每日一练第四天

    openGauss 每日一练第四天 本文出处:https://www.modb.pro/db/193083 学习地址 https://www.modb.pro/course/133 学习目标 学习 o ...

  2. Luogu P3007 奶牛议会

    观前须知 本题解使用 CC BY-NC-SA 4.0 许可. 同步发布于 Luogu 题解区. 更好的观看体验 请点这里. 笔者的博客主页 正文 Luogu P3007 [USACO11JAN] Th ...

  3. nginx重新整理——————编译nginx[二]

    前言 简单编译一下nginx. 正文 为什么我们要去编译nginx. 系统安装,比如yum安装,会把nginx 模块直接编译进来. 这意味着,我们无法使用第三方的包.如果我们需要使用第三方包,那么需要 ...

  4. This beta version of Typora is expired, please download and install a newer version. 实测最简单有效的方案

    This beta version of Typora is expired, please download and install a newer version. 实测最简单有效的方案 一.问题 ...

  5. 国内首家!百度智能云宣布支持Llama3全系列训练推理

    继18日Llama3的8B.70B大模型发布后,百度智能云千帆大模型平台19日宣布在国内首家推出针对Llama3全系列版本的训练推理方案,便于开发者进行再训练,搭建专属大模型,现已开放邀约测试. 目前 ...

  6. 力扣1045(MySQL)-买下所有产品的客户(中等)

    题目: Customer 表: Product 表: 写一条 SQL 查询语句,从 Customer 表中查询购买了 Product 表中所有产品的客户的 id. 示例:  解题思路: 建表语句: 1 ...

  7. 可观测告警运维系统调研——SLS告警与多款方案对比

    简介: 本文介绍对比多款告警监控运维平台方案,覆盖阿里云SLS.Azure.AWS.自建系统(ELK.Prometheus.TICK)等方案. 前言 本篇是SLS新版告警系列宣传与培训的第三篇,后续我 ...

  8. Dubbo 和 HSF 在阿里巴巴的实践:携手走向下一代云原生微服务

    ​简介: HSF 和 Dubbo 的融合是大势所趋.为了能更好的服务内外用户,也为了两个框架更好发展,Dubbo 3.0 和以 Dubbo 3.0 为内核适配集团内基础架构生态的 HSF 3 应运而生 ...

  9. [Docker] 假如宿主机 Nginx 代理到 Docker 的 PHP

    其实没有多少区别,同样 php 镜像启动服务暴露一个端口,nginx 的 proxy_pass 代理过去,唯一要注意的是 nginx 配置的项目路径. nginx 配置的 root 是本地项目路径,给 ...

  10. [ML] 机器学习简介

    监督学习(Supervised Learning) 添加标签,手把手训练. 比如线性回归算法. 半监督学习(Semi-supervised Learning) 非监督学习(Unsupervised L ...