Skywalking原理
4.1 java agent原理
上文中我们知道,要使用Skywalking去监控服务,需要在其 VM 参数中添加 “-javaagent:/usr/local/skywalking/apache-skywalking-apm-bin/agent/skywalking-agent.jar"。
这里就
使用到了java agent技术。
Java agent 是什么?
Java agent是java命令的一个参数。参数 javaagent 可以用于指定一个 jar 包。
1. 这个 jar 包的 MANIFEST.MF 文件必须指定 Premain-Class 项。
2. Premain-Class 指定的那个类必须实现 premain() 方法。
当Java 虚拟机启动时,在执行 main 函数之前,JVM 会先运行-javaagent 所指定 jar 包内 Premain-
Class 这个类的 premain 方法 。
如何使用java agent?
使用 java agent 需要几个步骤:
3. 定义一个 MANIFEST.MF 文件,必须包含 Premain-Class 选项,通常也会加入Can-Redefine-
Classes 和 Can-Retransform-Classes 选项。
4. 创建一个Premain-Class 指定的类,类中包含 premain 方法,方法逻辑由用户自己确定。
5. 将 premain 的类和 MANIFEST.MF 文件打成 jar 包。
6. 使用参数 -javaagent: jar包路径 启动要代理的方法。
4.1.1 搭建java agent工程
使用maven创建java_agent_demo工程:
在java文件夹下新建PreMainAgent类:
import java.lang.instrument.Instrumentation;
public class PreMainAgent {
/**
* 在这个 premain 函数中,开发者可以进行对类的各种操作。
* 1、agentArgs 是 premain 函数得到的程序参数,随同 “– javaagent”一起传入。与 main
函数不同的是,
* 这个参数是一个字符串而不是一个字符串数组,如果程序参数有多个,程序将自行解析这个字符串。
* 2、Inst 是一个 java.lang.instrument.Instrumentation 的实例,由 JVM 自动传入。*
* java.lang.instrument.Instrumentation 是 instrument 包中定义的一个接口,也是这
个包的核心部分,
* 集中了其中几乎所有的功能方法,例如类定义的转换和操作等等。
类中提供两个静态方法,方法名均为premain,不能拼错。
在pom文件中添加打包插件:
* @param agentArgs
* @param inst
*/
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("=========premain方法执行1========");
System.out.println(agentArgs);
}
/**
* 如果不存在 premain(String agentArgs, Instrumentation inst)
* 则会执行 premain(String agentArgs)
* @param agentArgs
*/
public static void premain(String agentArgs) {
System.out.println("=========premain方法执行2========");
System.out.println(agentArgs);
}
}
类中提供两个静态方法,方法名均为premain,不能拼错。
在pom文件中添加打包插件:
<build>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<appendAssemblyId>false</appendAssemblyId>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<!--自动添加META-INF/MANIFEST.MF -->
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Premain-Class>com.itcast.PreMainAgent</Premain-Class>
<Agent-Class>com.itcast.PreMainAgent</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
该插件会在自动生成META-INF/MANIFEST.MF文件时,帮我们添加agent相关的配置信息。
使用maven的package命令进行打包:
打包成功之后,复制打包出来的jar包地址。
4.1.2 搭建主工程
使用maven创建java_agent_user工程:
Main类代码:
先运行一次,然后点击编辑MAIN启动类:
public class Main {
public static void main(String[] args) {
System.out.println("Hello World");
}
}
代码为
-javaagent:路径\java-agent-demo-1.0-SNAPSHOT.jar=HELLOAGENT
启动时加载javaagent,指向上一节中编译出来的java agent工程jar包地址,同时在最后追加参数HELLOAGENT。
运行MAIN方法,查看结果:
可以看到java agent的代码优先于MAIN函数的方法运行,证明java agent运行正常。
4.1.3 统计方法调用时间
Skywalking中对每个调用的时长都进行了统计,这一小节中我们会使用ByteBuddy和Java agent技术来统计方法的调用时长。
-javaagent:路径\java-agent-demo-1.0-SNAPSHOT.jar=HELLOAGENT
Byte Buddy是开源的、基于Apache 2.0许可证的库,它致力于解决字节码操作和instrumentation API的复杂性。Byte Buddy所声称的目标是将显式的字节码操作隐藏在一个类型安全的领域特定语言背后。通过使用Byte Buddy,任何熟悉Java编程语言的人都有望非常容易地进行字节码操作。ByteBuddy提供了额外的API来生成Java agent,可以轻松的增强我们已有的代码。
添加依赖:
<dependencies>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.10.16</version>
</dependency>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy-agent</artifactId>
<version>1.10.16</version>
</dependency>
</dependencies>
修改PreMainAgent代码:
public static void premain(String agentArgs, Instrumentation inst) {
//创建一个转换器,转换器可以修改类的实现
//ByteBuddy对java agent提供了转换器的实现,直接使用即可
AgentBuilder.Transformer transformer = new AgentBuilder.Transformer() {
public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder, TypeDescription typeDescription,
ClassLoader classLoader, JavaModule javaModule) {
return builder
// 拦截任意方法
.method(ElementMatchers.<MethodDescription>any())
// 拦截到的方法委托给TimeInterceptor
.intercept(MethodDelegation.to(MyInterceptor.class));
}
};
new AgentBuilder // Byte Buddy专门有个AgentBuilder来处理Java Agent的场景
.Default()
// 根据包名前缀拦截类
.ignore(
nameStartsWith("net.bytebuddy.")
.or(nameStartsWith("org.slf4j."))
.or(nameStartsWith("org.groovy."))
.or(nameContains("javassist"))
.or(nameContains(".asm."))
.or(nameContains(".reflectasm."))
.or(nameStartsWith("sun.reflect"))
.or(ElementMatchers.isSynthetic()))
.type(ElementMatchers.nameStartsWith("org.springframework.kafka.core").or(ElementMatchers.nameStartsWith("org.apache.kafka")).or(ElementMatchers.nameStartsWith("com.itcast")))
// 拦截到的类由transformer处理
.transform(transformer)
.installOn(inst);
}
先生成一个转换器,ByteBuddy提供了java agent专用的转换器。通过实现Transformer接口利用builder对象来创建一个转换器。转换器可以配置拦截方法的格式,比如用名称,本例中拦截所有方法,并定义一个拦截器类MyInterceptor。
创建完拦截器之后可以通过Byte Buddy的AgentBuilder建造者来构建一个agent对象。AgentBuilder可
以对指定的包名前缀来生效,同时需要指定转换器对象。
MyInterceptor类:
package com.itcast;
import net.bytebuddy.implementation.bind.annotation.Origin;
import net.bytebuddy.implementation.bind.annotation.RuntimeType;
import net.bytebuddy.implementation.bind.annotation.SuperCall;
import java.lang.reflect.Method;
import java.util.concurrent.Callable;
public class MyInterceptor {
@RuntimeType
public static Object intercept(@Origin Method method,
@SuperCall Callable<?> callable) {
System.out.println("MyInterceptor start...");
long start = System.currentTimeMillis();
try {
//执行原方法
try {
if (callable == null) {
return null;
}
return callable.call();
} catch (Exception e) {
System.out.println("MyInterceptor----Exception" + e.toString());
return null;
}
} finally {
//打印调用时长
System.out.println(
method.getDeclaringClass() + "---" + method.getName() + ":" + (System.currentTimeMillis() - start)
+ "ms");
System.out.println("MyInterceptor end...");
}
}
}
MyInterceptor就是一个拦截器的实现,统计的调用的时长。参数中的method是反射出的方法对象,而callable就是调用对象,可以通过callable.call()方法来执行原方法。重新打包,执行maven package命令。接下来修改主工程代码。主工程将Main类放置到com.agent 包下。修改代码内容为:
package com.agent;
public class Main {
public static void main(String[] args) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Hello World");
}
}
休眠1秒,使统计时长的演示效果更好一些。执行main方法之后显示结果:
我们在没有修改代码的情况下,利用java agent和Byte Buddy统计出了方法的时长,Skywalking的agent也是基于这些技术来实现统计调用时长。
请参考黑马程序员文档 https://github.com/heheliu321/jvm/tree/master/java_agent_user