上一篇我们聊到了sandbox-agent会通过自定义类加载器SandboxClassloader加载并反射JettyCoreServer实例,然后反射调用该实例的JettyCoreServer#bind方发启动JettyServer。目前的启动流程可以由下图展示:
这里再多嘴一句,注意反射调用JettyCoreServer#bind方法时,传递了Instrumentation这个参数,这个可是实现Sandbox增强能力的核心。
这一篇我们主要讲解bind之后的流程,包括JettyServer的启动、沙箱的启动以及模块的加载。
老样子,这里先给出bind方法的全貌,并且给出了简单注释。因为每行被注释的代码都包含了其他逻辑,因此会在下面分开讲解,下面会主要讲解4个部分的内容:
- 初始化沙箱
- 初始化 Jetty 服务器
- 初始化 Jetty context handler
- 初始化加载所有的沙箱模块
// JettyCoreServer.java
@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 {
LogbackUtils.init(cfg.getNamespace(), cfg.getCfgLibPath() + File.separator + "sandbox-logback.xml");
logger.info("initializing server. cfg={}", cfg);
// 初始化沙箱
jvmSandbox = new JvmSandbox(cfg, inst);
// 初始化 Jetty 服务器
initHttpServer();
// 初始化 Jetty context handler
initJettyContextHandler();
// 启动 Jetty 服务器
httpServer.start();
}
});
try {
// 初始化加载所有的沙箱模块
jvmSandbox.getCoreModuleManager().reset();
} catch (Throwable cause) {
logger.warn("reset occur error when initializing.", cause);
}
final InetSocketAddress local = getLocal();
logger.info("initialized server. actual bind to {}:{}", local.getHostName(), local.getPort());
} catch (Throwable cause) {
// 这里会抛出到目标应用层,所以在这里留下错误信息
logger.warn("initialize server failed.", cause);
// 对外抛出到目标应用中
throw new IOException("server bind failed.", cause);
}
logger.info("{} bind success.", this);
}
1、初始化沙箱
初始化沙箱的代码就是调用了JvmSandbox的构造方法,并且将Instrumentation传入了沙箱。注意,这里又透传了Instrumentation对象,足以见得该对象的重要性。
// JettyCoreServer.java
jvmSandbox = new JvmSandbox(cfg, inst);
构造方法如下所示,它首先通过创建代理的方式初始化了沙箱管理器,从其中可以看出默认的实现为DefaultCoreModuleManager,它负责持有并管理各个模块,后期模块的各种操作都委托给该管理器来完成。
// JvmSandbox.java
public JvmSandbox(final CoreConfigure cfg, final Instrumentation inst) {
EventListenerHandler.getSingleton();
this.cfg = cfg;
this.coreModuleManager = SandboxProtector.instance.protectProxy(CoreModuleManager.class, new DefaultCoreModuleManager(
cfg,
inst,
new DefaultCoreLoadedClassDataSource(inst, cfg.isEnableUnsafe()),
new DefaultProviderManager(cfg)
));
init();
}
1.1、守护代理
这里有一个比较有意思的创建保护代理的方式,SandboxProtector.instance.protectProxy,官方对其称呼为 【守护接口定义的所有方法】 :它是在代理方法的前后计算Threadlocal中的引用计数,判断进入方法前和退出方法后引用计数是否相同。如果相同,说明是同一个线程的正常进入和退出,则代理正常返回;如果不同,说明是同一个线程多次进入了代理方法(虽然我也不太清楚为什么会存在这种情况)。针对这种情况,官方的策略是仅仅打印了异常日志,并未做其它处理。 这里我也咨询了我的大佬同事,他猜想这里可能是为了保护某种极端场景下的线程问题,记个日志,便于后期问题定位。
// SandboxProtector.java
private final ThreadLocal<AtomicInteger> isInProtectingThreadLocal = new ThreadLocal<AtomicInteger>() {
@Override
protected AtomicInteger initialValue() {
return new AtomicInteger(0);
}
};
/**
* 守护接口定义的所有方法
*
* @param protectTargetInterface 保护目标接口类型
* @param protectTarget 保护目标接口实现
* @param <T> 接口类型
* @return 被保护的目标接口实现
*/
@SuppressWarnings("unchecked")
public <T> T protectProxy(final Class<T> protectTargetInterface, final T protectTarget) {
return (T) Proxy.newProxyInstance(getClass().getClassLoader(), new Class<?>[]{protectTargetInterface}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
final int enterReferenceCount = enterProtecting();
try {
return method.invoke(protectTarget, args);
} finally {
final int exitReferenceCount = exitProtecting();
// 这里判断两次获得的引用计数是否相同
// 由于Java规定return和finally的执行顺序为:先执行return后的表达式,再执行finally块,最后退出。
// 这就说明无论引用计数是否相同,代理方法依旧会先执行,作者应该只是在这里判断下,做个异常日志的记录,并未针对这种异常现象做其它处理
if (enterReferenceCount != exitReferenceCount) {
logger.warn("thread:{} exit protecting with error!, expect:{} actual:{}", Thread.currentThread(), enterReferenceCount, exitReferenceCount);
}
}
}
});
}
public int enterProtecting() {
// 进入守护区前(也就是执行代理方法前)获得ThreadLocal中的引用计数 cnt,然后 cnt++
final int referenceCount = isInProtectingThreadLocal.get().getAndIncrement();
// ... 中间是一些日志打印
return referenceCount;
}
public int exitProtecting() {
// 退出守护区后(也就是执行完代理方法之后)先让ThreadLocal中的引用计数cnt--,然后再get
final int referenceCount = isInProtectingThreadLocal.get().decrementAndGet();
// ... 中间是一些日志打印
return referenceCount;
}
这里用一张图也许能够更好的解释下(这里解释的或许有些问题,有了解的小伙帮可以在评论区告知下哈,感激 ψ(`∇´)ψ ):
1.2、初始化模块管理器
由上述的代码可知,我们在创建守护代理时,代理的是DefaultCoreModuleManager这个对象,创建这个对象是直接调用构造方法来实例化:
new DefaultCoreModuleManager(cfg, inst, new DefaultCoreLoadedClassDataSource(inst, cfg.isEnableUnsafe()), new DefaultProviderManager(cfg))
这个构造方法里面并没有什么好说的,就是一些赋值操作和目录合并,我们应该关注在调用这个构造方法时创建的两个对象:DefaultCoreLoadedClassDataSource和DefaultProviderManager。
- DefaultCoreLoadedClassDataSource
DefaultCoreLoadedClassDataSource抽象表示所有已加载类的数据池,其实它可以看成是对Instrumentation的封装,构造该对象时传递了inst这个Instrumentation这个对象,DefaultCoreLoadedClassDataSource是通过Instrumentation#getAllLoadedClasses方法获取所有已加载的类集合的,整个类逻辑比较简单,大家可以自行查看。
- DefaultProviderManager
DefaultProviderManager抽象表示默认服务提供管理器,这里我就总结性的说下初始化DefaultProviderManager做了什么,具体细节大家可以参考注释和源码。DefaultProviderManager通过JDK SPI的方式加载了ModuleJarLoadingChain和ModuleLoadingChain的实例,并对这些实例里面所有被标记了@Resource注解的字段注入CoreConfigure属性。这个过程也是想要将cfg注入到ModuleJarLoadingChain和ModuleLoadingChain实例当中。
// DefaultProviderManager.java
public DefaultProviderManager(final CoreConfigure cfg) {
this.cfg = cfg;
try {
// 透传cfg
init(cfg);
} // ... 异常处理
}
private void init(final CoreConfigure cfg) {
final File providerLibDir = new File(cfg.getProviderLibPath());
// ... 一些校验
for (final File providerJarFile : FileUtils.listFiles(providerLibDir, new String[]{"jar"}, false)) {
try {
final ProviderClassLoader providerClassLoader = new ProviderClassLoader(providerJarFile, getClass().getClassLoader());
// SPI加载
inject(moduleJarLoadingChains, ModuleJarLoadingChain.class, providerClassLoader, providerJarFile);
inject(moduleLoadingChains, ModuleLoadingChain.class, providerClassLoader, providerJarFile);
} // ... 日志和异常处理
}
}
private <T> void inject(final Collection<T> collection, final Class<T> clazz, final ClassLoader providerClassLoader,
final File providerJarFile) throws IllegalAccessException {
// JDK SPI 加载 ModuleJarLoadingChain 或者 ModuleLoadingChain
final ServiceLoader<T> serviceLoader = ServiceLoader.load(clazz, providerClassLoader);
for (final T provider : serviceLoader) {
// 注入 cfg 属性
injectResource(provider);
collection.add(provider);
// ... 日志处理
}
}
private void injectResource(final Object provider) throws IllegalAccessException {
// 获取所有被标记了 @Resource 注解的 field
final Field[] resourceFieldArray = FieldUtils.getFieldsWithAnnotation(provider.getClass(), Resource.class);
// ... 校验
for (final Field resourceField : resourceFieldArray) {
// 遍历field,将cfg写入到field当中
final Class<?> fieldType = resourceField.getType();
if (ConfigInfo.class.isAssignableFrom(fieldType)) {
final ConfigInfo configInfo = new DefaultConfigInfo(cfg);
FieldUtils.writeField(resourceField, provider, configInfo, true);
}
}
}
1.3、Spy初始化
初始化沙箱的最后一步就是init方法了,我们来看看init方法做了什么:
- 提前加载了两个类:SandboxClassUtils、ClassStructureImplByAsm,后者是通过ASM增强业务方代码的重要类
- 通过SpyUtils将EventListenerHandler注入到Spy类中。前面我们说过了,Spy类最终会通过ASM框架写入到业务方代码中,而此时被注入的EventListenerHandler也是沙箱与业务代码通信的重要媒介,后面我们会重点介绍这个handler。
// JvmSandbox.java
private final static List<String> earlyLoadSandboxClassNameList = new ArrayList<String>();
static {
earlyLoadSandboxClassNameList.add("com.alibaba.jvm.sandbox.core.util.SandboxClassUtils");
earlyLoadSandboxClassNameList.add("com.alibaba.jvm.sandbox.core.util.matcher.structure.ClassStructureImplByAsm");
}
private void init() {
doEarlyLoadSandboxClass();
SpyUtils.init(cfg.getNamespace());
}
private void doEarlyLoadSandboxClass() {
// 提前加载一些类
for(String className : earlyLoadSandboxClassNameList){
try {
Class.forName(className);
} catch (ClassNotFoundException e) {
//加载sandbox内部的类,不可能加载不到
}
}
}
// SpyUtils.java
public synchronized static void init(final String namespace) {
if (!Spy.isInit(namespace)) {
// 注入 EventListenerHandler
Spy.init(namespace, EventListenerHandler.getSingleton());
}
}
// Spy.java
public static void init(final String namespace, final SpyHandler spyHandler) {
// 缓存 EventListenerHandler
namespaceSpyHandlerMap.putIfAbsent(namespace, spyHandler);
}
1.4、小结
沙箱初始完成后,整体流程便可以由下图展示了:
2、初始化Jetty服务器
初始化Jetty服务器的代码非常容易,主要逻辑是调用Jetty包中的一些接口,初始化Server和Server使用的ThreadPool,大家可以自行查看源码。
3、初始化JettyContextHandler
这里我们稍微花一点篇幅来聊一下,因为这个ContextHandler关系到我们如何通过脚本与沙箱进行http通信。
// JettyCoreServer.java
initJettyContextHandler();
private void initJettyContextHandler() {
final String namespace = cfg.getNamespace();
final ServletContextHandler context = new ServletContextHandler(NO_SESSIONS);
// 这里namespace一般为default(如果没有设置的话),因此 contextPath = /sandbox/default
final String contextPath = "/sandbox/" + namespace;
context.setContextPath(contextPath);
context.setClassLoader(getClass().getClassLoader());
// WebSocketAcceptorServlet 被废弃了,不再维护了,因此这里就不考虑了
final String wsPathSpec = "/module/websocket/*";
logger.info("initializing ws-http-handler. path={}", contextPath + wsPathSpec);
context.addServlet(new ServletHolder(new WebSocketAcceptorServlet(jvmSandbox.getCoreModuleManager())), wsPathSpec);
// 这个 Servlet 是 http 通信的关键
final String pathSpec = "/module/http/*";
logger.info("initializing http-handler. path={}", contextPath + pathSpec);
// 这里设置的是 Servlet 绑定的 path,再加上前面的 contextPath,我们可以知道 ModuleHttpServlet 最终会处理 /sandbox/default/module/http/* 这个路径
context.addServlet(new ServletHolder(new ModuleHttpServlet(cfg, jvmSandbox.getCoreModuleManager())), pathSpec);
httpServer.setHandler(context);
}
注释描述的非常清楚了,初始化这个handler后,ModuleHttpServlet将会绑定/sandbox/default/module/http/*这个路径。
ModuleHttpServlet中核心处理方法为doMethod,该方法的核心逻辑如下:
- 解析请求url,获取moduleId,进而从缓存中拿到id对应的CoreModule。
- 判断请求路径的请求方法是否在对应的CoreModule中被
@Command和@Http注解所标记。 - 如果方法被标记了,则反射调用该方法,实现外界与沙箱(模块)的通信。
doMethod关联的代码太多了,但具体逻辑就是上面说的,这里就不多余贴代码了,避免被怀疑凑字数。
3.1、shell脚本的http请求组装
通过上面描述的,我们知道了沙箱是如何通过Jetty服务器处理http请求的(即服务器端的处理),现在我们来看看sandbox.sh是如何组装http请求的(客户端发起请求的过程)。
前面的脚本分析过了,在执行main方法时,会轮训执行脚本时紧跟的参数,然后针对参数进行不同的处理,这里我们针对v这个参数进行分析,这个参数的作用是让沙箱在bash中打印出运行时信息。针对v参数处理的全链路源码如下:
- 设置OP_VERSION标志位,待后续处理
- 通过一系列的sandbox_curl_*方法组装curl请求,根据源码我们清楚的知道,最终组装的curl请求为:
curl -N -s "http://host:port/sandbox/default/module/http/sandbox-info/version?1=1",怎么样,是不是和上面的ModuleHttpServlet处理的路径对应上了。
v) OP_VERSION=1 ;;
[[ -n ${OP_VERSION} ]] &&
sandbox_curl_with_exit "sandbox-info/version"
function sandbox_curl_with_exit() {
sandbox_curl "${@}"
exit
}
function sandbox_curl() {
sandbox_debug_curl "module/http/${1}?1=1${2}"
}
function sandbox_debug_curl() {
local host=${SANDBOX_SERVER_NETWORK%;**}
local port=${SANDBOX_SERVER_NETWORK#**;}
if [[ "$host" == "0.0.0.0" ]]; then
host="127.0.0.1"
fi
curl -N -s "http://${host}:${port}/sandbox/${TARGET_NAMESPACE}/${1}" ||
exit_on_err 1 "target JVM ${TARGET_JVM_PID} lose response."
}
之后在doMethod方法中,会解析出sandbox-info和version两个信息,前者代表待处理的模块,后者代表模块中待处理的方法。
现在让我们来看看sandbox-info这个模块的内容吧,该模块在sandbox-mgr-module这个module下的com.alibaba.jvm.sandbox.module.mgr包中。模块的作用注释描述的非常清楚了,大家可以自行挑选感兴趣的继续深入学习。
// 该注解能够在打包时,添加JDK SPI加载所需要的文件
@MetaInfServices(Module.class)
// @Information注解中的id信息和请求路径匹配
@Information(id = "sandbox-info", version = "0.0.4", author = "luanjia@taobao.com")
public class InfoModule implements Module {
// 注入的全局配置信息
@Resource
private ConfigInfo configInfo;
// @Command注解标记,最终在doMethod中会反射调用该方法,方法内容比较简单粗暴,直接打印配置信息
@Command("version")
public void version(final PrintWriter writer) throws IOException {
final StringBuilder versionSB = new StringBuilder()
.append(" NAMESPACE : ").append(configInfo.getNamespace()).append("\n")
.append(" VERSION : ").append(configInfo.getVersion()).append("\n")
.append(" MODE : ").append(configInfo.getMode()).append("\n")
.append(" SERVER_ADDR : ").append(configInfo.getServerAddress().getHostName()).append("\n")
.append(" SERVER_PORT : ").append(configInfo.getServerAddress().getPort()).append("\n")
.append(" UNSAFE_SUPPORT : ").append(configInfo.isEnableUnsafe() ? "ENABLE" : "DISABLE").append("\n")
.append(" SANDBOX_HOME : ").append(configInfo.getHome()).append("\n")
.append(" SYSTEM_MODULE_LIB : ").append(configInfo.getSystemModuleLibPath()).append("\n")
.append(" USER_MODULE_LIB : ").append(configInfo.getUserModuleLibPath()).append("\n")
.append(" SYSTEM_PROVIDER_LIB : ").append(configInfo.getSystemProviderLibPath()).append("\n")
.append(" EVENT_POOL_SUPPORT : ").append(configInfo.isEnableEventPool() ? "ENABLE" : "DISABLE");
writer.println(versionSB.toString());
writer.flush();
}
// ...
}
有了上面的分析,我们便能够非常清楚脚本与沙箱的通信原理,就是通过请求路径匹配沙箱和其中的方法,然后通过反射调用方法实现沙箱的通信了。
这里用一张图作为这一小节的结尾吧(这里只展示通信过程,删除掉了无关信息):
4、总结
我们这一篇主要分析了沙箱与Sandbox内置Jetty服务器初始化的主要内容,并且深入讲解了沙箱模块管理器的守护代理机制和沙箱的http通信原理。
讲到这里,大家就已经对Sandbox里面大部分核心对象有了一定的认知,Sandbox目前的全貌也如图所示,后面将会进入到各个模块的加载流程中。
ps:原本这一篇还想分析沙箱是如何加载所有模块的,但上面几步分析的有些多,并且模块加载的逻辑也挺多的,放在一篇话怕内容过于堆积,因此咱们就期待下一篇吧。