字节码编程

摘抄的 Java 字节码编程知识

字节码编程

方法一:修改类文件

首先定义 Item 类,位于 set 包内

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
package set;

import bytecodeAnnotations.LogEntry;

import java.util.Objects;

/**
* An item with a description and a part number.
*
* @author qusong
* @Date 2022/11/1
**/
public class Item {
private String description;
private int partNumber;

/**
* Constructs an item.
*
* @param description the item's description
* @param partNumber the item's part number
*/
public Item(String description, int partNumber) {
this.description = description;
this.partNumber = partNumber;
}

/**
* Gets the description of this item.
*
* @return the description
*/
public String getDescription() {
return description;
}

@Override
public String toString() {
return "[description=" + description +
", partNumber=" + partNumber + "]";
}

@LogEntry(logger = "com.qusong")
@Override
public boolean equals(Object otherObject) {
if (this == otherObject) return true;
if (otherObject == null) return false;
if (getClass() != otherObject.getClass()) return false;
Item other = (Item) otherObject;
return Objects.equals(description, other.description)
&& partNumber == other.partNumber;
}

@LogEntry(logger = "com.qusong")
@Override
public int hashCode() {
return Objects.hash(description, partNumber);
}
}

其中的 equals 方法和 hashcode 方法前添加了自定义的注解 @LogEntry(logger = "com.qusong") ,该注解的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
package bytecodeAnnotations;

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 LogEntry {
String logger();
}

再定义EntryLogger类用来解析 LogEntry 注解,实现为所有带LogEntry注解的方法在方法起始处添加log语句的功能。

EntryLogger 类的main 方法需要一个参数,该参数指定了要向哪个类文件中插入字节码。

本文中是要向 set/Item.class 文件中插入字节码,因此命令行参数需添加 item.class 文件的路径信息:java bytecodeAnnotations.EntryLogger set/Item.class

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
package bytecodeAnnotations;

import jdk.internal.org.objectweb.asm.*;
import jdk.internal.org.objectweb.asm.commons.AdviceAdapter;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

/**
* Adds "entering" logs to all methods of a class
* that have the LogEntry annotation.
*
* @author qusong
* @Date 2022/11/1
**/
public class EntryLogger extends ClassVisitor {
private String className;

/**
* Constructs an EntryLogger that inserts logging into
* annotated methods of a given class.
*
* @param writer
* @param className
*/
public EntryLogger(ClassWriter writer, String className) {
super(Opcodes.ASM5, writer);
this.className = className;
}

@Override
public MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions) {
MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
return new AdviceAdapter(Opcodes.ASM5, mv, access, name, desc) {
private String loggerName;

@Override
public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
return new AnnotationVisitor(Opcodes.ASM5) {
@Override
public void visit(String name, Object value) {
if (desc.equals("LbytecodeAnnotations/LogEntry;")
&& name.equals("logger"))
loggerName = value.toString();
}
};
}

@Override
protected void onMethodEnter() {
if (loggerName != null) {
visitLdcInsn(loggerName);
visitMethodInsn(INVOKESTATIC, "java/util/logging/Logger",
"getLogger", "(Ljava/lang/String;)Ljava/util/logging/Logger;", false);
visitLdcInsn(className);
visitLdcInsn(name);
visitMethodInsn(INVOKEVIRTUAL, "java/util/logging/Logger",
"entering", "(Ljava/lang/String;Ljava/lang/String;)V", false);
loggerName = null;
}
}
};
}

/**
* Adds entry logging code to the given class.
*
* @param args the name of the class file to path
*/
public static void main(String[] args) throws IOException {
if (args.length == 0) {
System.out.println("USAGE: java bytecodeAnnotations.EntryLogger classfile");
System.exit(1);
}
Path path = Paths.get(args[0]);
ClassReader reader = new ClassReader(Files.newInputStream(path));
ClassWriter writer = new ClassWriter(
ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
EntryLogger entryLogger = new EntryLogger(writer,
path.toString().replace(".class", "").
replaceAll("[/\\\\]", "."));
reader.accept(entryLogger, ClassReader.EXPAND_FRAMES);
Files.write(Paths.get(args[0]), writer.toByteArray());
}
}

可以使用 javap -c set.Item 查看插入字节码前后的指令区别

下面只粘贴了 Item 类中 hashCode 方法相关的字节码信息:

插入前

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public int hashCode();
Code:
0: iconst_2
1: anewarray #15 // class java/lang/Object
4: dup
5: iconst_0
6: aload_0
7: getfield #2 // Field description:Ljava/lang/String;
10: aastore
11: dup
12: iconst_1
13: aload_0
14: getfield #3 // Field partNumber:I
17: invokestatic #16 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
20: aastore
21: invokestatic #17 // Method java/util/Objects.hash:([Ljava/lang/Object;)I
24: ireturn

