Java-JavaAgent

Skywalking实现

如果需要我们对每个方法的执行添加打印执行时间,一般可以通过AOP的方式来实现,对每个方法做环绕通知的切面,这种方式有一个问题就是,如果后续还有一些其他需求需要在方法执行前做统计,比如打印HTTPRequest中的请求地址,请求参数等信息,就需要重新编写aop,编译部署上线。

skywalking使用0侵入的方式来扩展Java程序的功能,具体实现就是使用了Java插桩技术。

字节码插桩

字节码插桩实际就是在代码运行期间对字节码进行修改,比如动态代理,或者java Agent

skywalking就是通过javaAgent实现的字节码增强,启动方式:

启动jar包时,需要添加额外的运行参数

1
-javaagent:D:\tool\apache-skywalking-apm-bin-es7\agent\skywalking-agent.jar -Dskywalking.agent.service_name=skywalking-learn -Dskywalking.collector.backend_service=192.168.80.128:11800

skywalking-agent.jar就是修改字节码的具体实现的包

JavaAgent

jdk1.5引入的新特性,在加载class前进行拦截,我们可以对拦截的class做一些修改操作,然后再加载这个class。

agent的启动方式

  • 静态启动
    • 使用-javaagent的方式,在应用启动时挂载agent
    • 主方法入口:premain()
    • 对字节码操作自由度高,只要符合字节码规范,JVM可以验证通过,可以对字节码做任何修改
    • SkyWalking
  • 动态附加
    • 在应用运行过程中通过attach API挂载Agent
    • 主方法入口:agentmain()
    • 对字节码操作自由度低,不能创建父类,接口,字段…
    • Arthas

javaAgent探针包要求在META-INF/MANIFEST.MF文件中,要有一个包含premain方法的入口类

public static void premain(String agentArgs, Instrumentation inst)

agentArgs:挂载agent是设置的参数

Instrumentation :可以获取一些类信息

  • addTransformer()/removeTransformer() :注册/注销一个 ClassFileTransformer 类的实例,该 Transformer 会在类加载的时候被调用,可用于修改类定义(修改类的字节码)。
  • redefineClasses() :重新定义传入已经加载的类。
  • getAllLoadedClasses():返回 JVM 已加载的所有类。
  • getInitiatedClasses() :返回 JVM 已经初始化的类。
  • getObjectSize():获取参数指定的对象的大小。

字节码操作工具

JavaAgent就是一个插桩入口,主要是做代理拦截的,真正做字节码修改的,是有一套操作字节码的工具,目前主要有:

asm,javassist,bytebuddy

  • asm
    • 支持任意字节码插入,功能强大,比较偏底层,学习难度大
  • javassist
    • java原始语法,支持jdk1.5的语法,后面的一些其他语法比如泛型等不支持
  • bytebuddy
    • 基于asm实现,通过一些API操作class,支持java任意版本,性能比javassist高
    • skywalking就是使用bytebuddy来实现的修改字节码

bytebuddy

定义一个最简单的探针包主要分为如下几个步骤

  • 创建一个探针maven项目
  • 编写探针入口类:public static void premain(String agentArgs, Instrumentation inst)
  • 编写拦截配置,需要拦截哪些类,可以根据全类名前缀,后缀,标记的注解等等
  • 编写拦截后的处理类
  • 处理类的方法参数中可以获取当前执行类的method,参数,调用类,父类对象等等
  • 在方法中编写需要增强的代码
  • 使用maven插件,生成MANIFEST.MF文件,打包

DEMO实例:

(1)引入maven依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>cn.aacopy.learn</groupId>
<artifactId>agent-learn</artifactId>
<version>1.0-SNAPSHOT</version>

<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>

<dependencies>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.12.12</version>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive> <!--自动添加META-INF/MANIFEST.MF -->
<manifest>
<!-- 添加 mplementation-*和Specification-*配置项-->
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
<addDefaultSpecificationEntries>true</addDefaultSpecificationEntries>
</manifest>
<manifestEntries>
<!--指定premain方法所在的类-->
<Premain-Class>cn.aacopy.learn.agent.AgentMainTest</Premain-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>

</project>

(2)创建探针入口premain方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package cn.aacopy.learn.agent;

import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.matcher.ElementMatchers;

import java.lang.instrument.Instrumentation;

