当前位置:网站首页>透过JVM-SANDBOX源码,了解字节码增强技术原理
透过JVM-SANDBOX源码,了解字节码增强技术原理
2022-07-04 03:50:00 【InfoQ】
介绍
架构设计

- 模块控制管理:负责管理 sandbox 自身模块以及使用者自定义模块,例如模块的加载,激活,冻结,卸载
- 事件监听处理:用户自定义模块实现 Event 接口对增强的事件进行自定义处理,等待事件分发处理器的触发。
- 沙箱事件分发处理器:对目标方法增强后会对目标方法追加三个环节,分别为方法调用前
BEFORE
、调用后RETURN
、调用抛异常THROWS
、当代码执行到这三个环节时则会由分发器分配到对应的事件监听执行器中执行。
- 代码编织框架:通过 ASM 框架依托于 JVMTI 对目标方法进行字节码修改,从而完成代码增强的能力。
- 检索过滤器:当用户对目标方法创建增强事件时,沙箱会对目标 jvm 中的目标类和方法进行匹配以及过滤。匹配到用户设定目标类和方法进行增强,过滤掉 sandbox 内部的类以及 jvm 认为不可修改的类。
- 加载类检索: 获取到需要增强的类集合依赖检索过滤器模块
- HTTP 服务器:通过 http 协议与客户端进行通信(sandbox.sh 即可理解为客户端)本质是通过 curl 命令下发指令到沙箱的 http 服务器。例如模块的加载,激活,冻结,卸载等指令
相关技术
JVM TI
Instrumentation
- 在启动脚本中增加-javaagent 参数这种方式会伴随着 JVM 一起启动,agent 中需要提供大名鼎鼎的 premain 方法,顾名思义 premain 是在 main 方法运行前执行的,然后才会去运行主程序的 main 方法,这样就要求开发者在应用启动前就必须确认代理的处理逻辑和参数内容等等。这种挂在 agent 方式的好处是如果 agent 启动需要加载大量的类,随着 jvm 启动时直接加载不会导致 JVM 在运行时卡顿或者 CPU 抖动,缺点是不够灵活。
- 利用 Attach API 在 JVM 运行时不需要重启的情况下即可完成挂载,agent 需要提供 agentmain 方法,即插即用的模式非常灵活,Attach API 提供了一种附加到 Java 虚拟机的机制,使用此 API 附加到目标虚拟机并将其工具代理加载到该虚拟机中,本质上就是提供了和目标 jvm 通讯的能力。例如我们常常使用的 jastck,jmap 等命令都是利用 attach api 先与目标 jvm 建立通讯再执行命令。但如果 agent 启动需要加载大量的类可能会导致目标 jvm 出现卡顿,cpu 抖动等情况
- addTransformer:注册字节码转换器,当注册一个字节码转换器后,所有的类加载都会经过字节码转换器进行处理。
- retransformClasses 重新对 JVM 已加载的类进行字节码转换
- removeTransformer 删除已注册的字节码转换器,删除后新加载的类不会再经过字节码转换器处理,但是已经“增强”过的类还是会继续保留
ClassFileTransformer
byte[] transform( ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer)
throws IllegalClassFormatException;
- 新加载类的时候,例如 ClassLoader.defineClass
- 重新定义类的时候,例如 Instrumentation.redefineClasses
- 对类重新转换的时候,例如 Instrumentation.retransformClasses
字节码生成
- ClassReader:此类主要功能就是读取字节码文件,然后把读取的数据通知 ClassVisitor;
- ClassVisitor:用于生成和转换编译类的 ASM API 基于 ClassVisitor 抽象类,接收 ClassReader 发出的对 method 的访问请求,并且替换为另一个自定义的 MethodVisitor
- ClassWriter:其继承于 ClassVisitor,主要用来生成类;

