0%

使用agent和javassist实现基于jvm的aop日志打印系统

简介

java在JDK1.5中提供了java.lang.Instrument包,该包提供了一些工具帮助开发人员在 Java 程序运行时动态的修改class,此文介绍其中的agent组件来实现基于jvm层面的aop.

命令

如果我们在cmd输入java -help就会看到关于javaagent命令的简介

1
2
3
4
5
6
7
java -help
用法: java [-options] class [args...]

其中选项包括:

-javaagent:<jarpath>[=<选项>]
加载 Java 编程语言代理, 请参阅 java.lang.instrument

实例如下

1
java -javaagent:/to/agent.jar -jar /main.jar

其中javaagent参数后的路径就是我们要编写的代码,其可以在要代理运行的jar运行前进行一些class字节的操作,如果在此期间使用犹如javassist,或者asm字节码操作的框架则可以实现在类运行前对要运行的class进行修改.

agent简介

  • 参数 javaagent 可以用于指定一个 jar 包,并且对该 java 包有2个要求:
  1. 这个jar 包的MANIFEST.MF 文件必须指定 Premain-Class 项。
  2. Premain-Class 指定的那个类必须实现 premain()方法。
    关于第一点我们在后续创建工程中使用maven 打包插件maven-assembly-plugin去解决这一点.
    现在我们解释一下第二点:
    javaagent程序不需要实现任何接口,只需要创建一个类其中类中必须要有以下方法之一即可

public static void premain(String agentArgs, Instrumentation inst)
public static void premain(String agentArgs)

一般我们使用第一个方法 ,参数 agentArgs 时通过命令行传给 Java Agent 的参数,inst 是Java Class 字节码转换的工具,Instrumentation 常用方法如下:

void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
增加一个Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 canRetransform 设置是否允许重新转换我们可以使用ClassFileTransformer对象结合javassist或者asm对class文件进行修改

void redefineClasses(ClassDefinition… definitions) hrows ClassNotFoundException, UnmodifiableClassException;
在类加载之前,重新定义 Class 文件,ClassDefinition 表示对一个类新的定义,如果在类加载之后,需要使用 retransformClasses 方法重新定义。

本文中我们使用addTransformer方法在class文件加载前,我们对class字节码进行处理,添加打印日志代码,此方法接收一个ClassFileTransformer对象,Transformer 类包含了一个 transform 方法,它的签名会接受 ClassLoader、类名、要重定义的类所对应的 Class 对象、定义权限的 ProtectionDomain 以及这个类的原始字节。如果从 transform 方法中返回 null 的话,将会告诉运行时环境我们并没有对这个类进行变更。

1
2
3
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
return new byte[0];
}

实现思路简介

我们实现的原理很简单,首先我们在运行的主工程中定义个注解,此注解的功能是标识出哪些方法和哪些参数需要打印log,其作用域为在方法上,fields属性为数组,我们将需要打印的参数的角标放入其中如,

1
2
3
4
5
6
//打印方法的第一个参数
@PrintLog(fields = {"1"})
public void test(String name,String name2){


}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.liu;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface PrintLog {


String[] fields();

}

然后我们对需要打印日志的方法如上加上注解即可,在运行agent程序的时候我们会在class文件加载前,逐个扫描方法,查看是否有@PrintLog注解,如果有注解我们则应用javassist框架在class文件中加入打印代码,由于在之前文章讲述的类加载器的双亲委派模型,同一个class文件在双亲委派模式下只会加载一次,所以只有在class文件第一次被加载时才会触发此修改class的条件,因此也不必过多担心效率问题.

搭建agent工程

创建agent主题类

我们新建一个maven工程,首先新建一个类,其只包含上文简介的premain方法,并添加一个日志打印转换器,此方法会在代码里jar文件执行main方法前执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.liu;


import java.lang.instrument.Instrumentation;


/**
* @author Liush
* @description 自定义agent在main方法执行前执行
* @date 2019/9/23 15:32
**/
public class MyAgent {


public static void premain(String agentArgs, Instrumentation inst) {

inst.addTransformer(new LogPrintTransformer());

}


}
创建ClassFileTransformer转换类

上文提到premain方法中有一个Instrumentation对象,其的addTransformer方法可以添加一个创建ClassFileTransformer转换类,其可以在class第一次加载时对class文件进行处理,修改java代码因此我们现在创建一个ClassFileTransformer的实现类,类中主要使用javassist框架扫描加载的类的信息,和方法,查看方法上时候有我们之前标识的@PrintLog注解,如果有的话获取注解中的值,然后最后根据获取的值打印对应的参数

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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
package com.liu;

import javassist.*;
import javassist.bytecode.AnnotationsAttribute;
import javassist.bytecode.CodeAttribute;
import javassist.bytecode.LocalVariableAttribute;
import javassist.bytecode.MethodInfo;

import javassist.bytecode.annotation.Annotation;
import javassist.bytecode.annotation.ArrayMemberValue;
import javassist.bytecode.annotation.MemberValue;
import javassist.bytecode.annotation.StringMemberValue;

import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

/**
* @author Liush
* @description
* @date 2019/9/23 15:41
**/
public class LogPrintTransformer implements ClassFileTransformer {


/**
* 签名会接受 ClassLoader、类名、要重定义的类所对应的 Class 对象、定义权限的 ProtectionDomain 以及这个类的原始字节。
* 如果从 transform 方法中返回 null 的话,将会告诉运行时环境我们并没有对这个类进行变更。
*/
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {

try {
CtClass ctClass=ClassPool.getDefault().get(className.replaceAll("/", "."));
CtMethod[] ctMethods =ctClass.getDeclaredMethods();
for(CtMethod ctMethod:ctMethods){
if(getAnnotation(ctMethod)!=null) {
List<String> value = getParamIndexes(getAnnotation(ctMethod));
ctMethod.insertBefore(createJavaString(className, ctMethod, value));
}

}
return ctClass.toBytecode();
} catch (NotFoundException e) {
e.printStackTrace();
} catch (CannotCompileException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}

return null;
}


//在javassist中$1代表方法的第一个参数,$2代表第二个参数以此类推可参考https://www.jianshu.com/p/b9b3ff0e1bf8
private String createJavaString(String className,CtMethod ctMethod,List<String> params){
StringBuilder stringBuilder=new StringBuilder();
for(String index:params){
stringBuilder.append("System.out.println($");
stringBuilder.append(index);
stringBuilder.append(");");
}
return stringBuilder.toString();
}


//查找方法注解
public Annotation getAnnotation(CtMethod method) {
MethodInfo methodInfo = method.getMethodInfo();
AnnotationsAttribute attInfo = (AnnotationsAttribute) methodInfo
.getAttribute(AnnotationsAttribute.visibleTag);
if (attInfo != null) {
return attInfo.getAnnotation("com.liu.PrintLog");
}
return null;
}


//获得注解中的属性值
public List<String> getParamIndexes(Annotation annotation) {
ArrayMemberValue fields =
(ArrayMemberValue) annotation.getMemberValue("fields");
if (fields != null) {
MemberValue[] values = fields.getValue();
List<String> parameterIndexes = new ArrayList<>();
for (MemberValue val : values) {
parameterIndexes.add(((StringMemberValue) val).getValue());
}
return parameterIndexes;
}
return Collections.emptyList();
}





}
生成MANIFEST.MF文件

使用agent必须在MANIFEST.MF中指定Premain-Class表示我们只想的agent的类,设置Can-Redefine-Classes和Can-Retransform-Classes属性为true,我们采用maven插件的方式进行打包,由maven自动生成MANIFEST.MF文件
在maven pom依赖中加入

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
<build>
<plugins>
<!-- http://maven.apache.org/shared/maven-archiver/index.html#class_manifest -->
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.2</version>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<index>true</index>
<manifestEntries>
<!-- 设置MANIFEST中的属性 -->
<Premain-Class>com.liu.MyAgent</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>
<!-- 将插件绑定maven的package命令-->
<phase>package</phase>
<!-- append to the packaging phase. -->
<goals>
<goal>single</goal>
<!-- goals == mojos -->
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

