Java 动态编译的实现

前言

本文主要介绍如何实现 Java 的动态编译,并给出快速入门案例,点击下载完整的案例代码。

快速入门

编写接口

1
2
3
4
5
6
7
8
9
10
package com.clay.domain;

/**
* @author clay
*/
public interface Store {

public void sell();

}
1
2
3
4
5
6
7
8
9
10
11
12
13
package com.clay.domain;

/**
* @author clay
*/
public class Supermarket implements Store {

@Override
public void sell() {
System.out.println("invoke supermarket sell method");
}

}

编写工具类

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
package com.clay.loader;

import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.*;

/**
* 动态加载器
*
* @author clay
*/
public class DynamicLoader {

/**
* 编译参数
*/
private List<String> options = new ArrayList<>();

/**
* 添加编译参数
*
* @param key
* @param value
* @throws NullPointerException
*/
public void addOption(String key, String value) throws NullPointerException {
if (key == null || key.isEmpty()) {
throw new NullPointerException("Option key is empty");
}
options.add(key);
options.add(value);
}

/**
* 通过Java文件名和其代码,编译得到字节码,返回类名及其对应类的字节码,封装于Map中,
* 值得注意的是,平常类中就编译出来的字节码只有一个类,但是考虑到内部类的情况, 会出现很多个类名及其字节码,所以用Map封装方便
*
* @param javaName Java文件名,例如Student.java
* @param javaCode Java源码
* @return map
*/
public Map<String, byte[]> compile(String javaName, String javaCode) {
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager stdManager = compiler.getStandardFileManager(null, null, null);
try (MemoryJavaFileManager manager = new MemoryJavaFileManager(stdManager)) {
JavaFileObject javaFileObject = manager.makeStringSource(javaName, javaCode);
JavaCompiler.CompilationTask task = compiler.getTask(null, manager, null, options, null, Arrays.asList(javaFileObject));
if (task.call()) {
return manager.getClassBytes();
}
} catch (IOException e) {
e.printStackTrace();
}
return null;
}

/**
* 先根据类名在内存中查找是否已存在该类,若不存在则调用URLClassLoader.defineClass()方法加载该类
* URLClassLoader的具体作用就是将Class文件加载到JVM虚拟机中
*/
public static class MemoryClassLoader extends URLClassLoader {

private 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);
}

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

}
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
package com.clay.loader;

import javax.tools.*;
import java.io.*;
import java.net.URI;
import java.nio.CharBuffer;
import java.util.HashMap;
import java.util.Map;

/**
* 将编译好的Class文件保存到内存当中,这里的内存也就是Map映射当中
*
* @author clay
*/
public final class MemoryJavaFileManager extends ForwardingJavaFileManager {

/**
* 用于存放Class文件的内存
*/
private Map<String, byte[]> classBytes;

/**
* Java源文件的扩展名
*/
private final static String EXT = ".java";

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

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

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

@Override
public void flush() throws IOException {

}

@Override
public JavaFileObject getJavaFileForOutput(
JavaFileManager.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);
}
}

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

public 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");
}
}
}

/**
* 一个文件对象,用来表示从String中获取到的Source,以下内容是按照JDK给出的例子写的
*/
private static class StringInputBuffer extends SimpleJavaFileObject {

private final String code;

/**
* @param name 此文件对象表示的编译单元的name
* @param code 此文件对象表示的编译单元source的code
*/
StringInputBuffer(String name, String code) {
super(toURI(name), Kind.SOURCE);
this.code = code;
}

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

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

/**
* 将Java字节码存储到classBytes映射中的文件对象
*/
private class ClassOutputBuffer extends SimpleJavaFileObject {

private String name;

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

@Override
public OutputStream openOutputStream() {
return new FilterOutputStream(new ByteArrayOutputStream()) {
@Override
public void close() throws IOException {
out.close();
ByteArrayOutputStream bos = (ByteArrayOutputStream) out;
// 这里可能需要修改
classBytes.put(name, bos.toByteArray());
}
};
}
}

}

编写测试类

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
package com.clay.loader;

import com.clay.domain.Store;
import com.clay.domain.Supermarket;
import org.springframework.util.Assert;

import java.lang.reflect.Constructor;
import java.util.Map;

