言成言成啊 | Kit Chen's Blog

Java动态字节码技术

发布于2023-02-05 23:42:07,更新于2023-07-16 03:12:20,标签:java jvm  转载随意,文章会持续修订,请注明来源地址:https://meethigher.top/blog

该系列文章上篇理解加载字节码到JVM的时机

上学时,就想做一个签到系统。签到就需要执行动态任务。这所谓的动态任务,就是任务的执行逻辑可在线编辑,再进一步而言,就是要求同名字节码,可运行时更换。

对于Java来说,要想实现在线编辑,就需要动态创建对象。

有三种方式

  • 主类加载器加载字节码:×

    • 缺点:不能重复加载同名字节码
  • 自定义类加载器加载字节码:√

    1. 使用单例自定义类加载器,加载字节码:×
      • 缺点:不能重复加载同名字节码
    2. 使用多例自定义类加载器,加载字节码:√
      • 缺点:在jvm中,类加载器过多,且类加载实例过少。易出现oom。参考
  • 使用JVMTi agent和Instrumentation 连接到应用进程内部,直接修改字节码:√

    • 缺点:需要直接连接到进程内部,不实用

一、类加载器版本差异对比

源码参考如图

1.1 jdk8

以jdk8加载外部jar包为示例

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
import java.io.File;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;

public class ClassLoadUtils {
/**
* 将jar的地址链接到jvm
* @param jarPath jar包路径
*/
public static void loadJar(String jarPath) {
File jarFile = new File(jarPath);
// 从URLClassLoader类中获取类所在文件夹的方法,jar也可以认为是一个文件夹
Method method = null;
try {
method = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
} catch (NoSuchMethodException | SecurityException e1) {
e1.printStackTrace();
}
// 获取方法的访问权限以便写回
boolean accessible = method.isAccessible();
try {
method.setAccessible(true);
// 获取系统类加载器
URLClassLoader classLoader = (URLClassLoader) ClassLoader.getSystemClassLoader();
URL url = jarFile.toURI().toURL();
method.invoke(classLoader, url);
} catch (Exception e) {
e.printStackTrace();
} finally {
method.setAccessible(accessible);
}
}
}

在jdk8中,systemClassLoader是来自于sun.misc.Launcher.AppClassLoader,本质就是个UrlClassLoader

直接获取字节码的类即可,默认取的是当前类加载器,一般即主类加载器

1
Class<?> clazz = Class.forName("test.callback.TestCallback");

1.2 jdk11

jdk11的systemClassLoader是来自于jdk.internal.loader.ClassLoaders.AppClassLoader,本质是个私有内部类

jdk11的systemClassLoader没有直接api进行加载外部jar。openjdk的官方也有说明,参考

因此,如果想要加载外部jar,只能引入自定义的UrlClassLoader

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
public class ClassLoadUtils {

public static final URLClassLoader classLoader = new URLClassLoader(new URL[0], ClassLoader.getSystemClassLoader());


/**
* 将jar的地址链接到jvm
*
* @param jarPath jar包路径
*/
public static void loadJar(String jarPath) {
File jarFile = new File(jarPath);
// 从URLClassLoader类中获取类所在文件夹的方法,jar也可以认为是一个文件夹
Method method = null;
try {
method = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
} catch (NoSuchMethodException | SecurityException e1) {
e1.printStackTrace();
}
// 获取方法的访问权限以便写回
boolean accessible = method.isAccessible();
try {
method.setAccessible(true);
URL url = jarFile.toURI().toURL();
method.invoke(classLoader, url);
} catch (Exception e) {
e.printStackTrace();
} finally {
method.setAccessible(accessible);
}
}
}

获取字节码的类时,需要指定类加载器

1
Class<?> clazz = Class.forName("test.callback.TestCallback", true, ClassLoadUtils.classLoader);

二、自定义类加载器

2.1 单例

我最初的写法如下

如图进行单元测试

这个做法的缺陷是,无法重复加载同类字节码