创建主工程

创建打印注解

此类为了标识哪些方法的哪些参数需要打印

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.liu;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface PrintLog {


String[] fields();

}

创建一个测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.liu;

/**
* @author Liush
* @description
* @date 2019/9/23 18:12
**/
public class LogTest {


@PrintLog(fields = {"1"})
public void test(String name,String name2){

System.out.println("over...........................");

}


}

创建main方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.liu;

/**
* @author Liush
* @description
* @date 2019/9/23 18:06
**/
public class MainTest {


public static void main(String[] args) {
LogTest logTest=new LogTest();
logTest.test("liushaohuang111","liushaohuang2222");

}

}

同样我是使用maven maven-assembly-plugin插件进行打包,与上面不同的是我们这次不要指定Premain-Class,Can-Redefine-Classes和Can-Retransform-Classes参数我们只需要声明main函数的入口即可

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
<build>
<plugins>
<!-- http://maven.apache.org/shared/maven-archiver/index.html#class_manifest -->
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.2</version>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<index>true</index>
<manifest>
<mainClass>com.liu.MainTest</mainClass>
</manifest>
</archive>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<!-- this is used for inheritance merges -->
<phase>package</phase>
<!-- append to the packaging phase. -->
<goals>
<goal>single</goal>
<!-- goals == mojos -->
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

打包运行

在两个工程目录下执行

1
mvn clean package

运行javaagent命令

1
java -javaagent:.\agent-1.0-SNAPSHOT-jar-with-dependencies.jar -jar .\main_test-1.0-SNAPSHOT-jar-with-dependencies.jar

在控制台中出现,成功打印出传入的第一个参数,并且在原始方法执行打印overing之前执行打印

1
2
liushaohuang111
over...........................

更进一步

在实践中本人思考premain 中的Instrumentation 添加的转换器到底什么时候才起作用呢?这里再做一个实践,我们修改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
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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
package com.liu;

import javassist.*;
import javassist.bytecode.AnnotationsAttribute;
import javassist.bytecode.CodeAttribute;
import javassist.bytecode.LocalVariableAttribute;
import javassist.bytecode.MethodInfo;

import javassist.bytecode.annotation.Annotation;
import javassist.bytecode.annotation.ArrayMemberValue;
import javassist.bytecode.annotation.MemberValue;
import javassist.bytecode.annotation.StringMemberValue;

import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

/**
* @author Liush
* @description
* @date 2019/9/23 15:41
**/
public class LogPrintTransformer implements ClassFileTransformer {


/**
* 签名会接受 ClassLoader、类名、要重定义的类所对应的 Class 对象、定义权限的 ProtectionDomain 以及这个类的原始字节。
* 如果从 transform 方法中返回 null 的话,将会告诉运行时环境我们并没有对这个类进行变更。
*/
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {

try {
System.out.println("in transform....................");
CtClass ctClass=ClassPool.getDefault().get(className.replaceAll("/", "."));
CtMethod[] ctMethods =ctClass.getDeclaredMethods();
for(CtMethod ctMethod:ctMethods){
if(getAnnotation(ctMethod)!=null) {
List<String> value = getParamIndexes(getAnnotation(ctMethod));
ctMethod.insertBefore(createJavaString(className, ctMethod, value));
}

}
return ctClass.toBytecode();
} catch (NotFoundException e) {
e.printStackTrace();
} catch (CannotCompileException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}

return null;
}


//在javassist中$1代表方法的第一个参数,$2代表第二个参数以此类推可参考https://www.jianshu.com/p/b9b3ff0e1bf8
private String createJavaString(String className,CtMethod ctMethod,List<String> params){
StringBuilder stringBuilder=new StringBuilder();
for(String index:params){
stringBuilder.append("System.out.println($");
stringBuilder.append(index);
stringBuilder.append(");");
}
return stringBuilder.toString();
}


//查找方法注解
public Annotation getAnnotation(CtMethod method) {
MethodInfo methodInfo = method.getMethodInfo();
AnnotationsAttribute attInfo = (AnnotationsAttribute) methodInfo
.getAttribute(AnnotationsAttribute.visibleTag);
if (attInfo != null) {
return attInfo.getAnnotation("com.liu.PrintLog");
}
return null;
}


//获得注解中的属性值
public List<String> getParamIndexes(Annotation annotation) {
ArrayMemberValue fields =
(ArrayMemberValue) annotation.getMemberValue("fields");
if (fields != null) {
MemberValue[] values = fields.getValue();
List<String> parameterIndexes = new ArrayList<>();
for (MemberValue val : values) {
parameterIndexes.add(((StringMemberValue) val).getValue());
}
return parameterIndexes;
}
return Collections.emptyList();
}





}

