前言
我们都知道Java
属于编译型语言
,即源码需要经过编译成字节码然后运行于JVM
我们也知道,代码一旦编写完成,编译出的.class
文件是一定的。这里也就是静态编译
。
那我们需要在运行时编译并加载应该怎么办呢,存在如下场景
- 我们熟知的类似
LeetCode
这种测评平台,需要执行用户输入的代码。
- 服务器需要动态加载某些类文件进行编译。
那么我们就要使用Java
的动态编译
能力,在运行时编译代码并加载进jvm
。
原理
从Java 6
开始,引入了Java
代码重写过的编译器接口,使得我们可以在运行时编译Java
源代码,然后再通过类加载器将编译好的类加载进JVM
,这种在运行时编译代码的操作就叫做动态编译
。
主要类库
JavaCompiler
- 表示java
编译器, run
方法执行编译操作. 还有一种编译方式是先生成编译任务(CompilationTask
), 让后调用CompilationTask
的call
方法执行编译任务
JavaFileObject
- 表示一个java
源文件对象
JavaFileManager
- Java
源文件管理类, 管理一系列JavaFileObject
Diagnostic
- 表示一个诊断信息
DiagnosticListener
- 诊断信息监听器, 编译过程触发. 生成编译task(JavaCompiler#getTask())
或获取FileManager(JavaCompiler#getStandardFileManager())
时需要传递DiagnosticListener
以便收集诊断信息
流程图
源码文件 -> 字节码文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public static void fromJavaFile() { JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null); File file = new File("/Users/oneyoung/oneyoung/project/my/code/src/main/java/top/oneyoung/dynamic/TestHello.java"); Iterable<? extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjects(file); JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, null, null, null, compilationUnits); task.call(); }
|
我们这里准备了TestHello.java
1 2 3 4 5
| public class TestHello { public static void main(String[] args) { System.out.println("this is a test"); } }
|
执行后在源文件同目录下生成了编译的class文件HelloTest.class
我们试着手动加载该class
文件,使用类加载器的defineClass
方法,可以直接加载字节码文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public static Class<?> loadClassFromDisk(String path) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { Class<ClassLoader> classLoaderClass = ClassLoader.class; Method defineClass = classLoaderClass.getDeclaredMethod("defineClass", byte[].class, int.class, int.class); defineClass.setAccessible(true); File file = new File(path); byte[] bytes = new byte[(int) file.length()]; try (FileInputStream fileInputStream = new FileInputStream(file)) { fileInputStream.read(bytes); } catch (IOException e) { e.printStackTrace(); } return (Class<?>) defineClass.invoke(Thread.currentThread().getContextClassLoader(), bytes, 0, bytes.length); }
|
由于TestHello
中的方法为静态方法,使用class反射机制执行方法
1 2 3 4 5 6 7
| Boolean call = task.call(); if (call) { Class<?> o = loadClassFromDisk("/Users/oneyoung/oneyoung/project/my/code/src/main/java/top/oneyoung/dynamic/TestHello.class"); Method main = o.getMethod("main", String[].class); main.invoke(null, new Object[]{new String[]{}}); }
|
源码字符串 -> 字节码文件
在流程图中,getTask().call()
会通过调用作为参数传入的JavaFileObject
对象的getCharContent()
方法获得字符串序列,即源码的读取是通过 JavaFileObject
的 getCharContent()
方法,那我们只需要重写getCharContent()
方法,即可将我们的字符串源码装进JavaFileObject
了。
构造SourceJavaFileObject
实现定制的JavaFileObject
对象,用于存储字符串源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public class SourceJavaFileObject extends SimpleJavaFileObject {
private final String code;
SourceJavaFileObject(String name, String code) { super(URI.create("string:///" + name.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE); this.code = code; }
@Override public CharSequence getCharContent(boolean ignoreEncodingErrors) { return code; } }
|
则创建JavaFileObject
对象时,变为了:
1 2 3 4
| SourceJavaFileObject javaFileObject = new SourceJavaFileObject("TestHello", "public class TestHello { public static void main(String[] args) { System.out.println(\"Hello World\"); } }");
JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, null, null, null, Collections.singleton(javaFileObject));
|
执行后,同样编译出了class
文件,不过由于没有指定编译的class
输出路径,他会默认放在源文件的根目录下
后面同样可以通过defindClass
加载字节码完成加载。
源码字符串 -> 字节码数组
如果我们进行动态编译时,想要直接输入源码字符串并且输出的是字节码数组,而不是输出字节码文件,又该如何实现?实际上,这是从内存中得到源码,再输出到内存的方式。
在getTask().call()
源代码执行流程图中,我们可以发现JavaFileObject
的 openOutputStream()
方法控制了编译后字节码的输出行为,编译完成后会调用openOutputStream
获取输出流,并写数据(字节码)。所以我们需要重写JavaFileObject
的 openOutputStream()
方法。
同时在执行流程图中,我们还发现用于输出的JavaFileObject
对象是JavaFileManager
的getJavaFileForOutput()
方法提供的,所以为了让编译器编译完成后,将编译得到的字节码输出到我们自己构造的JavaFileObject
对象,我们还需要自定义JavaFileManager`。
这里我使用类委托的方式,把大部分功能委托给了传入的StandardJavaFileManager
,主要是重写了getJavaFileForOutput
,使输出编译完成的字节码文件为字节数组。
然后增加了方法getBytesByClassName
获取编译完成的字节码字节数组
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
| public class ByteArrayJavaFileManager implements JavaFileManager { private static final Logger LOG = LoggerFactory.getLogger(ByteArrayJavaFileManager.class);
private final StandardJavaFileManager fileManager;
private final Map<String, ByteArrayOutputStream> buffers = Collections.synchronizedMap(new LinkedHashMap<>());
public ByteArrayJavaFileManager(StandardJavaFileManager fileManager) { this.fileManager = fileManager; }
@Override public ClassLoader getClassLoader(Location location) { return fileManager.getClassLoader(location); }
@Override public synchronized Iterable<JavaFileObject> list(Location location, String packageName, Set<Kind> kinds, boolean recurse) throws IOException { return fileManager.list(location, packageName, kinds, recurse); }
@Override public String inferBinaryName(Location location, JavaFileObject file) { return fileManager.inferBinaryName(location, file); }
@Override public boolean isSameFile(FileObject a, FileObject b) { return fileManager.isSameFile(a, b); }
@Override public synchronized boolean handleOption(String current, Iterator<String> remaining) { return fileManager.handleOption(current, remaining); }
@Override public boolean hasLocation(Location location) { return fileManager.hasLocation(location); }
@Override public JavaFileObject getJavaFileForInput(Location location, String className, Kind kind) throws IOException {
if (location == StandardLocation.CLASS_OUTPUT) { boolean success; final byte[] bytes; synchronized (buffers) { success = buffers.containsKey(className) && kind == Kind.CLASS; bytes = buffers.get(className).toByteArray(); } if (success) {
return new SimpleJavaFileObject(URI.create(className), kind) { @Override public InputStream openInputStream() { return new ByteArrayInputStream(bytes); } }; } } return fileManager.getJavaFileForInput(location, className, kind); }
@Override public JavaFileObject getJavaFileForOutput(Location location, final String className, Kind kind, FileObject sibling) { return new SimpleJavaFileObject(URI.create(className), kind) { @Override public OutputStream openOutputStream() { ByteArrayOutputStream bos = new ByteArrayOutputStream(); buffers.putIfAbsent(className, bos); return bos; } }; }
@Override public FileObject getFileForInput(Location location, String packageName, String relativeName) throws IOException { return fileManager.getFileForInput(location, packageName, relativeName); }
@Override public FileObject getFileForOutput(Location location, String packageName, String relativeName, FileObject sibling) throws IOException { return fileManager.getFileForOutput(location, packageName, relativeName, sibling); }
@Override public void flush() { }
@Override public void close() throws IOException { fileManager.close(); }
@Override public int isSupportedOption(String option) { return fileManager.isSupportedOption(option); }
public void clearBuffers() { buffers.clear(); }
public Map<String, byte[]> getAllBuffers() { Map<String, byte[]> ret = new LinkedHashMap<>(buffers.size() * 2); Map<String, ByteArrayOutputStream> compiledClasses = new LinkedHashMap<>(ret.size()); synchronized (buffers) { compiledClasses.putAll(buffers); } compiledClasses.forEach((k, v) -> ret.put(k, v.toByteArray())); return ret; }
public byte[] getBytesByClassName(String className) { return buffers.get(className).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
| public static void fromJavaSourceToByteArray1() throws Exception { JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null); ByteArrayJavaFileManager byteArrayJavaFileManager = new ByteArrayJavaFileManager(fileManager); JavaFileObject javaFileObject = new SourceJavaFileObject("TestHello", "public class TestHello { public static void say(String args) { System.out.println(args); } }");
JavaCompiler.CompilationTask task = compiler.getTask(null, byteArrayJavaFileManager, null, null, null, Collections.singletonList(javaFileObject)); Boolean call = task.call(); if (Boolean.TRUE.equals(call)) { byte[] testHellos = byteArrayJavaFileManager.getBytesByClassName("TestHello"); Class<ClassLoader> classLoaderClass = ClassLoader.class; Method defineClass = classLoaderClass.getDeclaredMethod("defineClass", byte[].class, int.class, int.class); defineClass.setAccessible(true); Object invoke = defineClass.invoke(TestHello.class.getClassLoader(), testHellos, 0, testHellos.length); Class clazz = (Class) invoke; clazz.getMethod("say", String.class).invoke(null, "你好");
} }
|
成功输出