jvm-sandbox源码解析
jvm如何在运行时动态的修改字节码内容呢?
javaagent机制分为两种加载方式
命令行启动: 会根据META-INF文件,找到PERMAIN-CLASS配置的类,去执行类对应的permain方法
attach(远程): 会根据META-INF文件,找到AGENTMAIN-CLASS配置的类,去执行类对应的agentmain方法
下面都以attach方式启动为说明

先看sandbox.sh启动脚本,启动的是sandbox-core.jar,并在其后传了很多参数,分别是目标的jvm进程id,sandbox-agent.jar的目录,sandbox的主目录,目标的jvm所在服务器ip,目标的jvm所在服务器端口,命名空间
接下来看下sandbox-core.jar的入口类,在其pom文件中配置的是com.alibaba.jvm.sandbox.core.CoreLauncher

该类main方法最终了执行了这个方法,初始化了VirtualMachine类,并且启动脚本传入的sandbox-agent.jar类和一些配置参数传给了远程jvm进行加载
接下来去看sandbox-agent做了哪些操作,这时候sandbox-agent.jar类所有的操作都已经是在远程jvm中生效了

在其pom文件中配置了agent-class为com.alibaba.jvm.sandbox.agent.AgentLauncher,那么由于是attach方式,将会执行这个类的agentmain方法
核心是这个方法的install方法,它主要做了以下几件事
- 根据启动脚本传入的jvm-sandbox主目录,将其lib下的sandbox-spy.jar交由远程jvm的启动类加载器进行加载,主要是加载Spy类
- 构造自定义的类加载器SandboxClassLoader
- 使用SandboxClassLoader加载com.alibaba.jvm.sandbox.core.CoreConfigure
- 然后反射调用CoreConfigure的toConfigure方法,并传入启动脚本传入的参数和配置的propertiesPath,构造CoreConfigure对象
- 使用SandboxClassLoader加载com.alibaba.jvm.sandbox.core.server.ProxyCoreServer
- 反射调用ProxyCoreServer的getInstance方法,获取ProxyCoreServer实例
- 然后调用ProxyCoreServer的isBind方法判断是否已经绑定
- 如果isBind方法调用返回false的话,则反射调用bind方法执行后续逻辑(核心流程)

ProxyCoreServer是一个包装类,在其getInstance方法内部调用了JettyCoreServer的getInstance方法,初始化了JettyCoreServer对象,所以bind方法实际调用的是JettyCoreServer的bind方法
bind方法的执行流程如下
- 使用Initializer类进行一些初始化(后续详细展开),初始化了JvmSandbox类,初始化了HttpServer,并配置了一些接口
- 调用JvmSandbox的CoreModuleManager初始化模块
先看JvmSandbox类的初始化
- 初始化EventListenerHandler类,这是个单例类
- 将CoreConfigure配置类赋值给成员变量cfg
- 使用静态代理模式代理了DefaultCoreModuleManager对象(这个看就是在调用每个方法之前计数了一下调用次数),在初始化DefaultCoreModuleManager对象时,传入构造函数时同时初始化了DefaultCoreLoadedClassDataSource和DefaultProviderManager对象,DefaultCoreLoadedClassDataSource对象传入了配置赋值给成员变量,DefaultProviderManager对象传入了配置,根据配置的provider路径,加载其下的jar包,并将配置类对象注入给jar包里的ModuleJarLoadingChain和ModuleLoadingChain类中带有Resource注解的ConfigInfo字段(或者字段是ConfigInfo类的父类)
- 提前加载一些必要的类,com.alibaba.jvm.sandbox.core.util.SandboxClassUtils和com.alibaba.jvm.sandbox.core.util.matcher.structure.ClassStructureImplByAsm
- 调用SpyUtils的init方法,传入命名空间,本质上是给spy类传入EventListenerHandler类
再看HttpServer的初始化

就是新建了一个Jetty对象,根据启动脚本传入的目标的jvm所在服务器ip,目标的jvm所在服务器端口进行初始化

为服务器初始化了几个接口
然后就启动了这个jetty服务
接下来看CoreModuleManager初始化模块流程
- 首先卸载了所有模块
- 然后遍历moduleLibDirArray(该集合是DefaultCoreModuleManager初始化时将配置的sytem_module和user_module路径下所有jar路径组成的一个集合),判断路径存在且可读,就调用ModuleLibLoader类的load方法去加载module,初始化了InnerModuleJarLoadCallback和InnerModuleLoadCallback两个回调

首先回调InnerModuleJarLoadCallback的onload方法,传入module jar包路径

