原文作者:陆晨
目录
一、前言
在开始之前,我们先来模拟一下以下的场景:
- 小李:“小明,你的接口没有返回数据,麻烦帮忙看一下?”
- 小明:“我这边的数据也是从别人的服务器中拿到的,但是我不确定是因为逻辑处理有问题导致没有结果,还是因为我依赖的服务有问题而没有返回结果,我需要确认一下。”
- 小明:“哎呀,线上没有日志,我需要加个日志上个线。”
- 30 分钟之后……
- 小明:“不好意思,日志加错地方了……稍等……”
接来下隆重登场的就是本文的主角 JVM SandBox 了。基于 JVM SandBox,我们可以很容易地做到在不重新部署应用的情况下,给指定的某些类的某些方法加上日志功能。当然,动态加日志仅仅是 JVM SandBox 可以应用的一个小小的场景,JVM SandBox 的威力远不在于此。那么,JVM SandBox 是什么?JVM SandBox 从哪里来?JVM SandBox 怎么用?本文在第二章会回答这几个问题,如果你跟我一样对 JVM SandBox 的底层实现原理感兴趣,特别是 JVM 相关部分,那么第三章有相关的内容;如果你只想了解 JVM SandBox 自身具有哪些特性,以及 JVM SandBox 是如何设计实现的,那么可以跳过第三章,直接阅读第四章;最后,在第五章会简单地介绍其他两个可以应用 JVM SandBox 的场景。
二、JVM SandBox 简介
2.1 AOP
在介绍 JVM SandBox 之前,我们先来回顾一下 AOP 技术。AOP(面向切面编程,Aspect Oriented Programming)技术已被业界广泛应用,其思想是面向业务处理过程的某个步骤或阶段进行编程,这个步骤或阶段被称为切面,其目的是降低业务逻辑的各部分之间的耦合,常见的 AOP 实现基本原理有两种:代理和行为注入。
1)代理模式
在代理模式下,我们会创建一个代理对象来代理原对象的行为,代理对象拥有原对象行为执行的控制权,在这种模式下,我们基于代理对象在原对象行为执行的前后插入代码来实现 AOP。
![](https://i-blog.csdnimg.cn/blog_migrate/57795cfc866f514131416d47885fd550.png)
2)行为注入模式
在行为注入模式下,我们不会创建一个新的对象,而是修改原对象,在原对象行为的执行前后注入代码来实现 AOP。
![](https://i-blog.csdnimg.cn/blog_migrate/42240f2944aaade6ecd68a74951cbd0f.png)
2.2 JVM SandBox
JVM SandBox 是阿里开源的一款 基于JVM 平台非侵入式运行期 AOP 解决方案,本质上是一种 AOP 落地形式。那么可能有同学会问:已有成熟的 Spring AOP 解决方案,阿里巴巴为什么还要“重复造轮子”?这个问题要回到 JVM SandBox 诞生的背景中来回答。在 2016 年中,天猫双十一催动了阿里巴巴内部大量业务系统的改动,恰逢徐冬晨(阿里巴巴测试开发专家)所在的团队调整,测试资源保障严重不足,迫使他们必须考虑更精准、更便捷的老业务测试回归验证方案。开发团队面临的是新接手的老系统,老的业务代码架构难以满足可测性的要求,很多现有测试框架也无法应用到老的业务系统架构中,于是需要新的测试思路和测试框架。
为什么不采用 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 这个方法,我们编写的功能的调用就会被注入到目标地方。
JVM SandBox 使用起来非常很简单,但是 JVM SandBox 背后所涉及到的底层技术原理、实现细节却不简单,比如 Java Agent、Attach、JVMTI、Instrument、Class 字节码修改、ClassLoader、代码锁、事件驱动设计等等。如果要深究可能要究几本书,但这不是本文的目的。本文仅仅概括性地介绍 JVM SandBox 实现涉及到的一些核心技术点,力求通过本文可以回答如 JVMTI 是什么?Instrument 是什么?Java Agent 是什么?它们之间有什么关系?他们和 JVM SandBox 又是什么关系等问题。
三、JVM 核心技术
3.1 Java Agent
JVM SandBox 容器的启动依赖 Java Agent,Java Agent(Java 代理)是 JDK 1.5 之后引入的技术。开发一个 Java Agent 有两种方式,一种是实现一个 premain 方法,但是这种方式实现的 Java Agent 只能在 JVM 启动的时候被加载;另一种是实现一个 agentmain 方法,这种方式实现的 Java Agent 可以在 JVM 启动之后被加载。当然,两种实现方法各有利弊、各有适用场景,这里不再过多介绍,JVM SandBox Agent 对于这两种方式都有实现,用户可以自行选择使用,因为在 JVM 层这两种方式底层的实现原理大同小异,因此本文只选择 agentmain 方式进行介绍,下文的脉络也仅跟 agentmain 方式相关。下面先通过两行代码,来看看基于 agentmain 方式实现的 Java Agent 是如何被加载的