现在我们更改主项目中被代理的main方法代码,里面一共new了4次LogTest对象

  1. 第一个是第一次触发class加载(class文件只有被调用时候才会第一次加载到虚拟机),则会触发premain方法中的转换器
  2. 第二个是直接调用堆栈中的class又new一次对象对象,没有触发转化器
  3. 第三个我们使用自定义类加载器,并且不突破双亲委派模式(还是调用堆栈中的LogTest),同样也没有触发转化器
  4. 第四个,我们又创建了一个类加载器,并且突破双亲委派模式,重新加载一个LogTest,不再使用之前堆栈中的LogTest对象,最后发现触发了转化器

结论:只有在class重新被加载时才会触发ClassFileTransformer转换器

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
70
71
72
73
74
75
76
77
78
79
80
81
82
package com.liu;

import java.io.IOException;
import java.io.InputStream;

/**
* @author Liush
* @description
* @date 2019/9/23 18:06
**/
public class MainTest {


public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {

//第一次次加载触发ClassFileTransformer
LogTest logTest = new LogTest();
logTest.test("liushaohuang111", "liushaohuang2222");


//重新创建对象,不会触发premain中的ClassFileTransformer(类已经加载到堆栈中)
LogTest logTest1 = new LogTest();
logTest1.test("shao111111111111", "shao22222222222222");




//不突破双亲委派类进行类加载,不会触发premain中的ClassFileTransformer(类已经加载到堆栈中)
ClassLoader classLoader = new ClassLoader() {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
InputStream is = getClass().getResourceAsStream(fileName);
if (is == null) {
return super.loadClass(name);
}
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
} catch (IOException e) {
e.printStackTrace();
throw new ClassNotFoundException(name);
}
}
};

Class aClass = classLoader.loadClass("com.liu.LogTest");
LogTest logTest2 = (LogTest)aClass.newInstance();
logTest2.test("Parents Delegation Model 1","Parents Delegation Model 2");



//突破双亲委派模式
ClassLoader classLoader2=new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
InputStream is = getClass().getResourceAsStream(fileName);
if (is == null) {
return super.loadClass(name);
}
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
} catch (IOException e) {
e.printStackTrace();
throw new ClassNotFoundException(name);
}
}
};

Class aClass2 = classLoader.loadClass("com.liu.LogTest");
LogTest logTest3 = (LogTest)aClass.newInstance();
logTest3.test("No Parents Delegation Model1","No Parents Delegation Model 2");



}

}

输出结果

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
in transform....................
in transform....................
in transform....................
in transform....................
in transform....................
in transform....................
in transform....................
in transform....................
in transform....................
in transform....................
in transform....................
in transform....................
in transform....................
in transform....................
in transform....................
in transform....................
in transform....................
in transform....................
in transform....................
in transform....................
in transform....................
in transform....................
in transform....................
in transform....................
in transform....................
in transform....................
in transform....................
in transform....................
in transform....................
in transform....................
in transform....................
in transform....................
in transform....................
in transform....................
in transform....................
liushaohuang111
over...........................
shao111111111111
over...........................
Parents Delegation Model 1
over...........................
in transform....................
No Parents Delegation Model1
over...........................
in transform....................
in transform....................