1、什么是JVM SandBox
JVM SandBox(沙箱)实现了一种非侵入式运行期的AOP解决方案。JVM SandBox属于基于Instrumentation的动态编织类的AOP框架,可以在不重启应用的情况下,在运行时完成目标方法的增强和替换,同时沙箱以及沙箱的模块可以随时加载和卸载
主要特性如下:
- 无侵入:目标应用无需重启也无需感知沙箱的存在
- 类隔离:沙箱以及沙箱的模块不会和目标应用的类相互干扰
- 可插拔:沙箱以及沙箱的模块可以随时加载和卸载,不会在目标应用留下痕迹
- 多租户:目标应用可以同时挂载不同租户下的沙箱并独立控制
- 高兼容:支持JDK[6,11]
常见应用场景如下:
- 线上故障定位
- 线上系统流控
- 线上故障模拟
- 方法请求录制和结果回放
- 动态日志打印
- 安全信息监测和脱敏
2、JVM SandBox实现原理
1)、挂载
JVM SandBox支持通过premain()
方法在JVM启动的时候加载;也支持agentmain()
方法通过Attach API的方式在JVM启动之后被加载
sandbox-agent模块:
public class AgentLauncher {
/**
* 启动加载
*
* @param featureString 启动参数
* [namespace,prop]
* @param inst inst
*/
public static void premain(String featureString, Instrumentation inst) {
LAUNCH_MODE = LAUNCH_MODE_AGENT;
install(toFeatureMap(featureString), inst);
}
/**
* 动态加载
*
* @param featureString 启动参数
* [namespace,token,ip,port,prop]
* @param inst inst
*/
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)
);
}
JVM Sandbox主要包含SandBox Core、Jetty Server和自定义处理模块三部分
客户端通过Attach API将沙箱挂载到目标JVM进程上,启动之后沙箱会一直维护着Instrumentation对象引用,通过Instrumentation来修改字节码和重定义类。另外,SandBox启动之后同时会启动一个内部的Jetty服务器,这个服务器用于外部和SandBox进行通信,对模块的加载、卸载、激活、冻结等命令等命令操作都会通过Http请求的方式进行
JVM SandBox包括如下模块:
- sandbox-info:沙箱信息模块,查看当前Sandbox的版本等信息
- sandbox-module-mgr:沙箱模块管理模块,负责管理管理模块的生命周期(加载、冻结、激活等)
- sandbox-control:负责卸载sandbox
- 自定义处理模块扩展
JVM SandBox模块的生命周期:
只有当模块处于激活状态,才会真正调用用户的AOP增强逻辑
2)、类隔离机制
BootstrapClassLoader加载Spy类(真正织入代码的类)
JVM Sandbox中有两个自定义的ClassLoader:SandBoxClassLoader加载沙箱模块功能,ModuleJarClassLoader加载用户定义模块功能
它们通过重写java.lang.ClassLoader
的loadClass(String name, boolean resolve)
方法,打破了双亲委派约定,达到与目标类隔离的目的,不会引起应用的类污染、冲突
3)、ClassLoader源码解析
SandBoxClassLoader源码如下:
class SandboxClassLoader extends URLClassLoader {
@Override
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
//先走一次已加载类的缓存,如果没有命中,则继续往下加载
final Class<?> loadedClass = findLoadedClass(name);
if (loadedClass != null) {
return loadedClass;
}
try {
//调用URLClassLoader的findClass方法,从自定义的路径中寻找类
Class<?> aClass = findClass(name);
if (resolve) {
resolveClass(aClass);
}
return aClass;
} catch (Exception e) {
//没有找到类,在委托AppClassLoader去加载
return super.loadClass(name, resolve);
}
}
ModuleClassLoader继承了RoutingURLClassLoader,RoutingURLClassLoader中有一个静态内部Routing类,这里传进来的classLoader是SandboxClassLoader,意思是这些指定正则的路径由SandboxClassLoader加载
Routing类的作用如下:
Sandbox是允许有多个module的jar包的,每个module分别new一个ModuleClassLoader去加载,jar包里面要用到@Resource注解注入model还有Sandbox的核心类,如果@Resource注解被ModuleClassLoader加载,那一个JVM实例中就会有多个Resource实例,Sandbox内部的核心类也一样。因此这些类只能由SandboxClassLoader加载
/**
* 类加载路由匹配器
*/
public static class Routing {
private final Collection<String/*REGEX*/> regexExpresses = new ArrayList<String>();
private final ClassLoader classLoader;
/**
* 构造类加载路由匹配器
*
* @param classLoader 目标ClassLoader
* @param regexExpressArray 匹配规则表达式数组
*/
Routing(final ClassLoader classLoader, final String... regexExpressArray) {
if (ArrayUtils.isNotEmpty(regexExpressArray)) {
regexExpresses.addAll(Arrays.asList(regexExpressArray));
}
this.classLoader = classLoader;
}
/**
* 当前参与匹配的Java类名是否命中路由匹配规则
* 命中匹配规则的类加载,将会从此ClassLoader中完成对应的加载行为
*
* @param javaClassName 参与匹配的Java类名
* @return true:命中;false:不命中;
*/
private boolean isHit(final String javaClassName) {
for (final String regexExpress : regexExpresses) {
try {
if (javaClassName.matches(regexExpress)) {
return true;
}
} catch (Throwable cause) {
logger.warn("routing {} failed, regex-express={}.", javaClassName, regexExpress, cause);
}
}
return false;
}
}
ModuleClassLoader的父类RoutingURLClassLoader中重写了loadClass(String javaClassName)
方法,在module中引用sandbox-core的类由SandboxClassLoader负责加载:
public class RoutingURLClassLoader extends URLClassLoader {
@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);
}
}
});
}
ModuleClassLoader类加载流程如下:
4)、SandBox初始化流程
SandBox初始化流程如下图:
5)、类增强策略
SandBox通过在BootstrapClassLoader中埋藏的Spy类完成目标类和沙箱内核的通讯,最终执行到用户模块的AOP方法
6)、字节码增强和撤销流程
字节码增强时,通过Instrumentation的addTransformer(ClassFileTransformer transformer)
方法注册一个ClassFileTransformer,从此之后的类加载都会被ClassFileTransformer拦截,然后调用Instrumentation的retransformClasses(Class<?>... classes)
对JVM已经加载的类重新触发类加载,类加载时会被ClassFileTransformer拦截
字节码增强撤销时,通过Instrumentation的removeTransformer(ClassFileTransformer transformer)
方法移除相应的ClassFileTransformer,然后调用Instrumentation的retransformClasses(Class<?>... classes)
重新触发类加载
推荐JVM SandBox文章: