写过规则引擎的同学都知道drools语言,我们都通过一个drools容器来加载并执行drools写的各种规则,也玩过通过Java的脚本引擎执行过Javascript代码.这些动态加载并运行代码主要是用于编写不同规则,而非在代码中写满各种ifelse判断.
有的开发同学可能会想,Java语言可以作为想动态语言一样使用吗?答案是可以的,下面我们就开始!

简单实现

sun公司在jdk1.6后就正式发布了关于Java编译器的API,下面我们直接看一个简单的例子:

1
2
3
4
5
6
7
8
9
10
@Test
public void simpleCode() throws Exception {
JavaCompiler javaCompiler = ToolProvider.getSystemJavaCompiler();
int run = javaCompiler.run(null, null, null, "file/SimpleBean.java");
if (run == 0) {
log.info("编译成功");
} else {
log.error("编译失败");
}
}

上面代码是直接对Java源码文件进行编译,编译后的class文件会存在和源码的同一个目录下.

带有诊断器的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 带有诊断器,编译本地磁盘上源码
@Test
public void templateCode() throws Exception {
JavaCompiler javaCompiler = ToolProvider.getSystemJavaCompiler();
// 诊断收集器
DiagnosticCollector<JavaFileObject> diagnosticCollector = new DiagnosticCollector<JavaFileObject>();
StandardJavaFileManager standardFileManager = javaCompiler.getStandardFileManager(diagnosticCollector, null, null);
Iterable<? extends JavaFileObject> javaFileObjects = standardFileManager.getJavaFileObjects("/Users/sanxing/blog/note/code/file/SimpleBean.java");
JavaCompiler.CompilationTask compilerTask = javaCompiler.getTask(null, standardFileManager, diagnosticCollector, null, null, javaFileObjects);
Boolean call = compilerTask.call();
if (call) {
log.info("编译成功");
} else {
log.error("编译失败");
diagnosticCollector.getDiagnostics().forEach(diagnostic -> {
log.error(diagnostic.getKind().name());
log.error(diagnostic.getCode());
log.error(diagnostic.getSource().getName() + ">" + diagnostic.getLineNumber() + ":" + diagnostic.getColumnNumber()
+ ":" + diagnostic.getMessage(Locale.CHINA));
});
}
}

当编译失败后,诊断器会获取编译失败的源码文件名,行数和列数以及失败的具体原因,比如局部变量未初始化使用.

1
2
3
4
5
6
7
8
9
10
11
public class SimpleBean {
public String whoami(){
int i;
return "my name is SimpleBean for testing."+i;
}
}
// 编译结果
17:11:13:133|ERROR|main|52|编译失败
17:11:13:184|ERROR|main|54|ERROR
17:11:13:184|ERROR|main|55|compiler.err.var.might.not.have.been.initialized
17:11:13:188|ERROR|main|56|/Users/sanxing/blog/note/code/file/SimpleBean.java>4:53:可能尚未初始化变量i

复杂实现