插入后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public int hashCode();
Code:
0: ldc #45 // String com.qusong
2: invokestatic #51 // Method java/util/logging/Logger.getLogger:(Ljava/lang/String;)Ljava/util/logging/Logger;
5: ldc #53 // String .Users.qusong.IdeaProjects.JavaLearn.target.classes.set.Item
7: ldc #73 // String hashCode
9: invokevirtual #58 // Method java/util/logging/Logger.entering:(Ljava/lang/String;Ljava/lang/String;)V
12: iconst_2
13: anewarray #4 // class java/lang/Object
16: dup
17: iconst_0
18: aload_0
19: getfield #16 // Field description:Ljava/lang/String;
22: aastore
23: dup
24: iconst_1
25: aload_0
26: getfield #18 // Field partNumber:I
29: invokestatic #79 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
32: aastore
33: invokestatic #83 // Method java/util/Objects.hash:([Ljava/lang/Object;)I
36: ireturn

最后,可以在 set 包内定义 SetTest 类来使用 Item 类

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
package set;

import java.util.HashSet;
import java.util.Set;
import java.util.logging.ConsoleHandler;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
* @author qusong
* @Date 2022/11/1
**/
public class SetTest {
public static void main(String[] args) {
Logger.getLogger("com.qusong").setLevel(Level.FINEST);
Handler handler = new ConsoleHandler();
handler.setLevel(Level.FINEST);
Logger.getLogger("com.qusong").addHandler(handler);

Set<Item> parts = new HashSet<>();
parts.add(new Item("Toaster", 1279));
parts.add(new Item("Microwave", 4104));
parts.add(new Item("Toaster", 1279));
System.out.println(parts);
}
}

可以看到,调用 parts.add() 方法时,内部调用了 Item 方法的 hashcode 方法,因此会有自动添加的log语句

1
2
3
4
5
6
7
8
9
10
11
十一月 01, 2022 2:18:56 下午 .Users.qusong.IdeaProjects.JavaLearn.target.classes.set.Item hashCode
较详细: ENTRY
十一月 01, 2022 2:18:56 下午 .Users.qusong.IdeaProjects.JavaLearn.target.classes.set.Item hashCode
较详细: ENTRY
十一月 01, 2022 2:18:56 下午 .Users.qusong.IdeaProjects.JavaLearn.target.classes.set.Item hashCode
较详细: ENTRY
十一月 01, 2022 2:18:56 下午 .Users.qusong.IdeaProjects.JavaLearn.target.classes.set.Item equals
较详细: ENTRY
[[description=Microwave, partNumber=4104], [description=Toaster, partNumber=1279]]

Process finished with exit code 0

第二次调用parts.add(new Item("Toaster", 1279)) 时,触发了 Item 的 equals 方法,最终parts 内只有两个元素。

方法二:在运行时修改字节

instrumentation API 有一个 hook 可以安装一个字节码变换器,但是这个变换器必须在 main 方法调用前安装,所以需要定义一个 agent(一个用来监控程序的库)来实现。

构建 agent 的步骤如下:

  1. 实现一个类,包含 premain 方法

    public static void premain(String arg, Instrumentation instr)

    该方法将会在 agent 被加载时被调用,agent 可以获取一个单行的命令行参数,传入 arg ,另一个参数 instr 则是用来指示不同的 hooks

  2. 新建一个 manifest 文件 EntryLoggingAgent.mf 来设置 Premain-Class 的属性,例如

    remain-Class: bytecodeAnnotations.EntryLoggingAgent

  3. 将 agent 代码和 manifest 文件打包进 jar 包内

    javac -classpath .:asm/lib/\* bytecodeAnnotations/EntryLoggingAgent.java

    jar cvfm EntryLoggingAgent.jar bytecodeAnnotations/EntryLoggingAgent.mf bytecodeAnnotations/Entry*.class

运行带 agent 的 Java 代码的方法:

java -javaagent:AgentJARFile=agentArgumen

于是,定义 EntryLoggingAgent 类构建 agent

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
package bytecodeAnnotations;

import jdk.internal.org.objectweb.asm.ClassReader;
import jdk.internal.org.objectweb.asm.ClassWriter;

import java.lang.instrument.Instrumentation;

/**
* @author qusong
* @Date 2022/11/1
**/
public class EntryLoggingAgent {
public static void premain(final String arg, Instrumentation instr) {
instr.addTransformer(((loader, className, classBeingRedefined, protectionDomain, data) -> {
if (!className.equals(arg)) return null;
ClassReader reader = new ClassReader(data);
ClassWriter writer = new ClassWriter(
ClassWriter.COMPUTE_MAXS|ClassWriter.COMPUTE_FRAMES
);
EntryLogger el = new EntryLogger(writer, className);
reader.accept(el, ClassReader.EXPAND_FRAMES);
return writer.toByteArray();
}));
}
}

使用 javac -classpath .:asm/lib/\* bytecodeAnnotations/EntryLoggingAgent.java 编译EntryLoggingAgent

之后使用 jar 命令打包

jar cvfm EntryLoggingAgent.jar bytecodeAnnotations/EntryLoggingAgent.mf bytecodeAnnotations/Entry*.class

最后使用 java -javaagent:EntryLoggingAgent.jar=set/Item set/SetTest 命令运行,即可得到和方法一同样的输出

打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2022 qusong
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信