前言
1.工作原因,使用jvm-sandbox比较多,遂进行源码分析,做到知己知彼,个人能力有限,如有错误,欢迎指正。
2.关于jvm-sandbox 是什么,如何安装相关环境,可移步 官方文档
3.源码分析基于jvm-sandbox 最新的master代码,tag-1.2.1。
4.暂定计划通过启动简析,加载模块,刷新模块,卸载模块,激活模块等方面入手,通过几篇文章覆盖jvm-sandbox关键流程。
Jvm-Sandbox源码分析--启动简析
Jvm-Sandbox源码分析--启动时加载模块
Jvm-Sandbox源码分析--增强目标类
Jvm-Sandbox源码分析--模块刷新和卸载
启动
attach方式启动
sh sandbox/bin/sandbox.sh -p pid
脚本分析
简单看一下启动脚本sandbox.sh
# the sandbox main function
function main() {
check_permission
while getopts "hp:vFfRu:a:A:d:m:I:P:ClSn:X" ARG
do
case ${ARG} in
h) usage;exit;;
p) TARGET_JVM_PID=${OPTARG};;
v) OP_VERSION=1;;
l) OP_MODULE_LIST=1;;
R) OP_MODULE_RESET=1;;
F) OP_MODULE_FORCE_FLUSH=1;;
f) OP_MODULE_FLUSH=1;;
u) OP_MODULE_UNLOAD=1;ARG_MODULE_UNLOAD=${OPTARG};;
a) OP_MODULE_ACTIVE=1;ARG_MODULE_ACTIVE=${OPTARG};;
A) OP_MODULE_FROZEN=1;ARG_MODULE_FROZEN=${OPTARG};;
d) OP_DEBUG=1;ARG_DEBUG=${OPTARG};;
m) OP_MODULE_DETAIL=1;ARG_MODULE_DETAIL=${OPTARG};;
I) TARGET_SERVER_IP=${OPTARG};;
P) TARGET_SERVER_PORT=${OPTARG};;
C) OP_CONNECT_ONLY=1;;
S) OP_SHUTDOWN=1;;
n) OP_NAMESPACE=1;ARG_NAMESPACE=${OPTARG};;
X) set -x;;
?) usage;exit_on_err 1;;
esac
done
reset_for_env
# reset IP
[ -z ${TARGET_SERVER_IP} ] && TARGET_SERVER_IP="${DEFAULT_TARGET_SERVER_IP}";
# reset PORT
[ -z ${TARGET_SERVER_PORT} ] && TARGET_SERVER_PORT=0;
# reset NAMESPACE
[[ ${OP_NAMESPACE} ]] \
&& TARGET_NAMESPACE=${ARG_NAMESPACE}
[[ -z ${TARGET_NAMESPACE} ]] \
&& TARGET_NAMESPACE=${DEFAULT_NAMESPACE}
if [[ ${OP_CONNECT_ONLY} ]]; then
[[ 0 -eq ${TARGET_SERVER_PORT} ]] \
&& exit_on_err 1 "server appoint PORT (-P) was missing"
SANDBOX_SERVER_NETWORK="${TARGET_SERVER_IP};${TARGET_SERVER_PORT}"
else
# -p was missing
[[ -z ${TARGET_JVM_PID} ]] \
&& exit_on_err 1 "PID (-p) was missing.";
attach_jvm
fi
...省略代码...
...省略代码...
# default
sandbox_curl "sandbox-info/version"
exit
}
通过脚本源码,我们可以看到在执行sandbox.sh的时候,会先执行reset_for_env方法
reset_for_env()
{
#使用默认环境变量 JAVA_HOME
# use the env JAVA_HOME for default
[[ ! -z ${JAVA_HOME} ]] \
&& SANDBOX_JAVA_HOME="${JAVA_HOME}"
# 或者通过TARGET_JVM_PID查找 设置sandbox环境变量
# use the target JVM for SANDBOX_JAVA_HOME
[[ -z ${SANDBOX_JAVA_HOME} ]] \
&& SANDBOX_JAVA_HOME="$(\
ps aux\
|grep ${TARGET_JVM_PID}\
|grep java\
|awk '{print $11}'\
|xargs ls -l\
|awk '{if($1~/^l/){print $11}else{print $9}}'\
|sed 's/\/bin\/java//g'\
)"
[[ ! -x "${SANDBOX_JAVA_HOME}" ]] \
&& exit_on_err 1 "permission denied, ${SANDBOX_JAVA_HOME} is not accessible! please set JAVA_HOME"
[[ ! -x "${SANDBOX_JAVA_HOME}/bin/java" ]] \
&& exit_on_err 1 "permission denied, ${SANDBOX_JAVA_HOME}/bin/java is not executable!"
#判断 JVM 版本
# check the jvm version, we need 6+
local JAVA_VERSION=$("${SANDBOX_JAVA_HOME}/bin/java" -version 2>&1|awk -F '"' '/version/&&$2>"1.5"{print $2}')
[[ -z ${JAVA_VERSION} ]] \
&& exit_on_err 1 "illegal java version: ${JAVA_VERSION}, please make sure target java process: ${TARGET_JVM_PID} run int JDK[6,11]"
#若 ${JAVA_HOME}/lib/tools.jar 存在,则通过 -Xbootclasspath/a 这个配置,将它加入 classpath 末尾,为执行 attach_jvm 方法做准备
[[ -f "${SANDBOX_JAVA_HOME}"/lib/tools.jar ]] \
&& SANDBOX_JVM_OPS="${SANDBOX_JVM_OPS} -Xbootclasspath/a:${SANDBOX_JAVA_HOME}/lib/tools.jar"
}
关键步骤:
- 1.使用默认环境变量 JAVA_HOME
- 2.或者通过TARGET_JVM_PID查找 设置sandbox环境变量
- 3.判断 JVM 版本是否符合要求
- 4.若 ${JAVA_HOME}/lib/tools.jar 存在,则通过 -Xbootclasspath/a 这个配置,将它加入 classpath 末尾,为执行 attach_jvm 方法做准备
然后再执行attach_jvm方法
# attach sandbox to target JVM
# return : attach jvm local info
function attach_jvm() {
# got an token
local token=`date |head|cksum|sed 's/ //g'`
# attach target jvm
# 通过java -jar 命令启动 sandbox-core.jar 并传递参数 1. TARGET_JVM_PID 2. sandbox-agent.jar 3. 启动要用到的数据信息
"${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."
# get network from attach result
SANDBOX_SERVER_NETWORK=$(grep ${token} ${SANDBOX_TOKEN_FILE}|grep ${TARGET_NAMESPACE}|tail -1|awk -F ";" '{print $3";"$4}');
[[ -z ${SANDBOX_SERVER_NETWORK} ]] \
&& exit_on_err 1 "attach JVM ${TARGET_JVM_PID} fail, attach lose response."
}
关键步骤:
- 通过java -jar 命令启动 sandbox-core.jar 并传递参数 1. TARGET_JVM_PID 2. sandbox-agent.jar 3. 启动要用到的数据信息
代码分析
我们来看sandbox-core 这个moudle
在pom文件中存在插件配置如下,通过mainClass 指定了这个主函数,所以我们通过java -jar sandbox-core.jar命令会执行这个函数
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>attached</goal>
</goals>
<phase>package</phase>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifest>
<mainClass>com.alibaba.jvm.sandbox.core.CoreLauncher</mainClass>
</manifest>
</archive>
</configuration>
</execution>
</executions>
</plugin>
我们再看这个CoreLauncher这个类的main方法
关键步骤:
- 1.attach pid
- 2.load sandbox-angent.jar
/**
* 内核启动程序
*
* @param args 参数
* [0] : PID
* [1] : agent.jar's value
* [2] : token
*/
public static void main(String[] args) {
try {
// check args
if (args.length != 3
|| StringUtils.isBlank(args[0])
|| StringUtils.isBlank(args[1])
|| StringUtils.isBlank(args[2])) {
throw new IllegalArgumentException("illegal args");
}
new CoreLauncher(args[0], args[1], args[2]);
} catch (Throwable t) {
t.printStackTrace(System.err);
System.err.println("sandbox load jvm failed : " + getCauseMessage(t));
System.exit(-1);
}
}
public CoreLauncher(final String targetJvmPid,
final String agentJarPath,
final String token) throws Exception {
// 加载agent
attachAgent(targetJvmPid, agentJarPath, token);
}
// 加载Agent
private void attachAgent(final String targetJvmPid,
final String agentJarPath,
final String cfg) throws Exception {
VirtualMachine vmObj = null;
try {
//attach 目标 pid
vmObj = VirtualMachine.attach(targetJvmPid);
if (vmObj != null) {
//通过vm类 加载sandbox-agent.jar
vmObj.loadAgent(agentJarPath, cfg);
}
} finally {
if (null != vmObj) {
vmObj.detach();
}
}
}
我们可以看到在attach pid 之后加载了sandbox-agent.jar
接下来我们看一下sandbox-agent.jar
和sandbox-core.jar的pom文件类似,agent这个模块也通过maven插件配置了Premain-Class和Agent-Class两个参数,并且都指向AgentLauncher这个类
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>attached</goal>
</goals>
<phase>package</phase>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<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>
</archive>
</configuration>
</execution>
</executions>
</plugin>
接下来我们看一下AgentLauncher
通过attach pid方式执行,会调用这个类中的agentmain方法,
如果大家不明白为何通过maven插件配置后,便会和指定类中的某个方法进行关联,可以参考一下这篇文章 和 这篇文章
/**
* 动态加载
*
* @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)
);
}
关键步骤:
- 1.组装通过脚本命令 attach_jvm()方法传过来的参数featureString
home=/Users/zhengmaoshao/sandbox/bin/..;token=341948577048;server.ip=0.0.0.0;server.port=0;namespace=default
- 2.writeAttachResult() 方法是写了一些数据到/Users/zhengmaoshao/.sandbox.token 这个文件
default;226298528348;0.0.0.0;55060
java agent方式启动
在应用服务启动脚本中添加:
java -javaagent:/yourpath/sandbox/lib/sandbox-agent.jar
通过javaagent方式启动会调用AgentLauncher类中的premain方法
/**
* 启动加载
*
* @param featureString 启动参数
* [namespace,prop]
* @param inst inst
*/
public static void premain(String featureString, Instrumentation inst) {
LAUNCH_MODE = LAUNCH_MODE_AGENT;
install(toFeatureMap(featureString), inst);
}
到这一步我们可以很清楚的看到,不管是通过attach pid的方式还是通过javaagent的方式进行启动,最终都会执行install这个方法,
install方法做了什么事情。
1.启动类加载器加载sandbox-spy.jar
SANDBOX_SPY_JAR_PATH=/Users/zhengmaoshao/sandbox/bin/../lib/sandbox-spy.jar
// 将Spy注入到BootstrapClassLoader
inst.appendToBootstrapClassLoaderSearch(new JarFile(new File(
getSandboxSpyJarPath(getSandboxHome(featureMap))
// SANDBOX_SPY_JAR_PATH
)));
2.构造自定义的类加载器,实现代码隔离
SANDBOX_CORE_JAR_PATH=/Users/zhengmaoshao/sandbox/bin/../lib/sandbox-core.jar
// 构造自定义的类加载器,尽量减少Sandbox对现有工程的侵蚀
final ClassLoader sandboxClassLoader = loadOrDefineClassLoader(
namespace,
getSandboxCoreJarPath(getSandboxHome(featureMap))
// SANDBOX_CORE_JAR_PATH
);
3.实例化sandbox-core.jar中的CoreConfigure内核启动配置类
CoreConfigure内核启动配置类内容:
// CoreConfigure类定义
final Class<?> classOfConfigure = sandboxClassLoader.loadClass(CLASS_OF_CORE_CONFIGURE);
// 反序列化成CoreConfigure类实例
final Object objectOfCoreConfigure = classOfConfigure.getMethod("toConfigure", String.class, String.class)
.invoke(null, coreFeatureString, propertiesFilePath);
4.获取sandbox-core.jar中的ProxyCoreServer对象实例,注意这里真正被实例化的其实JettyCoreServer
// CoreServer类定义
final Class<?> classOfProxyServer = sandboxClassLoader.loadClass(CLASS_OF_PROXY_CORE_SERVER);
// 获取CoreServer单例
final Object objectOfProxyServer = classOfProxyServer
.getMethod("getInstance")
.invoke(null);
...省略代码...
//ProxyCoreServer
public static CoreServer getInstance() {
try {
return new ProxyCoreServer(
(CoreServer) classOfCoreServerImpl
.getMethod("getInstance")
.invoke(null)
);
} catch (Throwable cause) {
throw new RuntimeException(cause);
}
}
...省略代码...
//JettyCoreServer
/**
* 单例
*
* @return CoreServer单例
*/
public static CoreServer getInstance() {
if (null == coreServer) {
synchronized (CoreServer.class) {
if (null == coreServer) {
coreServer = new JettyCoreServer();
}
}
}
return coreServer;
}
5.调用JettyCoreServer bind方法开始进入启动httpServer流程
// CoreServer.isBind()
final boolean isBind = (Boolean) classOfProxyServer.getMethod("isBind").invoke(objectOfProxyServer);
// 如果未绑定,则需要绑定一个地址
if (!isBind) {
try {
classOfProxyServer
.getMethod("bind", classOfConfigure, Instrumentation.class)
.invoke(objectOfProxyServer, objectOfCoreConfigure, inst);
} catch (Throwable t) {
classOfProxyServer.getMethod("destroy").invoke(objectOfProxyServer);
throw t;
}
}
接下来我们看一下启动httpServer过程中都做了什么事情
- 1.初始化logback日志框架
//初始化logback日志框架
LogbackUtils.init(
cfg.getNamespace(),
cfg.getCfgLibPath() + File.separator + "sandbox-logback.xml"
);
- 2.创建一个沙箱对象
JvmSandbox构造方法中的关键步骤:
2.1获取事件处理类实例
2.2初始化模块管理实例
- 2.2.1这里面通过new DefaultProviderManager(cfg)对默认服务提供管理器实现进行实例化。
主要是创建了一个针对服务提供库sandbox-mgr-provider.jar的ClassLoader,sandbox-mgr-provider中的类通过JAVA SPI的方式实现可扩展性 - 2.2.2 初始化模块目录,包括/Users/zhengmaoshao/sandbox/bin/../module文件夹中系统模块和/Users/zhengmaoshao/.sandbox-module文件夹中的用户自定义模块
2.3初始化Spy类
public JvmSandbox(final CoreConfigure cfg,
final Instrumentation inst) {
//获取事件处理类实例
EventListenerHandlers.getSingleton();
this.cfg = cfg;
//初始化模块管理实例
this.coreModuleManager = new DefaultCoreModuleManager(
cfg,
inst,
new DefaultLoadedClassDataSource(inst, cfg.isEnableUnsafe()),
new DefaultProviderManager(cfg)
);
//初始化Spy类
init();
}
3.初始化Jetty's ContextHandler,启动httpServer
//JettyCoreServer
private void initHttpServer() {
final String serverIp = cfg.getServerIp();
final int serverPort = cfg.getServerPort();
// 如果IP:PORT已经被占用,则无法继续被绑定
// 这里说明下为什么要这么无聊加个这个判断,让Jetty的Server.bind()抛出异常不是更好么?
// 比较郁闷的是,如果这个端口的绑定是"SO_REUSEADDR"端口可重用的模式,那么这个server是能正常启动,但无法正常工作的
// 所以这里必须先主动检查一次端口占用情况,当然了,这里也会存在一定的并发问题,BUT,我认为这种概率事件我可以选择暂时忽略
if (isPortInUsing(serverIp, serverPort)) {
throw new IllegalStateException(format("address[%s:%s] already in using, server bind failed.",
serverIp,
serverPort
));
}
httpServer = new Server(new InetSocketAddress(serverIp, serverPort));
if (httpServer.getThreadPool() instanceof QueuedThreadPool) {
final QueuedThreadPool qtp = (QueuedThreadPool) httpServer.getThreadPool();
qtp.setName("sandbox-jetty-qtp-" + qtp.hashCode());
}
}
//初始化Jetty's ContextHandler
private void initJettyContextHandler() {
final String namespace = cfg.getNamespace();
final ServletContextHandler context = new ServletContextHandler(NO_SESSIONS);
final String contextPath = "/sandbox/" + namespace;
context.setContextPath(contextPath);
context.setClassLoader(getClass().getClassLoader());
// web-socket-servlet
final String wsPathSpec = "/module/websocket/*";
logger.info("initializing ws-http-handler. path={}", contextPath + wsPathSpec);
//noinspection deprecation
context.addServlet(
new ServletHolder(new WebSocketAcceptorServlet(jvmSandbox.getCoreModuleManager())),
wsPathSpec
);
// module-http-servlet
final String pathSpec = "/module/http/*";
logger.info("initializing http-handler. path={}", contextPath + pathSpec);
context.addServlet(
new ServletHolder(new ModuleHttpServlet(jvmSandbox.getCoreModuleManager())),
pathSpec
);
httpServer.setHandler(context);
}
//最终启动httpServer
httpServer.start();
最后初始化加载所有的模块,详情后续分析
// 初始化加载所有的模块 后续分析
try {
jvmSandbox.getCoreModuleManager().reset();
} catch (Throwable cause) {
logger.warn("reset occur error when initializing.", cause);
}
//这里校验httpServer是否启动成功
final InetSocketAddress local = getLocal();
logger.info("initialized server. actual bind to {}:{}",
local.getHostName(),
local.getPort()
);
启动了一个jetty服务之后,后续我们的加载,卸载,等命令操作都会通过http请求的方式进行。