/**
* @author clay
*/
public class ProxyUtil {

/**
* 获取Java代码
*
* @return
*/
public String getJavaCode() {
String rt = "\r\n";
// 这里定义的Java类代码里,建议首行不要带包名,否则容易出现编译失败的问题
String code = "import com.clay.domain.Store;" + rt
+ "public class Dealer implements Store" + rt
+ "{" + rt
+ "private Store s;" + rt
+ "public Dealer(Store s)" + rt + " {" + " this.s = s;" + rt
+ " }" + rt
+ "@Override" + rt
+ "public void sell()" + " {" + rt
+ "System.out.println(\"invoke dealer sell method\");" + rt
+ "s.sell();" + rt
+ " }" + rt
+ "}";
return code;
}

/**
* 动态编译
*
* @throws Exception
*/
public void handle() throws Exception {
String javaName = "Dealer.java";

// 对Java代码进行编译,并将生成Class文件存放在Map中
DynamicLoader dynamicLoader = new DynamicLoader();
Map<String, byte[]> bytecode = dynamicLoader.compile(javaName, getJavaCode());

// 加载字节码到虚拟机中
DynamicLoader.MemoryClassLoader classLoader = new DynamicLoader.MemoryClassLoader(bytecode);
Class<?> clazz = classLoader.loadClass("Dealer");
Assert.notNull(clazz, "");

// 通过反射进行调用
Constructor constructor = clazz.getConstructor(Store.class);
Store store = (Store) constructor.newInstance(new Supermarket());
store.sell();
}

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

import com.clay.loader.ProxyUtil;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
* @author clay
*/
@SpringBootApplication
public class ProxyApplication {

public static void main(String[] args) {
SpringApplication.run(ProxyApplication.class, args);

try {
ProxyUtil util = new ProxyUtil();
util.handle();
} catch (Exception e) {
e.printStackTrace();
}
}

}

程序运行结果

1
2
invoke dealer sell method
invoke supermarket sell method

常见问题

动态编译时找不到第三方包的类

动态编译 Java 文件时,如果这个 Java 文件引用了第三方 Jar 包里的类,那么程序运行在 IDE 工具时,则可以正常动态编译。如果程序单独运行在 Web 容器(例如 Tomcat),又或者是直接通过 java -jar xxx.jar 的命令行方式运行,那么执行动态编译时,往往就会提示找不到第三方 Jar 包里的 Class 或者 Package,导致无法正常编译生成 Class 文件或者字节码。

解决方案:

  • 方法一:将所依赖到的第三方 Jar 文件,复制到 %JAVA_HOME%\jre\lib\ext 目录下,然后再重启 Web 容器(Tomcat)或者应用,此方法不一定兼容所有 JDK 版本,且未经验证是否有效

  • 方法二:执行动态编译时,添加 -classpath 参数来指定第三方 Jar 包的绝对路径,示例代码如下:

1
2
3
4
5
6
7
8
String jars = "/root/.m2/repository/com/clay/proxy/1.0.0/proxy-1.0.0.jar";
Iterable<String> options = Arrays.asList("-encoding", "UTF-8", "-classpath", jars);

JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager fileMgr = compiler.getStandardFileManager(null, null, null);
Iterable units = fileMgr.getJavaFileObjects(fileName);
JavaCompiler.CompilationTask task = compiler.getTask(null, fileMgr, null, options, null, units);
result = task.call();

SpringBoot 找不到动态编译后的类

在 SpringBoot 应用内执行动态编译时,可以正常生成 Class 文件,但往往无法直接通过 URLClassLoader 类加载 Class 文件来实例化 Java 对象,示例代码如下:

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
package com.clay.loader;

import javax.tools.JavaCompiler;
import javax.tools.JavaCompiler.CompilationTask;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.net.URL;
import java.net.URLClassLoader;

public class ProxyUtil {

/**
* 生成文件
*
* @param path
* @return content
*/
public boolean createFile(String path, String content) {
FileWriter fw = null;
try {
String parentPath = path.substring(0, path.lastIndexOf("/"));
File parentFile = new File(parentPath);
if (!parentFile.exists()) {
parentFile.mkdirs();
}

File javaFile = new File(path);
if (!javaFile.exists()) {
javaFile.createNewFile();
}

fw = new FileWriter(javaFile);
fw.write(content);
fw.flush();
return true;
} catch (Exception e) {
e.printStackTrace();
} finally {
if (fw != null) {
try {
fw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return false;
}

/**
* 动态编译
*
* @throws Exception
*/
public void handle() throws Exception {
String rt = "\r\n";
String outputDir = "/tmp/jdk/compile/";

// 这里定义的Java类代码里,建议首行不要带包名,否则容易出现编译失败的问题
String source = "import com.clay.domain.Store;" + rt
+ "public class Dealer implements Store" + rt
+ "{" + rt
+ "private Store s;" + rt
+ "public Dealer(Store s)" + rt + " {" + " this.s = s;" + rt
+ " }" + rt
+ "@Override" + rt
+ "public void sell()" + " {" + rt
+ "System.out.println(\"call dealer sell method\");" + rt
+ "s.sell();" + rt
+ " }" + rt
+ "}";

// Java文件的完整路径
String javaPath = outputDir + "Dealer.java";
System.out.println("===> java file path: " + javaPath);

// 生成Java文件
createFile(javaPath, source);

// 编译Java文件
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager fileMgr = compiler.getStandardFileManager(null, null, null);
Iterable units = fileMgr.getJavaFileObjects(javaPath);
CompilationTask task = compiler.getTask(null, fileMgr, null, null, null, units);
boolean result = task.call();
fileMgr.close();
System.out.println("===> compile result: " + result);

String classPath = "file:/" + outputDir;
System.out.println("===> class file path: " + classPath);

// 加载Class文件
URL[] urls = new URL[]{new URL(classPath)};
URLClassLoader ul = new URLClassLoader(urls);
Class clazz = ul.loadClass("Dealer");

// 实例化
Constructor ctr = clazz.getConstructor(Store.class);
Store s = (Store) ctr.newInstance(new Supermarket());
s.sell();
}
}

特别注意:在 SpringBoot 应用内无法正常运行上述代码,即调用 loadClass () 方法的时候会抛出异常 “java.lang.ClassNotFoundException: Dealer”

此时可以尝试使用 Thread.currentThread().getContextClassLoader() 来替代 new URLClassLoader(urls),具体的实现代码可参考开源项目 dynamic-loader,这里不再累述

开源项目

参考博客