当我们需要自己在程序运行时候编译Java源码的情况下,大部分源码并非是在磁盘上,很有可能是数据库中.那么我们如何实现呢?下面的例子将会展现Java源码的编译,源码的自由获取,源码编译后的字节码加载到jvm中类加载器并运行其中的方法.

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
@Test
public void forwardingJavaFileManagerCodeWithInvoke() throws Exception {
JavaCompiler javaCompiler = ToolProvider.getSystemJavaCompiler();
// 诊断收集器
DiagnosticCollector<JavaFileObject> diagnosticCollector = new DiagnosticCollector<JavaFileObject>();
// 获取标准Java文件管理器
StandardJavaFileManager standardFileManager = javaCompiler.getStandardFileManager(diagnosticCollector, Locale.CHINA, Charset.forName("UTF-8"));
List<ClassByteFileObject> classFileList = new ArrayList<>();
// 创建标准Java文件包装器
ForwardingJavaFileManager forwardingJavaFileManager = new ForwardingJavaFileManager(standardFileManager) {
@Override
public JavaFileObject getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind, FileObject sibling) throws IOException {
// 设置编译后的class对象输出对象
ClassByteFileObject javaFileObject = new ClassByteFileObject(className);
classFileList.add(javaFileObject);
return javaFileObject;
}
};
// 创建Java编译任务
JavaCompiler.CompilationTask compilerTask = javaCompiler.getTask(null, // 错误输出,null则打印控制台
forwardingJavaFileManager, // 设置编译后对象输入文件管理器
diagnosticCollector, // 设置诊断器
null, null,
Collections.singleton(new JavaSourceFileObject("SimpleBean.java",getSource("file/SimpleBean.java")))// 设置源文件管理器,我们可以从任何地方加载,包括DB
);
Boolean call = compilerTask.call();
if (call) {
log.info("编译成功!");
} else {
log.error("编译失败!");
diagnosticCollector.getDiagnostics().forEach(diagnostic -> {
log.error(diagnostic.getKind().name());
log.error(diagnostic.getCode());
log.error(diagnostic.getSource().getName() + ">" + diagnostic.getLineNumber() + ":" + diagnostic.getColumnNumber()
+ ":" + diagnostic.getMessage(Locale.CHINA));
});
}

// 加载编译好的类并调用
ByteClassLoader byteClassLoader = new ByteClassLoader(classFileList.get(0).getBytes());
Class<?> aClass = byteClassLoader.findClass("SimpleBean");
Object whoami = aClass.getMethod("whoami").invoke(aClass.newInstance());
log.info(s(whoami));
}

// 加载字节类加载器
class ByteClassLoader extends ClassLoader{
private byte[] bytes;

public ByteClassLoader(byte[] bytes) {
this.bytes = bytes;
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
return defineClass(name,bytes,0,bytes.length);
}
}

// 加载Java源码字符串,可以是任何来源
private String getSource(String path) throws IOException {
StringBuilder sb = new StringBuilder();
List<String> allLines = Files.readAllLines(Paths.get(path));
allLines.forEach(line->{
sb.append(line);
});
return sb.toString();
}

// 类编译后的文件输出对象
class ClassByteFileObject extends SimpleJavaFileObject {
private ByteArrayOutputStream stream;

public ClassByteFileObject(String name) {
super(URI.create("bytes:///" + name), Kind.CLASS);
stream = new ByteArrayOutputStream();
}
// 表示类文件输出
@Override
public OutputStream openOutputStream() throws IOException {
return stream;
}
public byte[] getBytes() {
return stream.toByteArray();
}
}

// Java源码文件对象
class JavaSourceFileObject extends SimpleJavaFileObject {
private String source;
public JavaSourceFileObject(String name,String source) throws IOException {
super(URI.create("string:///" + name), Kind.SOURCE);
this.source = source;
}

@Override
public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
return source;
}
}

上面代码的简介:

  • 如果要获取到编译后的字节码的字节,我们需要定制自己的JavaFileObject来装载编译结果.
  • 编译的结果获取需要通过文件管理器的包装类ForwardingJavaFileManager.getJavaFileForOutput()的方法中设置我们定义的文件对象ClassByteFileObject
  • 自定义文件对象来获取输出结果需要继承SimpleJavaFileObject并重写openOutputStream方法.构造器中Kind为CLASS.
  • 我们把输入的结果存入到classFileList中,下面类加载需要用到.
  • 源码的输入同样需要源码的文件对象JavaSourceFileObject继承并重写SimpleJavaFileObject.getCharContent方法来自定义源文件的字符串,这样我们可以把远程加载过来的Java源码包装成Java文件对象.
  • 定义自己的类字节码加载器ByteClassLoader用来加载编译后的class,然后通过反射调用目标方法.

运行结果:

1
2
17:31:47:745|INFO |main|90|编译成功!
17:31:47:747|INFO |main|105|my name is SimpleBean for testing.

参考文献

  • Java doc
  • 《Java核心技术二》