/**
* @author iseven.yang
* @date 2022/10/27 9:07
*/
public class AgentMainTest {

public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("执行agent拦截,参数:" + agentArgs);
//使用bytebuddy的AgentBuilder构建Agent
AgentBuilder.Identified.Extendable agentBuilder = new AgentBuilder
//使用默认agent实例
.Default()
//拦截配置方式设置
.type(ElementMatchers.nameStartsWith(agentArgs))
//拦截到的class的处理方式
.transform((builder, typeDescription, classLoader, javaModule) ->
// 设置拦截规则,哪些方法会被拦截
builder.method(ElementMatchers.isPublic().and(ElementMatchers.nameEndsWith("Haha")))
// 拦截后交给 SpringControllerInterceptor 处理
.intercept(MethodDelegation.to(TimeInterceptor.class)));
// 装载到 instrumentation 上
agentBuilder.installOn(inst);
}
}

(3)编写一个拦截后的方法处理类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package cn.aacopy.learn.agent;

import net.bytebuddy.implementation.bind.annotation.AllArguments;
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.Arrays;
import java.util.concurrent.Callable;

/**
* @author iseven.yang
* @date 2022/10/27 10:56
*/
public class TimeInterceptor {

//@RuntimeType:不进行严格的参数类型检测,在参数匹配失败时,尝试使用类型转换方式(runtime type casting)进行类型转换,匹配相应方法。
//@Origin:注入正在执行的方法Method 对象
//@AllArguments:获取入参列表
//@SuperCall:方法的调用者对象,可以用于对原始方法的调用
@RuntimeType
public static Object intercept(@Origin Method method,
@AllArguments Object[] args,
@SuperCall Callable<?> callable) throws Exception {
long l1 = System.currentTimeMillis();
// 执行原函数
Object result = callable.call();
System.out.println(method.getName() + ":执行耗时" + (System.currentTimeMillis() - l1) + "ms");
System.out.println("参数列表:"+Arrays.toString(args));
System.out.println("返回类型:"+method.getReturnType());
System.out.println("返回值:"+result);
return result;
}
}

(4)随便写一个应用程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package cn.aacopy.test.simpleboot.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
* @author iseven.yang
* @date 2022/10/27 11:16
*/
@RestController
public class TestController {

@GetMapping("/test1")
public String test1Haha() {
System.out.println("hahaha");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "hahah";
}

@GetMapping("/test2")
public String test2Haha(@RequestParam String name) {
System.out.println("hahaha2sssssssss");
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "hihei" + name;
}

@GetMapping("/test3")
public String test3Haha1() {
System.out.println("hihei123123213");
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "hihei1232312322";
}
}

(5)修改启动jvm配置:

1
-javaagent:D:\code\mine\agent-learn\target\agent-learn-1.0-SNAPSHOT-jar-with-dependencies.jar=cn.aacopy.test.simpleboot.controller

(6)启动服务,调用接口

启动服务前会打印日志

1
执行agent拦截,参数:cn.aacopy.test.simpleboot.controller

调用接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
hihei123123213

hahaha
test1Haha:执行耗时100ms
参数列表:[]
返回类型:class java.lang.String
返回值:hahah

hahaha2sssssssss
test2Haha:执行耗时306ms
参数列表:[wwwww]
返回类型:class java.lang.String
返回值:hiheiwwwww