2.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
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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
import javax.tools.*;
import java.io.*;
import java.net.URI;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.CharBuffer;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* Required JDK >= 1.6<br><br>
* This class can help you create the Java byte code dynamically through the string and load it into memory.<br><br>
* <p>
* HOW TO:<br>
* First step. <code>Map<String, byte[]> bytecode = DynamicLoader.compile("TestClass.java", javaSrc);</code><br>
* Second step. <code>DynamicLoader.MemoryClassLoader classLoader = new DynamicLoader.MemoryClassLoader(bytecode);</code><br>
* Third step. <code>Class clazz = classLoader.loadClass("TestClass");</code><br>
* <br>
* Then just like the normal use of the call this class can be.
*
* @author https://github.com/Lua12138/UtilsClass/blob/master/locals/DynamicLoader.java
*/
public class DynamicLoader {
/**
* auto fill in the java-name with code, return null if cannot find the public class
*
* @param javaSrc source code string
* @return return the Map, the KEY means ClassName, the VALUE means bytecode.
*/
public static Map<String, byte[]> compile(String javaSrc) {
Pattern pattern = Pattern.compile("public\\s+class\\s+(\\w+)");

Matcher matcher = pattern.matcher(javaSrc);

if (matcher.find())
return compile(matcher.group(1) + ".java", javaSrc);
return null;
}

/**
* @param javaName the name of your public class,eg: <code>TestClass.java</code>
* @param javaSrc source code string
* @return return the Map, the KEY means ClassName, the VALUE means bytecode.
*/
public static Map<String, byte[]> compile(String javaName, String javaSrc) {
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager stdManager = compiler.getStandardFileManager(null, null, null);

try (MemoryJavaFileManager manager = new MemoryJavaFileManager(stdManager)) {
JavaFileObject javaFileObject = manager.makeStringSource(javaName, javaSrc);
JavaCompiler.CompilationTask task = compiler.getTask(null, manager, null, null, null, Arrays.asList(javaFileObject));
if (task.call())
return manager.getClassBytes();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}

public static class MemoryClassLoader extends URLClassLoader {

Map<String, byte[]> classBytes = new HashMap<String, byte[]>();

public MemoryClassLoader(Map<String, byte[]> classBytes) {
super(new URL[0], MemoryClassLoader.class.getClassLoader());
this.classBytes.putAll(classBytes);
}

public void setClassBytes(Map<String, byte[]> classBytes) {
this.classBytes = classBytes;
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] buf = classBytes.get(name);
if (buf == null) {
return super.findClass(name);
}
classBytes.remove(name);
return defineClass(name, buf, 0, buf.length);
}
}
}


/**
* JavaFileManager that keeps compiled .class bytes in memory.
*/
@SuppressWarnings("unchecked")
final class MemoryJavaFileManager extends ForwardingJavaFileManager {

/**
* Java source file extension.
*/
private final static String EXT = ".java";

private Map<String, byte[]> classBytes;

public MemoryJavaFileManager(JavaFileManager fileManager) {
super(fileManager);
classBytes = new HashMap<String, byte[]>();
}

public Map<String, byte[]> getClassBytes() {
return classBytes;
}

public void close() throws IOException {
classBytes = new HashMap<String, byte[]>();
}

public void flush() throws IOException {
}

/**
* A file object used to represent Java source coming from a string.
*/
private static class StringInputBuffer extends SimpleJavaFileObject {
final String code;

StringInputBuffer(String name, String code) {
super(toURI(name), Kind.SOURCE);
this.code = code;
}

public CharBuffer getCharContent(boolean ignoreEncodingErrors) {
return CharBuffer.wrap(code);
}

public Reader openReader() {
return new StringReader(code);
}
}

/**
* A file object that stores Java bytecode into the classBytes map.
*/
private class ClassOutputBuffer extends SimpleJavaFileObject {
private String name;

ClassOutputBuffer(String name) {
super(toURI(name), Kind.CLASS);
this.name = name;
}

public OutputStream openOutputStream() {
return new FilterOutputStream(new ByteArrayOutputStream()) {
public void close() throws IOException {
out.close();
ByteArrayOutputStream bos = (ByteArrayOutputStream) out;
classBytes.put(name, bos.toByteArray());
}
};
}
}

public JavaFileObject getJavaFileForOutput(Location location,
String className,
JavaFileObject.Kind kind,
FileObject sibling) throws IOException {
if (kind == JavaFileObject.Kind.CLASS) {
return new ClassOutputBuffer(className);
} else {
return super.getJavaFileForOutput(location, className, kind, sibling);
}
}

static JavaFileObject makeStringSource(String name, String code) {
return new StringInputBuffer(name, code);
}

static URI toURI(String name) {
File file = new File(name);
if (file.exists()) {
return file.toURI();
} else {
try {
final StringBuilder newUri = new StringBuilder();
newUri.append("mfm:///");
newUri.append(name.replace('.', '/'));
if (name.endsWith(EXT)) newUri.replace(newUri.length() - EXT.length(), newUri.length(), EXT);
return URI.create(newUri.toString());
} catch (Exception exp) {
return URI.create("mfm:///com/sun/script/java/java_source");
}
}
}
}