ClassLoader
加载器种类
- 启动类加载器(Bootstrap Class Loader):这个类加载器负责加载存放在 <JAVA_HOME>\lib 目录,或者被-Xbootclasspath 参数所指定的路径中存放的,而且是 Java 虚拟机能够 识别的(按照文件名识别,如 rt .jar、t ools.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类 库加载到虚拟机的内存中
- 扩展类加载器(Extension Class Loader):这个类加载器是在类 sun.misc.Launcher$ExtClassLoader 中以 Java 代码的形式实现的。它负责加载<JAVA_HOM E>\lib\ext 目录中,或者被 java.ext.dirs 系统变量所 指定的路径中所有的类库。
- 应用程序类加载器(Application Class Loader):这个类加载器由 sun.misc.Launcher$App ClassLoader 来实现。由于应用程序类加载器是 ClassLoader 类中的 getSystem- ClassLoader()方法的返回值,所以有些场合中也称它为“系统类加载器”。它负责加载用户类路径 (ClassPath)上所有的类库,开发者同样可以直接在代码中使用这个类加载器。如果应用程序中没有 自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
- 自定义类加载器:顾名思义是由开发者自定义的类加载器
双亲委派模型

自定义类加载器
沙箱类隔离策略

SPI
启动过程
挂载 Agent
- -javaagent 启动脚本中直接挂载
- 利用 attach api 在运行时挂载
function attach_jvm() {
# attach target jvm
"${SANDBOX_JAVA_HOME}/bin/java" \
${SANDBOX_JVM_OPS} \
-jar "${SANDBOX_LIB_DIR}/sandbox-core.jar" \
"${TARGET_JVM_PID}" \
"${SANDBOX_LIB_DIR}/sandbox-agent.jar" \
"home=${SANDBOX_HOME_DIR};token=${token};server.ip=${TARGET_SERVER_IP};server.port=${TARGET_SERVER_PORT};namespace=${TARGET_NAMESPACE}" ||
exit_on_err 1 "attach JVM ${TARGET_JVM_PID} fail."
}
<manifest>
<mainClass>com.alibaba.jvm.sandbox.core.CoreLauncher</mainClass>
</manifest>
private void attachAgent(final String targetJvmPid,
final String agentJarPath,
final String cfg) throws Exception {
vmObj = VirtualMachine.attach(targetJvmPid);
if (vmObj != null) {
vmObj.loadAgent(agentJarPath, cfg);
}
}
加载 Agent
<manifestEntries>
<Premain-Class>com.alibaba.jvm.sandbox.agent.AgentLauncher</Premain-Class>
<Agent-Class>com.alibaba.jvm.sandbox.agent.AgentLauncher</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
- String featureString 是脚本中的执行脚本执行 attach 时传递过来的参数 如 sandbox 路径地址,token,namspace(租户)等
- Instrumentation 是 JVM 提供的可以在运行时动态修改已加载类的基础库,获取 Instrumentation 实例只能通过 premain 或者 agentmian 方法参数中获取。
//启动加载 -javaagent
public static void premain(String featureString, Instrumentation inst) {
LAUNCH_MODE = LAUNCH_MODE_AGENT;
install(toFeatureMap(featureString), inst);
}
//动态加载 attach api
public static void agentmain(String featureString, Instrumentation inst) {
LAUNCH_MODE = LAUNCH_MODE_ATTACH;
final Map<String, String> featureMap = toFeatureMap(featureString); //解析惨参数
writeAttachResult(
getNamespace(featureMap), //获取租户所属 namespace
getToken(featureMap), //获取 token
install(featureMap, inst)
);
}
初始化 Agent
Spy 间谍类
// 将 Spy 注入到 BootstrapClassLoader
inst.appendToBootstrapClassLoaderSearch(new JarFile(new File(
getSandboxSpyJarPath(home)
// SANDBOX_SPY_JAR_PATH
)));
// BEFORE
try {
/*
* do something...
*/
// RETURN
return;
} catch (Throwable cause) {
// THROWS
}

SandBoxClassLoader
final ClassLoader sandboxClassLoader = loadOrDefineClassLoader(
namespace,
getSandboxCoreJarPath(home)
// SANDBOX_CORE_JAR_PATH
);
@Override
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
final Class<?> loadedClass = findLoadedClass(name);
if (loadedClass != null) {
return loadedClass;
}
try {
Class<?> aClass = findClass(name);
if (resolve) {
resolveClass(aClass);
}
return aClass;
} catch (Exception e) {
return super.loadClass(name, resolve);
}
}
启动 HTTP 服务
@Override
public synchronized void bind(final CoreConfigure cfg, final Instrumentation inst) throws IOException {
this.cfg = cfg;
try {
initializer.initProcess(new Initializer.Processor() {
@Override
public void process() throws Throwable {
logger.info("initializing server. cfg={}", cfg);
jvmSandbox = new JvmSandbox(cfg, inst);
initHttpServer(); //初始化 http server
initJettyContextHandler(); //初始化 jetty context 处理器
httpServer.start();// 启动 http server
}
});
// 初始化加载所有的模块
try {
jvmSandbox.getCoreModuleManager().reset();
} catch (Throwable cause) {
logger.warn("reset occur error when initializing.", cause);
}
......
}
private void initJettyContextHandler() {
......
// module-http-servlet
final String pathSpec = "/module/http/*";
logger.info("initializing http-handler. path={}", contextPath + pathSpec);
context.addServlet(
new ServletHolder(new ModuleHttpServlet(cfg, jvmSandbox.getCoreModuleManager())),
pathSpec
);
httpServer.setHandler(context);
}
小结

模块管理
- $sandbox_home/module/沙箱系统模块目录,由配置项 system_module 进行定义。用于存放沙箱通用的管理模块,比如用于沙箱模块管理功能的 module-mgr 模块,未来的模块运行质量监控模块、安全校验模块也都将存放在此处,跟随沙箱的发布而分发。系统模块不受刷新(-f)、**强制刷新(-F)功能的影响,只有容器重置(-R)**能让沙箱重新加载系统模块目录下的所有模块。
- $sandbox_home/sandbox-module/沙箱用户模块目录,由 sandbox.properties 的配置项 user_module 进行定义,默认为${HOME}/.sandbox-module/。一般用于存放用户自研的模块。自研的模块经常要面临频繁的版本升级工作,当需要进行模块动态热插拔替换的时候,可以通过**刷新(-f)或强制刷新(-F)**来完成重新加载。
模块的生命周期
- 【加载模块】被沙箱正确加载,沙箱将会允许模块进行命令相应、代码插桩等动作
- 【激活模块】加载成功后默认是冻结状态,需要代码主动进行激活。模块只有在激活状态下才能监听到沙箱事件
- 【冻结模块】进入到冻结状态之后,之前侦听的所有沙箱事件都将被屏蔽。需要注意的是,冻结的模块不会退回事件侦听的代码插桩,只有 delete()、wathcing()或者模块被卸载的时候插桩代码才会被清理
- 【卸载沙箱】不会再看到该模块,之前给该模块分配的所有资源都将会被回收,包括模块已经侦听事件的类都将会被移除掉侦听插桩,干净利落不留后遗症
自定义模块
@Information(id = "broken-clock-tinker")
public class BrokenClockTinkerModule implements Module {
@Resource
private ModuleEventWatcher moduleEventWatcher;
@Command("repairCheckState")
public void repairCheckState() {
new EventWatchBuilder(moduleEventWatcher)
.onClass("com.taobao.demo.Clock")
.onBehavior("checkState")
.onWatch(new AdviceListener() {
/**
* 拦截{@code com.taobao.demo.Clock#checkState()}方法,当这个方法抛出异常时将会被
* AdviceListener#afterThrowing()所拦截
*/
@Override
protected void afterThrowing(Advice advice) throws Throwable {
// 在此,你可以通过 ProcessController 来改变原有方法的执行流程
// 这里的代码意义是:改变原方法抛出异常的行为,变更为立即返回;void 返回值用 null 表示
ProcessController.returnImmediately(null);
}
});
}
}
模块加载
jvmSandbox.getCoreModuleManager().reset();
public synchronized CoreModuleManager reset() throws ModuleException {
logger.info("resetting all loaded modules:{}", loadedModuleBOMap.keySet());
// 1. 强制卸载所有模块
unloadAll();
// 2. 加载所有模块
for (final File moduleLibDir : moduleLibDirArray) {
// 用户模块加载目录,加载用户模块目录下的所有模块
// 对模块访问权限进行校验
if (moduleLibDir.exists() && moduleLibDir.canRead()) {
new ModuleLibLoader(moduleLibDir, cfg.getLaunchMode())
.load(
new InnerModuleJarLoadCallback(),
new InnerModuleLoadCallback()
);
} else {
logger.warn("module-lib not access, ignore flush load this lib. path={}", moduleLibDir);
}
}
return this;
}
void load(final ModuleLoadCallback mCb) throws IOException {
boolean hasModuleLoadedSuccessFlag = false;
ModuleJarClassLoader moduleJarClassLoader = null;
logger.info("prepare loading module-jar={};", moduleJarFile);
try {
moduleJarClassLoader = new ModuleJarClassLoader(moduleJarFile);
final ClassLoader preTCL = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(moduleJarClassLoader);
try {
hasModuleLoadedSuccessFlag = loadingModules(moduleJarClassLoader, mCb);
} finally {
Thread.currentThread().setContextClassLoader(preTCL);
}
ModuleJarClassLoader
public class ModuleJarClassLoader extends RoutingURLClassLoader {
private ModuleJarClassLoader(final File moduleJarFile,
final File tempModuleJarFile) throws IOException {
super(
new URL[]{new URL("file:" + tempModuleJarFile.getPath())},
new Routing(
ModuleJarClassLoader.class.getClassLoader(),
"^com\\.alibaba\\.jvm\\.sandbox\\.api\\..*",
"^javax\\.servlet\\..*",
"^javax\\.annotation\\.Resource.*$"
)
);
}
RoutingURLClassLoader
public class RoutingURLClassLoader extends URLClassLoader {
private static final Logger logger = LoggerFactory.getLogger(RoutingURLClassLoader.class);
private final ClassLoadingLock classLoadingLock = new ClassLoadingLock();
private final Routing[] routingArray;
public RoutingURLClassLoader(final URL[] urls,
final Routing... routingArray) {
super(urls);
this.routingArray = routingArray;
}
@Override
protected Class<?> loadClass(final String javaClassName, final boolean resolve) throws ClassNotFoundException {
return classLoadingLock.loadingInLock(javaClassName, new ClassLoadingLock.ClassLoading() {
@Override
public Class<?> loadClass(String javaClassName) throws ClassNotFoundException {
// 优先查询类加载路由表,如果命中路由规则,则优先从路由表中的 ClassLoader 完成类加载
if (ArrayUtils.isNotEmpty(routingArray)) {
for (final Routing routing : routingArray) {
if (!routing.isHit(javaClassName)) {
continue;
}
final ClassLoader routingClassLoader = routing.classLoader;
try {
return routingClassLoader.loadClass(javaClassName);
} catch (Exception cause) {
// 如果在当前 routingClassLoader 中找不到应该优先加载的类(应该不可能,但不排除有就是故意命名成同名类)
// 此时应该忽略异常,继续往下加载
// ignore...
}
}
}
// 先走一次已加载类的缓存,如果没有命中,则继续往下加载
final Class<?> loadedClass = findLoadedClass(javaClassName);
if (loadedClass != null) {
return loadedClass;
}
try {
Class<?> aClass = findClass(javaClassName);
if (resolve) {
resolveClass(aClass);
}
return aClass;
} catch (Exception cause) {
DelegateBizClassLoader delegateBizClassLoader = BusinessClassLoaderHolder.getBussinessClassLoader();
try {
if(null != delegateBizClassLoader){
return delegateBizClassLoader.loadClass(javaClassName,resolve);
}
} catch (Exception e) {
//忽略异常,继续往下加载
}
return RoutingURLClassLoader.super.loadClass(javaClassName, resolve);
}
}
});
}
}
private boolean loadingModules(final ModuleJarClassLoader moduleClassLoader,
final ModuleLoadCallback mCb) {
final Set<String> loadedModuleUniqueIds = new LinkedHashSet<String>();
final ServiceLoader<Module> moduleServiceLoader = ServiceLoader.load(Module.class, moduleClassLoader);
final Iterator<Module> moduleIt = moduleServiceLoader.iterator();
while (moduleIt.hasNext()) {
final Module module;
try {
module = moduleIt.next();
} catch (Throwable cause) {
logger.warn("loading module instance failed: instance occur error, will be ignored. module-jar={}", moduleJarFile, cause);
continue;
}
final Class<?> classOfModule = module.getClass();
// 判断模块是否实现了@Information 标记
if (!classOfModule.isAnnotationPresent(Information.class)) {
logger.warn("loading module instance failed: not implements @Information, will be ignored. class={};module-jar={};",
classOfModule,
moduleJarFile
);
continue;
}
final Information info = classOfModule.getAnnotation(Information.class);
final String uniqueId = info.id();
// 判断模块 ID 是否合法
if (StringUtils.isBlank(uniqueId)) {
logger.warn("loading module instance failed: @Information.id is missing, will be ignored. class={};module-jar={};",
classOfModule,
moduleJarFile
);
continue;
}
// 判断模块要求的启动模式和容器的启动模式是否匹配
if (!ArrayUtils.contains(info.mode(), mode)) {
logger.warn("loading module instance failed: launch-mode is not match module required, will be ignored. module={};launch-mode={};required-mode={};class={};module-jar={};",
uniqueId,
mode,
StringUtils.join(info.mode(), ","),
classOfModule,
moduleJarFile
);
continue;
}
try {
if (null != mCb) {
mCb.onLoad(uniqueId, classOfModule, module, moduleJarFile, moduleClassLoader);
}
}
......
}
@Infomation
@Information(id = "broken-clock-tinker")
public class BrokenClockTinkerModule implements Module
onLoad(真正的加载)
- 初始化模块信息 CoreModule 在 coreModule 中有 module 的实现类,moduleJar,ModuleClassLoader,以及最重要的模块类转换器集合 sandboxClassFileTransformers,类转换器则是字节码增强的关键,代码增强章节会详细介绍
- 注入注解@Resource 资源,例如 ModuleEventWatcher 事件观察者。关于 ModuleEventWatcher 代码增强章节会详细介绍
- 通知生命周期中模块加载的对应实现(module 实现类可以同时实现 Module 接口以及沙箱模块生命周期接口 ModuleLifecyle 接口),在 ModuleLifecyle 接口对应的方法中,用户可以自定义实现业务逻辑。
- 激活模块并通知生命周期中模块激活的对应实现,只有被激活的模块才能响应模块的增强事件。
- 将模块唯一 id 和当前的 coreModule 实例存入模块列表,模块列表是一个全局的 ConcurrentHashMap。还记得在 Httpserver 启动时会初始化 ModuleHttpServlet,在 ModuleHttpServlet 接收请求时会从参数中解析出模块 id,从而获取到对应的 coreModule.
- 通知生命周期中模块加载完成的对应实现
private synchronized void load(final String uniqueId,
final Module module,
final File moduleJarFile,
final ModuleJarClassLoader moduleClassLoader) throws ModuleException {
......
// 初始化模块信息
final CoreModule coreModule = new CoreModule(uniqueId, moduleJarFile, moduleClassLoader, module);
// 注入@Resource 资源
injectResourceOnLoadIfNecessary(coreModule);
// 通知生命周期,模块加载
callAndFireModuleLifeCycle(coreModule, MODULE_LOAD);
// 设置为已经加载
coreModule.markLoaded(true);
// 如果模块标记了加载时自动激活,则需要在加载完成之后激活模块
markActiveOnLoadIfNecessary(coreModule);
// 注册到模块列表中
loadedModuleBOMap.put(uniqueId, coreModule);
// 通知生命周期,模块加载完成
callAndFireModuleLifeCycle(coreModule, MODULE_LOAD_COMPLETED);
}
小结

模块激活
# -a active module
[[ -n ${OP_MODULE_ACTIVE} ]] &&
sandbox_curl_with_exit "sandbox-module-mgr/active" "&ids=${ARG_MODULE_ACTIVE}"
@Command("active")
public void active(final Map<String, String> param,
final PrintWriter writer) throws ModuleException {
int total = 0;
final String idsStringPattern = getParamWithDefault(param, "ids", EMPTY);
for (final Module module : search(idsStringPattern)) {
final Information info = module.getClass().getAnnotation(Information.class);
final boolean isActivated = moduleManager.isActivated(info.id());
if (!isActivated) {
try {
moduleManager.active(info.id());
total++;
} catch (ModuleException me) {
logger.warn("active module[id={};] occur error={}.", me.getUniqueId(), me.getErrorCode(), me);
}// try
} else {
total++;
}
}// for
output(writer, "total %s module activated.", total);
}
小结

模块冻结
# -A frozen module
[[ -n ${OP_MODULE_FROZEN} ]] &&
sandbox_curl_with_exit "sandbox-module-mgr/frozen" "&ids=${ARG_MODULE_FROZEN}
小结

模块卸载
# -u unload module
[[ -n ${OP_MODULE_UNLOAD} ]] &&
sandbox_curl_with_exit "sandbox-module-mgr/unload" "&action=unload&ids=${ARG_MODULE_UNLOAD}"
- 尝试冻结模块,让事件处理器暂停不在执行用户自定义的增强逻辑。
- 通知生命周期模块卸载的对应实现
- 从模块注册表中删除对应的模块
- 标记卸载,isLoaded=true
- 释放资源,在模块加载流程中有一步是注入@Resource 资源,在注入 ModuleEventWatcher 资源时,实际上是构建了 ReleaseResource 对象,并实现了 release 方法。在 ModuleEventWatcher.delete 中会利用 Instrumentation 类库删除类增强转换器 SandboxClassFileTransformer,这样有新的类加载时将不会植入 spy 间谍类了。但是已加载的类总还是有间谍类的代码。通过 Instrumentation 类库重新渲染目标类字节码。
@Override
public void delete(final int watcherId,
final Progress progress) {
final Set<Matcher> waitingRemoveMatcherSet = new LinkedHashSet<Matcher>();
// 找出待删除的 SandboxClassFileTransformer
final Iterator<SandboxClassFileTransformer> cftIt = coreModule.getSandboxClassFileTransformers().iterator();
int cCnt = 0, mCnt = 0;
while (cftIt.hasNext()) {
final SandboxClassFileTransformer sandboxClassFileTransformer = cftIt.next();
if (watcherId == sandboxClassFileTransformer.getWatchId()) {
// 冻结所有关联代码增强
EventListenerHandler.getSingleton()
.frozen(sandboxClassFileTransformer.getListenerId());
// 在 JVM 中移除掉命中的 ClassFileTransformer
inst.removeTransformer(sandboxClassFileTransformer);
// 计数
cCnt += sandboxClassFileTransformer.getAffectStatistic().cCnt();
mCnt += sandboxClassFileTransformer.getAffectStatistic().mCnt();
// 追加到待删除过滤器集合
waitingRemoveMatcherSet.add(sandboxClassFileTransformer.getMatcher());
// 清除掉该 SandboxClassFileTransformer
cftIt.remove();
}
}
// 查找需要删除后重新渲染的类集合
final List<Class<?>> waitingReTransformClasses = classDataSource.findForReTransform(
new GroupMatcher.Or(waitingRemoveMatcherSet.toArray(new Matcher[0]))
);
logger.info("watch={} in module={} found {} classes for delete.",
watcherId,
coreModule.getUniqueId(),
waitingReTransformClasses.size()
);
beginProgress(progress, waitingReTransformClasses.size());
try {
// 应用 JVM
reTransformClasses(watcherId, waitingReTransformClasses, progress);
} finally {
finishProgress(progress, cCnt, mCnt);
}
}
- 关闭 ModuleJarClassLoader,可以将其加载的模块类全部从 jvm 清理掉。在关闭前会利用 SPI 找到模块中 ModuleJarUnloadSPI 接口的实现类,通知模块已经被卸载了可以做一些自定义的业务处理。
public void closeIfPossible() {
onJarUnLoadCompleted();
try {
// 如果是 JDK7+的版本, URLClassLoader 实现了 Closeable 接口,直接调用即可
if (this instanceof Closeable) {
logger.debug("JDK is 1.7+, use URLClassLoader[file={}].close()", moduleJarFile);
try {
((Closeable)this).close();
} catch (Throwable cause) {
logger.warn("close ModuleJarClassLoader[file={}] failed. JDK7+", moduleJarFile, cause);
}
return;
}
.......
}
}
小结

代码增强
自定义模块
@Information(id = "broken-clock-tinker")
public class BrokenClockTinkerModule implements Module {
@Resource
private ModuleEventWatcher moduleEventWatcher;
@Command("repairCheckState")
public void repairCheckState() {
new EventWatchBuilder(moduleEventWatcher)
.onClass("com.taobao.demo.Clock")
.onBehavior("checkState")
.onWatch(new AdviceListener() {
/**
* 拦截{@code com.taobao.demo.Clock#checkState()}方法,当这个方法抛出异常时将会被
* AdviceListener#afterThrowing()所拦截
*/
@Override
protected void afterThrowing(Advice advice) throws Throwable {
// 在此,你可以通过 ProcessController 来改变原有方法的执行流程
// 这里的代码意义是:改变原方法抛出异常的行为,变更为立即返回;void 返回值用 null 表示
ProcessController.returnImmediately(null);
}
});
}
}
watch
- 在 watch 方法中首先会创建 SandboxClassFileTransformer 沙箱类转换器,并将其注册到 CoreModule 中
- 利用 Instrumentation 类库的 addTransformer api 注册 SandboxClassFileTransformer 沙箱类转换器实例注册类转换器后,后面所有的类加载都会经过 SandboxClassFileTransformer
- 查找需要渲染的类集合,利用 matcher 对象查找当前 jvm 已加载的类matcher 对象则是 EventWatchBuilder 中指定的目标类和目标方法的包装对象,在 matcher 对象中指定了匹配规则。
- 将查找到的类进行渲染(代码增强),利用 Instrumentation 的 retransformClasses 方法重新对 JVM 已加载的类进行字节码转换。
private int watch(final Matcher matcher,
final EventListener listener,
final Progress progress,
final Event.Type... eventType) {
final int watchId = watchIdSequencer.next();
// 给对应的模块追加 ClassFileTransformer
final SandboxClassFileTransformer sandClassFileTransformer = new SandboxClassFileTransformer(
watchId, coreModule.getUniqueId(), matcher, listener, isEnableUnsafe, eventType, namespace);
// 注册到 CoreModule 中
coreModule.getSandboxClassFileTransformers().add(sandClassFileTransformer);
//这里 addTransformer 后,接下来引起的类加载都会经过 sandClassFileTransformer
inst.addTransformer(sandClassFileTransformer, true);
// 查找需要渲染的类集合
final List<Class<?>> waitingReTransformClasses = classDataSource.findForReTransform(matcher);
logger.info("watch={} in module={} found {} classes for watch(ing).",
watchId,
coreModule.getUniqueId(),
waitingReTransformClasses.size()
);
int cCnt = 0, mCnt = 0;
// 进度通知启动
beginProgress(progress, waitingReTransformClasses.size());
try {
// 应用 JVM
reTransformClasses(watchId,waitingReTransformClasses, progress);
// 计数
cCnt += sandClassFileTransformer.getAffectStatistic().cCnt();
mCnt += sandClassFileTransformer.getAffectStatistic().mCnt();
// 激活增强类
if (coreModule.isActivated()) {
final int listenerId = sandClassFileTransformer.getListenerId();
EventListenerHandler.getSingleton()
.active(listenerId, listener, eventType);
}
} finally {
finishProgress(progress, cCnt, mCnt);
}
return watchId;
代码增强
private byte[] _transform(final ClassLoader loader,
final String internalClassName,
final Class<?> classBeingRedefined,
final byte[] srcByteCodeArray) {
// 如果未开启 unsafe 开关,是不允许增强来自 BootStrapClassLoader 的类
if (!isEnableUnsafe
&& null == loader) {
logger.debug("transform ignore {}, class from bootstrap but unsafe.enable=false.", internalClassName);
return null;
}
final ClassStructure classStructure = getClassStructure(loader, classBeingRedefined, srcByteCodeArray);
final MatchingResult matchingResult = new UnsupportedMatcher(loader, isEnableUnsafe).and(matcher).matching(classStructure);
final Set<String> behaviorSignCodes = matchingResult.getBehaviorSignCodes();
// 如果一个行为都没匹配上也不用继续了
if (!matchingResult.isMatched()) {
logger.debug("transform ignore {}, no behaviors matched in loader={}", internalClassName, loader);
return null;
}
// 开始进行类匹配
try {
final byte[] toByteCodeArray = new EventEnhancer().toByteCodeArray(
loader,
srcByteCodeArray,
behaviorSignCodes,
namespace,
listenerId,
eventTypeArray
);
if (srcByteCodeArray == toByteCodeArray) {
logger.debug("transform ignore {}, nothing changed in loader={}", internalClassName, loader);
return null;
}
// statistic affect
affectStatistic.statisticAffect(loader, internalClassName, behaviorSignCodes);
logger.info("transform {} finished, by module={} in loader={}", internalClassName, uniqueId, loader);
return toByteCodeArray;
} catch (Throwable cause) {
logger.warn("transform {} failed, by module={} in loader={}", internalClassName, uniqueId, loader, cause);
return null;
}
}
@Override
public byte[] toByteCodeArray(final ClassLoader targetClassLoader,
final byte[] byteCodeArray,
final Set<String> signCodes,
final String namespace,
final int listenerId,
final Event.Type[] eventTypeArray) {
// 返回增强后字节码
final ClassReader cr = new ClassReader(byteCodeArray);
final ClassWriter cw = createClassWriter(targetClassLoader, cr);
final int targetClassLoaderObjectID = ObjectIDs.instance.identity(targetClassLoader);
cr.accept(
new EventWeaver(
ASM7, cw, namespace, listenerId,
targetClassLoaderObjectID,
cr.getClassName(),
signCodes,
eventTypeArray
),
EXPAND_FRAMES
);
return dumpClassIfNecessary(cr.getClassName(), cw.toByteArray());
}

小结
事件处理
public static Ret spyMethodOnBefore(final Object[] argumentArray,
final String namespace,
final int listenerId,
final int targetClassLoaderObjectID,
final String javaClassName,
final String javaMethodName,
final String javaMethodDesc,
final Object target) throws Throwable {
final Thread thread = Thread.currentThread();
if (selfCallBarrier.isEnter(thread)) {
return Ret.RET_NONE;
}
final SelfCallBarrier.Node node = selfCallBarrier.enter(thread);
try {
final SpyHandler spyHandler = namespaceSpyHandlerMap.get(namespace);
if (null == spyHandler) {
return Ret.RET_NONE;
}
return spyHandler.handleOnBefore(
listenerId, targetClassLoaderObjectID, argumentArray,
javaClassName,
javaMethodName,
javaMethodDesc,
target
);
} catch (Throwable cause) {
handleException(cause);
return Ret.RET_NONE;
} finally {
selfCallBarrier.exit(thread, node);
}
}
EventListenHandler
@Override
public Spy.Ret handleOnBefore(int listenerId, int targetClassLoaderObjectID, Object[] argumentArray, String javaClassName, String javaMethodName, String javaMethodDesc, Object target) throws Throwable {
// 在守护区内产生的事件不需要响应
if (SandboxProtector.instance.isInProtecting()) {
logger.debug("listener={} is in protecting, ignore processing before-event", listenerId);
return newInstanceForNone();
}
// 获取事件处理器
final EventProcessor processor = mappingOfEventProcessor.get(listenerId);
// 如果尚未注册,则直接返回,不做任何处理
if (null == processor) {
logger.debug("listener={} is not activated, ignore processing before-event.", listenerId);
return newInstanceForNone();
}
// 获取调用跟踪信息
final EventProcessor.Process process = processor.processRef.get();
// 如果当前处理 ID 被忽略,则立即返回
if (process.isIgnoreProcess()) {
logger.debug("listener={} is marked ignore process!", listenerId);
return newInstanceForNone();
}
// 调用 ID
final int invokeId = invokeIdSequencer.getAndIncrement();
process.pushInvokeId(invokeId);
// 调用过程 ID
final int processId = process.getProcessId();
final ClassLoader javaClassLoader = ObjectIDs.instance.getObject(targetClassLoaderObjectID);
//放置业务类加载器
BusinessClassLoaderHolder.setBussinessClassLoader(javaClassLoader);
final BeforeEvent event = process.getEventFactory().makeBeforeEvent(
processId,
invokeId,
javaClassLoader,
javaClassName,
javaMethodName,
javaMethodDesc,
target,
argumentArray
);
try {
return handleEvent(listenerId, processId, invokeId, event, processor);
} finally {
process.getEventFactory().returnEvent(event);
}
}
EventProcessor
public void active(final int listenerId,
final EventListener listener,
final Event.Type[] eventTypes) {
mappingOfEventProcessor.put(listenerId, new EventProcessor(listenerId, listener, eventTypes));
logger.info("activated listener[id={};target={};] event={}",
listenerId,
listener,
join(eventTypes, ",")
);
}
listenerId
listener
小结

总结
- JVM-SANDBOX 基于 JVM TI(JVM 工具接口)实现 agent 对目标 jvm 进行挂载,利用 Instrumentation 类库注册自定义的类转换器(SandboxClassFileTransformer),在类转换器中使用 ASM 框架对目标类的方法织入 Spy 间谍类中的埋点方法,从而实现字节码增强功能。
- JVM-SANDBOX 利用 ClassLoader 双亲委派模型,自定义 SandboxClassLoader 加载沙箱内部类(spy-core),使用自定义 ModulerJarClassLoader 和 SPI 机制对自定义模块进行加载,从而实现类隔离。
- 可插拔以及多租户的特性更多的是偏于 JVM-SANDBOX 内部的业务逻辑,在模块管理章节模块的加载和卸载中有详细描述。
边栏推荐
- Cesiumjs 2022^ source code interpretation [0] - article directory and source code engineering structure
- Aperçu du code source futur - série juc
- laravel admin里百度编辑器自定义路径和文件名
- My opinion on how to effectively telecommute | community essay solicitation
- [PaddleSeg 源码阅读] PaddleSeg计算 mIoU
- JDBC advanced
- 2022-07-03: there are 0 and 1 in the array. Be sure to flip an interval. Flip: 0 becomes 1, 1 becomes 0. What is the maximum number of 1 after turning? From little red book. 3.13 written examination.
- 思考的小记录
- Huawei cloud Kunpeng engineer training (Guangxi University)
- Pytest multi process / multi thread execution test case
猜你喜欢
Balance between picture performance of unity mobile game performance optimization spectrum and GPU pressure
1289_FreeRTOS中vTaskSuspend()接口实现分析
2022-07-03:数组里有0和1,一定要翻转一个区间,翻转:0变1,1变0。 请问翻转后可以使得1的个数最多是多少? 来自小红书。3.13笔试。
Katalon中控件的参数化
[PaddleSeg 源码阅读] PaddleSeg 自定义数据类
JVM family -- monitoring tools
SQL statement strengthening exercise (MySQL 8.0 as an example)
【CSRF-01】跨站请求伪造漏洞基础原理及攻防
There is a problem that the package cannot be parsed in the like project
Tcpclientdemo for TCP protocol interaction
随机推荐
[paddleseg source code reading] paddleseg calculates Miou
Apple submitted the new MAC model to the regulatory database before the spring conference
Wechat official account web page authorization
Brief explanation of depth first search (with basic questions)
【华为云IoT】读书笔记之《万物互联:物联网核心技术与安全》第3章(上)
Illustrated network: what is the hot backup router protocol HSRP?
pytest多进程/多线程执行测试用例
Which product is better if you want to go abroad to insure Xinguan?
Object oriented -- encapsulation, inheritance, polymorphism
2022-07-03:数组里有0和1,一定要翻转一个区间,翻转:0变1,1变0。 请问翻转后可以使得1的个数最多是多少? 来自小红书。3.13笔试。
What kind of experience is it when the Institute earns 20000 yuan a month!
Penetration practice - sqlserver empowerment
函数计算异步任务能力介绍 - 任务触发去重
Confession code collection, who says program apes don't understand romance
Pandora IOT development board learning (HAL Library) - Experiment 6 independent watchdog experiment (learning notes)
pytest多进程/多线程执行测试用例
疫情来袭--远程办公之思考|社区征文
毕业总结
Recursive structure
LNK2038 检测到“RuntimeLibrary”的不匹配项: 值“MD_DynamicRelease”不匹配值“MDd_DynamicDebug”(main.obj 中)