Skywalking的插桩实现,静态方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public abstract class ClassEnhancePluginDefine extends AbstractClassEnhancePluginDefine {

/**
* Enhance a class to intercept class static methods.
*
* @param typeDescription target class description
* @param newClassBuilder byte-buddy's builder to manipulate class bytecode.
* @return new byte-buddy's builder for further manipulation.
*/
protected DynamicType.Builder<?> enhanceClass(TypeDescription typeDescription, DynamicType.Builder<?> newClassBuilder,
ClassLoader classLoader) throws PluginException {
StaticMethodsInterceptPoint[] staticMethodsInterceptPoints = getStaticMethodsInterceptPoints();
String enhanceOriginClassName = typeDescription.getTypeName();
if (staticMethodsInterceptPoints == null || staticMethodsInterceptPoints.length == 0) {
return newClassBuilder;
}

for (StaticMethodsInterceptPoint staticMethodsInterceptPoint : staticMethodsInterceptPoints) {
String interceptor = staticMethodsInterceptPoint.getMethodsInterceptor();
if (StringUtil.isEmpty(interceptor)) {
throw new EnhanceException("no StaticMethodsAroundInterceptor define to enhance class " + enhanceOriginClassName);
}

if (staticMethodsInterceptPoint.isOverrideArgs()) {
if (isBootstrapInstrumentation()) {
newClassBuilder = newClassBuilder.method(isStatic().and(staticMethodsInterceptPoint.getMethodsMatcher()))
.intercept(MethodDelegation.withDefaultConfiguration()
.withBinders(Morph.Binder.install(OverrideCallable.class))
.to(BootstrapInstrumentBoost.forInternalDelegateClass(interceptor)));
} else {
newClassBuilder = newClassBuilder.method(isStatic().and(staticMethodsInterceptPoint.getMethodsMatcher()))
.intercept(MethodDelegation.withDefaultConfiguration()
.withBinders(Morph.Binder.install(OverrideCallable.class))
.to(new StaticMethodsInterWithOverrideArgs(interceptor)));
}
} else {
if (isBootstrapInstrumentation()) {
newClassBuilder = newClassBuilder.method(isStatic().and(staticMethodsInterceptPoint.getMethodsMatcher()))
.intercept(MethodDelegation.withDefaultConfiguration()
.to(BootstrapInstrumentBoost.forInternalDelegateClass(interceptor)));
} else {
newClassBuilder = newClassBuilder.method(isStatic().and(staticMethodsInterceptPoint.getMethodsMatcher()))
.intercept(MethodDelegation.withDefaultConfiguration()
.to(new StaticMethodsInter(interceptor)));
}
}

}

return newClassBuilder;
}

(增强类)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
public class StaticMethodsInter {
private static final ILog LOGGER = LogManager.getLogger(StaticMethodsInter.class);

/**
* A class full name, and instanceof {@link StaticMethodsAroundInterceptor} This name should only stay in {@link
* String}, the real {@link Class} type will trigger classloader failure. If you want to know more, please check on
* books about Classloader or Classloader appointment mechanism.
*/
private String staticMethodsAroundInterceptorClassName;

/**
* Set the name of {@link StaticMethodsInter#staticMethodsAroundInterceptorClassName}
*
* @param staticMethodsAroundInterceptorClassName class full name.
*/
public StaticMethodsInter(String staticMethodsAroundInterceptorClassName) {
this.staticMethodsAroundInterceptorClassName = staticMethodsAroundInterceptorClassName;
}

/**
* Intercept the target static method.
*
* @param clazz target class 要修改字节码的目标类
* @param allArguments all method arguments 原方法所有的入参
* @param method method description. 原方法
* @param zuper the origin call ref. 原方法的调用 zuper.call()代表调用原方法
* @return the return value of target static method.
* @throws Exception only throw exception because of zuper.call() or unexpected exception in sky-walking ( This is a
* bug, if anything triggers this condition ).
*/
@RuntimeType
public Object intercept(@Origin Class<?> clazz, @AllArguments Object[] allArguments, @Origin Method method,
@SuperCall Callable<?> zuper) throws Throwable {
StaticMethodsAroundInterceptor interceptor = InterceptorInstanceLoader.load(staticMethodsAroundInterceptorClassName, clazz
.getClassLoader());

MethodInterceptResult result = new MethodInterceptResult();
try {
interceptor.beforeMethod(clazz, method, allArguments, method.getParameterTypes(), result);
} catch (Throwable t) {
LOGGER.error(t, "class[{}] before static method[{}] intercept failure", clazz, method.getName());
}

Object ret = null;
try {
if (!result.isContinue()) {
ret = result._ret();
} else {
//调用原方法
ret = zuper.call();
}
} catch (Throwable t) {
try {
interceptor.handleMethodException(clazz, method, allArguments, method.getParameterTypes(), t);
} catch (Throwable t2) {
LOGGER.error(t2, "class[{}] handle static method[{}] exception failure", clazz, method.getName(), t2.getMessage());
}
throw t;
} finally {
try {
ret = interceptor.afterMethod(clazz, method, allArguments, method.getParameterTypes(), ret);
} catch (Throwable t) {
LOGGER.error(t, "class[{}] after static method[{}] intercept failure:{}", clazz, method.getName(), t.getMessage());
}
}
return ret;
}
}