Java Agent简介
什么是Java Agent
Java Agent本质上可以理解为一个插件,该插件就是一个特制的Jar包。这个Jar包通过JVMTI(JVM Tool Interface)完成加载,最终借助JPLISAgent(Java Programming Language Instrumentation Services Agent)完成对目标代码的修改
如何实现一个Java Agent
实现Agent启动方法
Java Agent
支持目标JVM启动时加载以及JVM运行时加载,这两种不同的加载模式会使用不同的入口函数。
如果需要在目标JVM启动的同时加载 Agent
,那么可以选择实现下面的方法
public static void premain(String agentArgs, Instrumentation inst)
public static void premain(String agentArgs)
如果需要在目标JVM运行时加载 Agent
,那么可以选择实现下面的方法
public static void agentmain(String agentArgs, Instrumentation inst)
public static void agentmain(String agentArgs)
JVM
将首先寻找1,如果没有发现1,再寻找2。代码中有两个参数需要注意
agentArgs:-javaagent 命令携带的参数。agent.service_name 这个配置项的默认值有三种覆盖方式,其中,使用探针配置进行覆盖,探针配置的值就是通过该参数传入的
inst:java.lang.instrumen.Instrumentation
是 Instrumention 包中定义的一个接口,它提供了操作类定义的相关方法,核心方法如下
- addTransformer()/removeTransformer():注册/注销一个 ClassFileTransformer 类的实例,该 Transformer 会在类加载的时候被调用,可用于修改类定义
- redefineClasses():该方法针对的是已经加载的类,它会对传入的类进行重新定义
- getAllLoadedClasses():返回当前 JVM 已加载的所有类
- getInitiatedClasses():返回当前 JVM 已经初始化的类
- getObjectSize():获取参数指定的对象的大小
指定Main-Class
定义一个 MANIFEST.MF
文件,在其中添加 premain-class
或 Agent-Class
配置项
Manifest-Version: 1.0
PreMain-Class: com.test.AgentClass
Agent-Class: com.test.AgentClass
Agent加载
- 启动时加载:启动参数增加-javaagent:[path],其中path为对应的agent的jar包路径
- 运行中加载:使用
com.sun.tools.attach.VirtualMachine
加载
SkyWalking Agent启动流程
核心流程
skywalking的agent入口函数在org.apache.skywalking.apm.agent.SkywalkingAgent#premain
方法具体实现如下(省略了异常代码块及日志打印)
final PluginFinder pluginFinder;
// 初始化配置信息
SnifferConfigInitializer.initializeCoreConfig(agentArgs);
// 加载agent插件, 并使用PluginFinder为插件进行分类
pluginFinder = new PluginFinder(new PluginBootstrap().loadPlugins());
// 创建一个ByteBuddy对象用于修改字节码
final ByteBuddy byteBuddy = new ByteBuddy().with(TypeValidation.of(Config.Agent.IS_OPEN_DEBUGGING_CLASS));
// 创建一个AgentBuilder对象,详细代码省略...
AgentBuilder agentBuilder = new AgentBuilder(byteBuddy)...
// 使用JDK SPI加载并启动BootService
ServiceManager.INSTANCE.boot();
// 添加一个JVM勾子函数, 在JVM退出时关闭所有BootService服务
Runtime.getRuntime()
.addShutdownHook(new Thread(ServiceManager.INSTANCE::shutdown, "skywalking service shutdown thread"));
核心启动流程如下图所示
初始化配置
配置初始化的入口方法在
org.apache.skywalking.apm.agent.core.conf.SnifferConfigInitializer
#initializeCoreConfig
方法的具体实现如下(省略了异常代码块及日志打印)
AGENT_SETTINGS = new Properties();
// 1.加载agent.config配置文件
final InputStreamReader configFileStream = loadConfig();
AGENT_SETTINGS.load(configFileStream);
for (String key : AGENT_SETTINGS.stringPropertyNames()) {
String value = (String) AGENT_SETTINGS.get(key);
// 2.解析agent.config文件中的配置
AGENT_SETTINGS.put(key, PropertyPlaceholderHelper.INSTANCE.replacePlaceholders(value, AGENT_SETTINGS));
}
// 3.解析skywalking.开头的系统参数,截取后写入配置类
overrideConfigBySystemProp();
agentOptions = StringUtil.trim(agentOptions, ',');
if (!StringUtil.isEmpty(agentOptions)) {
agentOptions = agentOptions.trim();
// 4.解析Java Agent参数, 写入配置类
overrideConfigByAgentOptions(agentOptions);
}
// 5.将配置类中的信息填充到Config类对应的静态字段中
initializeConfig(Config.class);
IS_INIT_COMPLETED = true;
可以看到配置信息会依次从三个地方进行获取并覆盖
agent.config
文件- 系统参数
javaagent options
所以,配置参数的优先级为 javaagent options > 系统参数 > agent.config文件
下面通过在3个地方同时配置 agent.service_name
参数来验证一下配置加载流程
以 Live-Demo + skywalking源码
项目为例
修改项目 ProjectB
的JVM启动参数,同时配置 javaagent options
、系统参数
-javaagent:/Users/wangbo/Documents/Study_Workspace/skywalking/skywalking-agent/skywalking-agent.jar=agent.service_name=Project_Options
-Dskywalking.agent.service_name=Project_Env
覆盖 agent.config
中指定参数的的环境变量
SW_AGENT_NAME=Project_Config
然后debug启动 projectB
进行源码调试
加载agent.config配置文件
private static final String SPECIFIED_CONFIG_PATH = "skywalking_config";
private static final String DEFAULT_CONFIG_FILE_NAME = "/config/agent.config";
String specifiedConfigPath = System.getProperty(SPECIFIED_CONFIG_PATH);
File configFile = StringUtil.isEmpty(specifiedConfigPath) ? new File(
AgentPackagePath.getPath(), DEFAULT_CONFIG_FILE_NAME) : new File(specifiedConfigPath);
源码中第一步为加载 agent.config
配置文件,可以看出agent.config
文件加载的优先级为
环境变量 skywalking_config
指定的路径 > skywalking-agent.jar
同级的config目录
解析agent.config配置
第一步agent.config文件加载到properties后,默认的配置格式都是 **配置项 = ${环境变量:配置默认值}**
第二步将对具体的值进行解析,首先判断环境变量中是否包含该配置,有则替换,无则使用默认值,所以解析后的配置格式变为 **配置项 = 配置值**
。可以看到 agent.service_name
对应的值为Project_Config
解析系统参数配置![在这里插入图片描述](https://img-blog.csdnimg.cn/038fc63f6c404d05b205326880904529.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3diNDkyNzU5OA==,size_16,color_FFFFFF,t_70)
第三步为解析系统参数,可以看到 agent.service_name
被覆盖为 Project_Env
解析javaagent options配置
第四步为解析javaagent options参数,可以看到agent.service_name
被覆盖为Project_Options
将解析出来的配置全部填充到Conig静态配置类中
配置加载的最终目的就是将配置项全部填充到Config配置类的各个静态字段中,这样后续使用配置信息时直接通过Config类进行获取即可,可以看到之前配置的 agent.service_name
在最后一步被确定为 Project_Options
插件加载
加载插件的入口方法在
org.apache.skywalking.apm.agent.core.plugin.PluginBootstrap#loadPlugins
方法的具体实现如下
// 自定义类加载器初始化
AgentClassLoader.initDefaultLoader();
// 创建插件定义解析类
PluginResourcesResolver resolver = new PluginResourcesResolver();
// 通过调用AgentClassLoader的资源加载方法解析出所有插件定义文件的路径