进行单元测试

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
@Test
void test() throws Exception {
String classContent = "public class TestInterface {\n" +
" public void sayHello() {\n" +
" System.out.println(\"halo wode\");\n" +
" }\n" +
"\n" +
" public int returnAdd(int a, int b) {\n" +
" return a + b;\n" +
" }\n" +
"}";
System.out.println("更换字节码:\n" + classContent);
Map<String, byte[]> bytecode = DynamicLoader.compile(classContent);
DynamicLoader.MemoryClassLoader classLoader = new DynamicLoader.MemoryClassLoader(bytecode);
Class<?> clazz = classLoader.loadClass("TestInterface");
Object o = clazz.newInstance();
Method method = clazz.getMethod("sayHello");
method.invoke(o);
System.out.println("------------------------------");
classContent = "public class TestInterface {\n" +
" public void sayHello() {\n" +
" System.out.println(\"Hello World\");\n" +
" }\n" +
"\n" +
" public int returnAdd(int a, int b) {\n" +
" return a + b;\n" +
" }\n" +
"}";
System.out.println("更换字节码:\n" + classContent);
bytecode = DynamicLoader.compile(classContent);
classLoader = new DynamicLoader.MemoryClassLoader(bytecode);
Class<?> aClazz = classLoader.loadClass("TestInterface");
Object o1 = aClazz.newInstance();
Method sayHello = aClazz.getMethod("sayHello");
sayHello.invoke(o1);
}

最后运行结果,如图

这个做法的缺陷是,类加载器越来越多,碎片化内存越来越多,容易造成OutOfMemoryError。可以参考假笨说-警惕大量类加载器的创建导致诡异的Full GC

三、Agent修改字节码

3.1 基础

在IDEA中,可以通过断点执行Evaluate Expression动态修改代码,此处就是使用了Agent的方式。

好多人提到asm、javassit,其实这些只能做到在字节码加载前的修改,并不能做到在运行时的修改。

动态字节码,官方解释叫做Bytecode Instrumentation,也叫字节码增强技术。

从狭义来讲,字节码增强讲的就是对已经是字节码的class文件进行操作。

那么主要有两种工具,一个是ASM,另一个是Javassit,ASM是纯粹的对字节码按照java的规范就行字节码理解范畴内的进行修改操作,可以说门槛很高; Javassit则可以理解为是一个提供了对字节码操作API的框架,来简化字节码操作的门槛,让字节码操作像面向对象编程一样简单;因此,由想而知,ASM要比Javassit性能要好,为此,为了鱼和熊掌兼得,我们从ASM基础上又衍生出了CGLib,虽然功能没有ASM强悍,但使用相对简单了很多。

那么从广义上来讲,所有让代码具备原本不具有的功能,那种类似魔法效果的技术,都统称为字节码增强。

3.2 demo

通过jvmti agent将该打印halo wode的字节码改为打印hello world

简单的demo可以参照meethigher/jvmti-agent: 使用jvmti-agent和Instrumentation实现的动态修改已加载字节码

执行命令打包,生成agent类

1
mvn clean package

idea启动TestMain,获取PID,将PID放入Attacher中,启动。再次查看TestMain执行日志,会发现方法内容已经被修改了。

这个做法的缺陷是,agent只能加载一次,这带来的后果是字节码只能修改一次。且只可作为监控在外部使用,如果业务开发使用,反而得不偿失

四、参考致谢

michaelliao/compiler: In-memory compile java source code and load compiled classes.

Java 动态字节码技术 - 枕边书

假笨说-警惕大量类加载器的创建导致诡异的Full GC

字节码增强技术&手写一个 Java Agent - 掘金

java运行时 动态修改class 动态增加方法耗时统计_罗政的博客-CSDN博客_运行中动态修改类

字节码增强技术介绍 | Isaac’s博客

字节码增强:原理与实战 - 知乎

发布:2023-02-05 23:42:07
修改:2023-07-16 03:12:20
链接:https://meethigher.top/blog/2023/dynamic-bytecode/
标签:java jvm 
付款码 打赏 分享
若无法评论请科学上网
shift+ctrl+1可控制目录显示