前提
笔者很久之前就有个想法:参考现有的主流ORM框架的设计,造一个ORM轮子,在基本不改变使用体验的前提下把框架依赖的大量的反射设计去掉,这些反射API构筑的组件使用「动态编译」加载的实例去替代,从而可以得到接近于直接使用原生JDBC的性能。于是带着这样的想法,深入学习Java的动态编译。编写本文的时候使用的是JDK11。
基本原理
下面这个很眼熟的图来源于《深入理解Java虚拟机》前端编译与优化的章节,主要描述编译的过程:
上图看起来只有三步,其实每一步都有大量的步骤,下图尝试相对详细地描述具体的步骤(图比较大难以分割,直接放原图):
实际上,仅仅对于编译这个过程来说,开发者或者使用者不必要完全掌握其中的细节,JDK提供了一个工具包javax.tools让使用者可以用简易的API进行编译(其实在大多数情况下,开发者是面向业务功能开发,像编译和打包这些细节一般直接由开发工具、Maven、Gradle等工具完成):
具体的使用过程包括:
- 获取一个
javax.tools.JavaCompiler实例。 - 基于
Java文件对象初始化一个编译任务javax.tools.JavaCompiler$CompilationTask实例。 CompilationTask实例执行结果代表着编译过程的成功与否。
❝我们熟知的
❞javac编译器其实就是JavaCompiler接口的实现,在JDK1.6+中,对应的实现类为com.sun.tools.javac.api.JavacTool。
因为JVM里面的Class是基于ClassLoader隔离的,所以编译成功之后可以通过自定义的类加载器加载对应的类实例,然后就可以应用反射API进行实例化和后续的调用。
JDK动态编译
JDK动态编译的步骤在上一节已经清楚地说明,这里造一个简单的场景。假设存在一个接口如下:
package club.throwable.compile; public interface HelloService { void sayHello(String name); } // 默认实现 package club.throwable.compile; public class DefaultHelloService implements HelloService { @Override public void sayHello(String name) { System.out.println(String.format("%s say hello [by default]", name)); } } 复制代码
我们可以通过字符串SOURCE_CODE定义一个类:
static String SOURCE_CODE = "package club.throwable.compile;\n" + "\n" + "public class JdkDynamicCompileHelloService implements HelloService{\n" + "\n" + " @Override\n" + " public void sayHello(String name) {\n" + " System.out.println(String.format(\"%s say hello [by jdk dynamic compile]\", name));\n" + " }\n" + "}"; // 这里不需要定义类文件,还原类文件内容如下 package club.throwable.compile; public class JdkDynamicCompileHelloService implements HelloService{ @Override public void sayHello(String name) { System.out.println(String.format("%s say hello [by jdk dynamic compile]", name)); } } 复制代码
在组装编译任务实例之前,还有几项工作需要完成:
- 内置的
JavaFileObject标准实现SimpleJavaFileObject是面向类源码文件,由于动态编译时候输入的是类源码文件的内容字符串,需要自行实现JavaFileObject。 - 内置的
JavaFileManager是面向类路径下的Java源码文件进行加载,这里也需要自行实现JavaFileManager。 - 需要自定义一个
ClassLoader实例去加载编译出来的动态类。
实现JavaFileObject
自行实现一个JavaFileObject,其实可以简单点直接继承SimpleJavaFileObject,覆盖需要用到的方法即可:
public class CharSequenceJavaFileObject extends SimpleJavaFileObject { public static final String CLASS_EXTENSION = ".class"; public static final String JAVA_EXTENSION = ".java"; private static URI fromClassName(String className) { try { return new URI(className); } catch (URISyntaxException e) { throw new IllegalArgumentException(className, e); } } private ByteArrayOutputStream byteCode; private final CharSequence sourceCode; public CharSequenceJavaFileObject(String className, CharSequence sourceCode) { super(fromClassName(className + JAVA_EXTENSION), Kind.SOURCE); this.sourceCode = sourceCode; } public CharSequenceJavaFileObject(String fullClassName, Kind kind) { super(fromClassName(fullClassName), kind); this.sourceCode = null; } public CharSequenceJavaFileObject(URI uri, Kind kind) { super(uri, kind); this.sourceCode = null; } @Override public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException { return sourceCode; } @Override public InputStream openInputStream() { return new ByteArrayInputStream(getByteCode()); } // 注意这个方法是编译结果回调的OutputStream,回调成功后就能通过下面的getByteCode()方法获取目标类编译后的字节码字节数组 @Override public OutputStream openOutputStream() { return byteCode = new ByteArrayOutputStream(); } public byte[] getByteCode() { return byteCode.toByteArray(); } } 复制代码
如果编译成功之后,直接通过自行添加的CharSequenceJavaFileObject#getByteCode()方法即可获取目标类编译后的字节码对应的字节数组(二进制内容)。这里的CharSequenceJavaFileObject预留了多个构造函数用于兼容原有的编译方式。
实现ClassLoader
只要简单继承ClassLoader即可,关键是要覆盖原来的ClassLoader#findClass()方法,用于搜索自定义的JavaFileObject实例,从而提取对应的字节码字节数组进行装载,为了实现这一点可以添加一个哈希表作为缓存,键-值分别是全类名的别名(xx.yy.MyClass形式,而非URI模式)和目标类对应的JavaFileObject实例。
public class JdkDynamicCompileClassLoader extends ClassLoader { public static final String CLASS_EXTENSION = ".class"; private final Map<String, JavaFileObject> javaFileObjectMap = Maps.newConcurrentMap(); public JdkDynamicCompileClassLoader(ClassLoader parentClassLoader) { super(parentClassLoader); } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { JavaFileObject javaFileObject = javaFileObjectMap.get(name); if (null != javaFileObject) { CharSequenceJavaFileObject charSequenceJavaFileObject = (CharSequenceJavaFileObject) javaFileObject; byte[] byteCode = charSequenceJavaFileObject.getByteCode(); return defineClass(name, byteCode, 0, byteCode.length); } return super.findClass(name); } @Nullable @Override public InputStream getResourceAsStream(String name) { if (name.endsWith(CLASS_EXTENSION)) { String qualifiedClassName = name.substring(0, name.length() - CLASS_EXTENSION.length()).replace('/', '.'); CharSequenceJavaFileObject javaFileObject = (CharSequenceJavaFileObject) javaFileObjectMap.get(qualifiedClassName); if (null != javaFileObject && null != javaFileObject.getByteCode()) { return new ByteArrayInputStream(javaFileObject.getByteCode()); } } return super.getResourceAsStream(name); } // 暂时存放编译的源文件对象,key为全类名的别名(非URI模式),如club.throwable.compile.HelloService void addJavaFileObject(String qualifiedClassName, JavaFileObject javaFileObject) { javaFileObjectMap.put(qualifiedClassName, javaFileObject); } Collection<JavaFileObject> listJavaFileObject() { return Collections.unmodifiableCollection(javaFileObjectMap.values()); } } 复制代码
实现JavaFileManager
JavaFileManager是Java文件的抽象管理器,它用于管理常规的Java文件,但是不局限于文件,也可以管理其他来源的Java类文件数据。下面就通过实现一个自定义的JavaFileManager用于管理字符串类型的源代码。为了简单起见,可以直接继承已经存在的ForwardingJavaFileManager:
public class JdkDynamicCompileJavaFileManager extends ForwardingJavaFileManager<JavaFileManager> { private final JdkDynamicCompileClassLoader classLoader; private final Map<URI, JavaFileObject> javaFileObjectMap = Maps.newConcurrentMap(); public JdkDynamicCompileJavaFileManager(JavaFileManager fileManager, JdkDynamicCompileClassLoader classLoader) { super(fileManager); this.classLoader = classLoader; } private static URI fromLocation(Location location, String packageName, String relativeName) { try { return new URI(location.getName() + '/' + packageName + '/' + relativeName); } catch (URISyntaxException e) { throw new IllegalArgumentException(e); } } @Override public FileObject getFileForInput(Location location, String packageName, String relativeName) throws IOException { JavaFileObject javaFileObject = javaFileObjectMap.get(fromLocation(location, packageName, relativeName)); if (null != javaFileObject) { return javaFileObject; } return super.getFileForInput(location, packageName, relativeName); } // 这里是编译器返回的同(源)Java文件对象,替换为CharSequenceJavaFileObject实现 @Override public JavaFileObject getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind, FileObject sibling) throws IOException { JavaFileObject javaFileObject = new CharSequenceJavaFileObject(className, kind); classLoader.addJavaFileObject(className, javaFileObject); return javaFileObject; } // 这里覆盖原来的类加载器 @Override public ClassLoader getClassLoader(Location location) { return classLoader; } @Override public String inferBinaryName(Location location, JavaFileObject file) { if (file instanceof CharSequenceJavaFileObject) { return file.getName(); } return super.inferBinaryName(location, file); } @Override public Iterable<JavaFileObject> list(Location location, String packageName, Set<JavaFileObject.Kind> kinds, boolean recurse) throws IOException { Iterable<JavaFileObject> superResult = super.list(location, packageName, kinds, recurse); List<JavaFileObject> result = Lists.newArrayList(); // 这里要区分编译的Location以及编译的Kind if (location == StandardLocation.CLASS_PATH && kinds.contains(JavaFileObject.Kind.CLASS)) { // .class文件以及classPath下 for (JavaFileObject file : javaFileObjectMap.values()) { if (file.getKind() == JavaFileObject.Kind.CLASS && file.getName().startsWith(packageName)) { result.add(file); } } // 这里需要额外添加类加载器加载的所有Java文件对象 result.addAll(classLoader.listJavaFileObject()); } else if (location == StandardLocation.SOURCE_PATH && kinds.contains(JavaFileObject.Kind.SOURCE)) { // .java文件以及编译路径下 for (JavaFileObject file : javaFileObjectMap.values()) { if (file.getKind() == JavaFileObject.Kind.SOURCE && file.getName().startsWith(packageName)) { result.add(file); } } } for (JavaFileObject javaFileObject : superResult) { result.add(javaFileObject); } return result; } // 自定义方法,用于添加和缓存待编译的源文件对象 public void addJavaFileObject(Location location, String packageName, String relativeName, JavaFileObject javaFileObject) { javaFileObjectMap.put(fromLocation(location, packageName, relativeName), javaFileObject); } } 复制代码
注意在这个类中引入了自定义类加载器JdkDynamicCompileClassLoader,目的是为了实现JavaFileObject实例的共享以及为文件管理器提供类加载器实例。
动态编译和运行
前置准备工作完成,我们可以通过JavaCompiler去编译这个前面提到的字符串,为了字节码的兼容性更好,编译的时候可以指定稍低的JDK版本例如1.6:
public class Client { static String SOURCE_CODE = "package club.throwable.compile;\n" + "\n" + "public class JdkDynamicCompileHelloService implements HelloService{\n" + "\n" + " @Override\n" + " public void sayHello(String name) {\n" + " System.out.println(String.format(\"%s say hello [by jdk dynamic compile]\", name));\n" + " }\n" + "}"; // 编译诊断收集器 static DiagnosticCollector<JavaFileObject> DIAGNOSTIC_COLLECTOR = new DiagnosticCollector<>(); public static void main(String[] args) throws Exception { // 获取系统编译器实例 JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); // 设置编译参数 - 指定编译版本为JDK1.6以提高兼容性 List<String> options = new ArrayList<>(); options.add("-source"); options.add("1.6"); options.add("-target"); options.add("1.6"); // 获取标准的Java文件管理器实例 StandardJavaFileManager manager = compiler.getStandardFileManager(DIAGNOSTIC_COLLECTOR, null, null); // 初始化自定义类加载器 JdkDynamicCompileClassLoader classLoader = new JdkDynamicCompileClassLoader(Thread.currentThread().getContextClassLoader()); // 初始化自定义Java文件管理器实例 JdkDynamicCompileJavaFileManager fileManager = new JdkDynamicCompileJavaFileManager(manager, classLoader); String packageName = "club.throwable.compile"; String className = "JdkDynamicCompileHelloService"; String qualifiedName = packageName + "." + className; // 构建Java源文件实例 CharSequenceJavaFileObject javaFileObject = new CharSequenceJavaFileObject(className, SOURCE_CODE); // 添加Java源文件实例到自定义Java文件管理器实例中 fileManager.addJavaFileObject( StandardLocation.SOURCE_PATH, packageName, className + CharSequenceJavaFileObject.JAVA_EXTENSION, javaFileObject ); // 初始化一个编译任务实例 JavaCompiler.CompilationTask compilationTask = compiler.getTask( null, fileManager, DIAGNOSTIC_COLLECTOR, options, null, Lists.newArrayList(javaFileObject) ); // 执行编译任务 Boolean result = compilationTask.call(); System.out.println(String.format("编译[%s]结果:%s", qualifiedName, result)); Class<?> klass = classLoader.loadClass(qualifiedName); HelloService instance = (HelloService) klass.getDeclaredConstructor().newInstance(); instance.sayHello("throwable"); } } 复制代码
输出结果如下:
编译[club.throwable.compile.JdkDynamicCompileHelloService]结果:true throwable say hello [by jdk dynamic compile] 复制代码
可见通过了字符串的类源码,实现了动态编译、类加载、反射实例化以及最终的方法调用。另外,编译过程的诊断信息可以通过DiagnosticCollector实例获取。为了复用,这里可以把JDK动态编译的过程抽取到一个方法中:
public final class JdkCompiler { static DiagnosticCollector<JavaFileObject> DIAGNOSTIC_COLLECTOR = new DiagnosticCollector<>(); @SuppressWarnings("unchecked") public static <T> T compile(String packageName, String className, String sourceCode, Class<?>[] constructorParamTypes, Object[] constructorParams) throws Exception { // 获取系统编译器实例 JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); // 设置编译参数 List<String> options = new ArrayList<>(); options.add("-source"); options.add("1.6"); options.add("-target"); options.add("1.6"); // 获取标准的Java文件管理器实例 StandardJavaFileManager manager = compiler.getStandardFileManager(DIAGNOSTIC_COLLECTOR, null, null); // 初始化自定义类加载器 JdkDynamicCompileClassLoader classLoader = new JdkDynamicCompileClassLoader(Thread.currentThread().getContextClassLoader()); // 初始化自定义Java文件管理器实例 JdkDynamicCompileJavaFileManager fileManager = new JdkDynamicCompileJavaFileManager(manager, classLoader); String qualifiedName = packageName + "." + className; // 构建Java源文件实例 CharSequenceJavaFileObject javaFileObject = new CharSequenceJavaFileObject(className, sourceCode); // 添加Java源文件实例到自定义Java文件管理器实例中 fileManager.addJavaFileObject( StandardLocation.SOURCE_PATH, packageName, className + CharSequenceJavaFileObject.JAVA_EXTENSION, javaFileObject ); // 初始化一个编译任务实例 JavaCompiler.CompilationTask compilationTask = compiler.getTask( null, fileManager, DIAGNOSTIC_COLLECTOR, options, null, Lists.newArrayList(javaFileObject) ); Boolean result = compilationTask.call(); System.out.println(String.format("编译[%s]结果:%s", qualifiedName, result)); Class<?> klass = classLoader.loadClass(qualifiedName); return (T) klass.getDeclaredConstructor(constructorParamTypes).newInstance(constructorParams); } } 复制代码