再调用DefaultProviderManager对象的loading方法,传入module jar包路径
遍历moduleJarLoadingChains集合,并调用每一个方法的loading方法(此处不知道干嘛,暂时不管了,且该集合也是之前初始化DefaultProviderManager类时遍历provider路径时加入的)
再就是调用ModuleJarLoader的load方法,传入module jar路径和配置信息,并传入InnerModuleLoadCallback回调

使用ModuleJarClassLoader类加载加载module jar,然后取出其中所有的Module对象,进行遍历.首先判断是否加了Information注解,没有加则跳过.再取出Information注解上配置的id值,如果没配置也跳过.再判断当前环境的注入模式与Information上配置的是否一致(agent和attach方式要保持一致),如果不一致也跳过.然后回调InnerModuleLoadCallback的onload方法,传入唯一id,module的class对象,module对象,module jar路径的文件对象.ModuleJarClassLoader类加载器.
再看InnerModuleLoadCallback的onload方法

首先判断这个id的module是否已经加载过,如果已经加载过,则跳过.
然后遍历DefaultProviderManager类的moduleLoadingChains集合,调用其loading方法,传入一些信息.
再调用load方法去加载module

再判断一下id,看module是否已经加载过,加载过,跳过(这一块重复判断了下,应该是为了防止在上面方法执行期间,有其他线程已经初始化了,这个方法加了 synchronized,所以这个方法是不会有多个线程同时执行的)
初始化了CoreModule对象,传入了唯一id,module的class对象,module对象和注入模式.
然后调用injectResourceOnLoadIfNecessary方法,该方法反射获取了module类的所有加了Resource注解的字段,并根据字段的类型,将各种类对象设置为值.
然后回调Module的onLoad方法,标记Module状态为已加载.
再调用markActiveOnLoadIfNecessary方法,该方法判断Information的isActiveOnLoad值是否配置了为true(默认为true),是则调用active方法去激活Module(核心流程,后续详细展开)
以id为key,module对象为value,放入loadedModuleBOMap中,用于后续判断,防止重复加载.
然后如果module是LoadCompleted类的子类,则回调Module的loadCompleted方法.
接下来看Module是怎么active的

首先判断该module状态,如果已经激活过了,则直接返回.
然后回调module的onActive方法.
遍历sandboxClassFileTransformers集合,将sandboxClassFileTransformers的listenerId,eventListener和eventTypeArray传给EventListenerHandler,EventListenerHandler将这些信息放入了mappingOfEventProcessor集合,key为ListenerId,值为EventProcessor对象.
标记module状态为已激活.
上面有个sandboxClassFileTransformers集合,但是集合值是什么时候加进去的呢?其实在上面InnerModuleLoadCallback的injectResourceOnLoadIfNecessary里有进行判断,如果字段是ModuleEventWatcher类型的话,则会初始化一个DefaultModuleEventWatcher对象并赋值给这个字段.

该类通过jvm-sandbox为其注入DefaultModuleEventWatcher对象,并实现了LoadCompleted方法.在方法内构造了EventWatchBuilder对象,并传入了DefaultModuleEventWatcher,最终调用了onWatch方法,并传入的一个回调函数.

最终调用到了DefaultModuleEventWatcher的watch方法.
其构造了一个SandboxClassFileTransformer对象,并将其加入到了sandboxClassFileTransformers集合.
并将该SandboxClassFileTransformer加到了Instrumentation里,接下来jvm加载的类都会传递给SandboxClassFileTransformer的transform方法.
接下来获取到所有需要渲染的类,并计数影响的类数量和方法数量.如果EventWatchBuilder构造是传入了Progress对象,则会在渲染开始、期间、失败和结束回调Progress对象的对应方法,传入进度
在上面需要关注SandboxClassFileTransformer类的transform方法,是怎么回调的,其次需要关注是如何找到需要渲染的类的.
首先看SandboxClassFileTransformer的transform方法.

在transform方法内部,首先根据类的结构,去判断是否有自己需要的条件,如果没有,则直接跳过,如果有的话,则会通过asm字节码编织技术,去修改目标类的字节码,主要是通过EventWeaver这个类,在解析目标类字节码文件时,每次解析method时,都会调用EventWeaver的visitMethod方法,在该方法里,会判断方法名是否是自己需要修改的方法,如果是则对这个方法代码进行修改.修改其实就是在各个环节调用Spy类定义的那几个环节方法,并将对应参数值传入.
再看是如何找到渲染的类的

通过Instrumentation获取jvm加载的所有类,然后遍历类结构,判断类是否是自己需要渲染的目标类,如果是则加入集合,然后再后续通过Instrumentation类让目标类重新加载,一旦重新加载,就会走之前定义的SandboxClassFileTransformer的transform方法,然后对目标类的目标方法进行了修改,以此达到了加强目标类的目的.
jvm-sandbox源码解析