Java 动态编译在项目中的实践分享
作者:政采云技术
1、什么是动态编译
在 Java 中,动态编译是指在运行时动态地编译 Java 源代码,生成字节码,并加载到 JVM 中执行。动态编译可以用于实现动态代码生成、动态加载、插件化等功能。
1.1、动态编译的相关概念
JavaFileManager 对象:用于管理编译过程中的文件。
- JavaFileManager 是一个接口,提供了对 Java 文件的管理功能,包括创建、查找、读写等操作。JavaFileManager 有多种实现方式,例如 StandardJavaFileManager、ForwardingJavaFileManager 等。
DiagnosticListener 对象:用于收集编译时的诊断信息。
- DiagnosticListener 是一个接口,用于接收编译时的诊断信息,例如错误、警告等。
JavaFileObject 对象:表示要编译的 Java 源代码。
- JavaFileObject 是一个抽象类,用于表示 Java 源代码或字节码。JavaFileObject 有多种实现方式,例如 SimpleJavaFileObject、JavaFileObjectWrapper 等。
1.2、如何简单的实现动态编译
- 创建一个 JavaCompiler 对象,该对象用于编译 Java 源代码。
- 创建一个 DiagnosticCollector 对象,该对象用于收集编译时的诊断信息。
- 创建一个 JavaFileManager 对象,该对象用于管理编译过程中的文件。
- 创建一个 JavaFileObject 对象,该对象用于表示要编译的 Java 源代码。
- 调用 JavaCompiler 对象的 getTask 方法,传入 JavaFileManager 对象和 DiagnosticCollector 对象,获取一个 CompilationTask 对象。
- 调用 CompilationTask 对象的 call 方法,编译 Java 源代码。
- 获取 DiagnosticCollector 对象的诊断信息,并处理编译结果。
下面是一个简单的示例,演示如何使用动态编译:
public class DynamicCompiler { public static void main(String[] args) throws Exception { // 创建 JavaCompiler 对象 JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); // 创建 DiagnosticCollector 对象,用于收集编译时的诊断信息 DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>(); // 创建 JavaFileManager 对象,用于管理编译过程中的文件 StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnostics, null, null); // 创建 JavaFileObject 对象,用于表示要编译的 Java 源代码 String code = "public class HelloWorld { public static void main(String[] args) { System.out.println("Hello World!"); } }"; JavaFileObject source = new JavaSourceFromString("HelloWorld", code); // 获取 CompilationTask 对象 Iterable<? extends JavaFileObject> compilationUnits = Arrays.asList(source); CompilationTask task = compiler.getTask(null, fileManager, diagnostics, null, null, compilationUnits); // 编译 Java 源代码 boolean success = task.call(); // 获取诊断信息 List<Diagnostic<? extends JavaFileObject>> messages = diagnostics.getDiagnostics(); for (Diagnostic<? extends JavaFileObject> message : messages) { System.out.println(message.getMessage(null)); } // 处理编译结果 if (success) { System.out.println("Compilation was successful."); } else { System.out.println("Compilation failed."); } fileManager.close(); } } class JavaSourceFromString extends SimpleJavaFileObject { final String code; JavaSourceFromString(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; } }
运行结果:
Hello World!
Compilation was successful.
2、如何结合 springboot 项目使用
上面展示了如何简单使用 Java 的动态编译功能,但是在日常项目开发中,会面对更多的场景。结合前言中我所遇到的问题,我简单的给大家介绍下我在项目中是如何使用 Java 的动态编译功能来解决我所遇到的问题的。
我当时的想法是这样的:
这样,各个业务方就可以自己管理自己的代码块,与外部对接或者修改代码无需在发布应用,彻底解放了我,让我有更多的精力给公司做更重要的事情!
2.1、动态编译在项目中遇到的问题
2.1.1、必须重写类加载器新编译的代码才能生效
在 Java 中使用动态编译功能时,重写类加载器是必要的。这是因为动态编译生成的类需要加载到 JVM 中执行,而默认的类加载器无法加载动态生成的类。
在 Java 中,类加载器分为三种:启动类加载器、扩展类加载器和应用程序类加载器。默认情况下,Java 使用应用程序类加载器来加载类。应用程序类加载器只能加载预先编译好的类,无法加载动态生成的类。因此,我们需要重写类加载器,使其能够加载动态生成的类。
重写类加载器有两种方式:继承 ClassLoader 类或实现 ClassLoader 接口。一般情况下,我们建议使用继承 ClassLoader 类的方式,因为这样可以更方便地控制类加载的过程。
当我们重写类加载器时,需要实现 findClass 方法。findClass 方法用于查找指定名称的类。如果类已经被加载过,可以直接返回已加载的类;否则,需要使用动态编译生成类的字节码,并通过 defineClass 方法将其加载到 JVM 中执行。
2.1.2、没有依赖的简单代码可以编译成功,但是一旦有依赖关系,编译就会失败
Java 编译器是通过 JavaFileManager 来加载相关依赖类的,如果不重写使用的是默认的 JavaFileManager 来获取 springboot 的 jarFile 来读取嵌套 jar,自然是获取不到的,需要我们重写 JavaFileManager,去获取编译代码所需的依赖,具体写法详见 2.2 代码示例。
2.2、代码示例
// 通过调用这个方法即可实现 java 的动态编译功能啦 public static Class compile(String className, String code) { try (MemoryClassLoader loader = MemoryClassLoader.genInstance()) { loader.registerJava(className, code); return MemoryClassLoader.getInstance().loadClass(className); } catch (Exception e) { // ignore } } }
public class MemoryClassLoader extends URLClassLoader { private static final Map<String, byte[]> classBytes = new ConcurrentHashMap<>(); private MemoryClassLoader() { super(new URL[0], MemoryClassLoader.class.getClassLoader()); } private static final Map<String, MemoryClassLoader> CLASSLOADER_MAP = new ConcurrentHashMap<String, MemoryClassLoader>() {{ put(KEY_CLASSLOADER, new MemoryClassLoader()); }}; private static final String KEY_CLASSLOADER = "key_classloader"; /** * 注册 Java 字符串到内存类加载器中 */ public void registerJava(String className, String javaCode) { try { Map<String, byte[]> compile = compile(className, javaCode); if (null != compile) { classBytes.putAll(compile); } } catch (Exception e) { e.printStackTrace(); } } /** * 编译 Java 代码 */ private static Map<String, byte[]> compile(String className, String javaCode) { JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); StandardJavaFileManager stdManager = getStandardFileManager(null, null, null); try (MemoryJavaFileManager manager = new MemoryJavaFileManager(stdManager)) { JavaFileObject javaFileObject = manager.makeStringSource(className, javaCode); JavaCompiler.CompilationTask task = compiler.getTask(null, manager, null, null, null, Collections.singletonList(javaFileObject)); Boolean result = task.call(); if (result != null && result) { return manager.getClassBytes(); } } return null; } @Override public Class<?> findClass(String name) throws ClassNotFoundException { byte[] buf = classBytes.get(name); if (buf == null) { return super.findClass(name); } return defineClass(name, buf, 0, buf.length); } @Override public void close() { classBytes.clear(); CLASSLOADER_MAP.clear(); } /** * 自定义 Java 文件管理器 */ public static SpringJavaFileManager getStandardFileManager(DiagnosticListener<? super JavaFileObject> var1, Locale var2, Charset var3) { Context var4 = new Context(); var4.put(Locale.class, var2); if (var1 != null) { var4.put(DiagnosticListener.class, var1); } PrintWriter var5 = var3 == null ? new PrintWriter(System.err, true) : new PrintWriter(new OutputStreamWriter(System.err, var3), true); var4.put(Log.outKey, var5); return new SpringJavaFileManager(var4, true, var3); } /** * 获取实例 */ public static MemoryClassLoader getInstance() { return CLASSLOADER_MAP.get(KEY_CLASSLOADER); } /** * 生成新的实例 */ public static MemoryClassLoader genInstance() { MemoryClassLoader classLoader = new MemoryClassLoader(); CLASSLOADER_MAP.put(KEY_CLASSLOADER, new MemoryClassLoader()); return classLoader; } public static String getPath() { ApplicationHome home = new ApplicationHome(MemoryJavaFileManager.class); String path = home.getSource().getPath(); return path; } public static boolean isJar() { return getPath().endsWith(".jar"); } }
class MemoryJavaFileManager extends ForwardingJavaFileManager<JavaFileManager> { // compiled classes in bytes: final Map<String, byte[]> classBytes = new HashMap<>(); final Map<String, List<JavaFileObject>> classObjectPackageMap = new HashMap<>(); private JavacFileManager javaFileManager; /** * key 包名 value javaobj 主要给 jdk 编译 class 的时候找依赖 class 用 */ public final static Map<String, List<JavaFileObject>> CLASS_OBJECT_PACKAGE_MAP = new HashMap<>(); private static final Object lock = new Object(); private boolean isInit = false; public void init() { try { String jarBaseFile = MemoryClassLoader.getPath(); JarFile jarFile = new JarFile(new File(jarBaseFile)); List<JarEntry> entries = jarFile.stream().filter(jarEntry -> jarEntry.getName().endsWith(".jar")).collect(Collectors.toList()); JarFile libTempJarFile; List<JavaFileObject> onePackageJavaFiles; String packageName; for (JarEntry entry : entries) { libTempJarFile = jarFile.getNestedJarFile(jarFile.getEntry(entry.getName())); if (libTempJarFile.getName().contains("tools.jar")) { continue; } Enumeration<JarEntry> tempEntriesEnum = libTempJarFile.entries(); while (tempEntriesEnum.hasMoreElements()) { JarEntry jarEntry = tempEntriesEnum.nextElement(); String classPath = jarEntry.getName().replace("/", "."); if (!classPath.endsWith(".class") || jarEntry.getName().lastIndexOf("/") == -1) { continue; } else { packageName = classPath.substring(0, jarEntry.getName().lastIndexOf("/")); onePackageJavaFiles = CLASS_OBJECT_PACKAGE_MAP.containsKey(packageName) ? CLASS_OBJECT_PACKAGE_MAP.get(packageName) : new ArrayList<>(); onePackageJavaFiles.add(new MemorySpringBootInfoJavaClassObject(jarEntry.getName().replace("/", ".").replace(".class", ""), new URL(libTempJarFile.getUrl(), jarEntry.getName()), javaFileManager)); CLASS_OBJECT_PACKAGE_MAP.put(packageName, onePackageJavaFiles); } } } } catch (Exception e) { e.printStackTrace(); } isInit = true; } MemoryJavaFileManager(JavaFileManager fileManager) { super(fileManager); this.javaFileManager = (JavacFileManager) fileManager; } public Map<String, byte[]> getClassBytes() { return new HashMap<>(this.classBytes); } @Override public void flush() { } @Override public void close() { classBytes.clear(); classObjectPackageMap.clear(); CLASS_OBJECT_PACKAGE_MAP.clear(); } public List<JavaFileObject> getLibJarsOptions(String packgeName) { synchronized (lock) { if (!isInit) { init(); } } return CLASS_OBJECT_PACKAGE_MAP.get(packgeName); } @Override public Iterable<JavaFileObject> list(Location location,String packageName, Set<JavaFileObject.Kind> kinds, boolean recurse) throws IOException { if ("CLASS_PATH".equals(location.getName()) && MemoryClassLoader.isJar()) { List<JavaFileObject> result = getLibJarsOptions(packageName); if (result != null) { return result; } } Iterable<JavaFileObject> it = super.list(location, packageName, kinds, recurse); if (kinds.contains(JavaFileObject.Kind.CLASS)) { final List<JavaFileObject> javaFileObjectList = classObjectPackageMap.get(packageName); if (javaFileObjectList != null) { if (it != null) { for (JavaFileObject javaFileObject : it) { javaFileObjectList.add(javaFileObject); } } return javaFileObjectList; } else { return it; } } else { return it; } } @Override public String inferBinaryName(Location location, JavaFileObject file) { if (file instanceof MemoryInputJavaClassObject) { return ((MemoryInputJavaClassObject) file).inferBinaryName(); } return super.inferBinaryName(location, file); } @Override public JavaFileObject getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind, FileObject sibling) throws IOException { if (kind == JavaFileObject.Kind.CLASS) { return new MemoryOutputJavaClassObject(className); } else { return super.getJavaFileForOutput(location, className, kind, sibling); } } JavaFileObject makeStringSource(String className, final String code) { String classPath = className.replace('.', '/') + JavaFileObject.Kind.SOURCE.extension; return new SimpleJavaFileObject(URI.create("string:///" + classPath), JavaFileObject.Kind.SOURCE) { @Override public CharBuffer getCharContent(boolean ignoreEncodingErrors) { return CharBuffer.wrap(code); } }; } void makeBinaryClass(String className, final byte[] bs) { JavaFileObject javaFileObject = new MemoryInputJavaClassObject(className, bs); String packageName = ""; int pos = className.lastIndexOf('.'); if (pos > 0) { packageName = className.substring(0, pos); } List<JavaFileObject> javaFileObjectList = classObjectPackageMap.get(packageName); if (javaFileObjectList == null) { javaFileObjectList = new LinkedList<>(); javaFileObjectList.add(javaFileObject); classObjectPackageMap.put(packageName, javaFileObjectList); } else { javaFileObjectList.add(javaFileObject); } } class MemoryInputJavaClassObject extends SimpleJavaFileObject { final String className; final byte[] bs; MemoryInputJavaClassObject(String className, byte[] bs) { super(URI.create("string:///" + className.replace('.', '/') + Kind.CLASS.extension), Kind.CLASS); this.className = className; this.bs = bs; } @Override public InputStream openInputStream() { return new ByteArrayInputStream(bs); } public String inferBinaryName() { return className; } } class MemoryOutputJavaClassObject extends SimpleJavaFileObject { final String className; MemoryOutputJavaClassObject(String className) { super(URI.create("string:///" + className.replace('.', '/') + Kind.CLASS.extension), Kind.CLASS); this.className = className; } @Override public OutputStream openOutputStream() { return new FilterOutputStream(new ByteArrayOutputStream()) { @Override public void close() throws IOException { out.close(); ByteArrayOutputStream bos = (ByteArrayOutputStream) out; byte[] bs = bos.toByteArray(); classBytes.put(className, bs); makeBinaryClass(className, bs); } }; } } }
class MemorySpringBootInfoJavaClassObject extends BaseFileObject { private final String className; private URL url; MemorySpringBootInfoJavaClassObject(String className, URL url, JavacFileManager javacFileManager) { super(javacFileManager); this.className = className; this.url = url; } @Override public Kind getKind() { return Kind.valueOf("CLASS"); } @Override public URI toUri() { try { return url.toURI(); } catch (URISyntaxException e) { e.printStackTrace(); } return null; } @Override public String getName() { return className; } @Override public InputStream openInputStream() { try { return url.openStream(); } catch (IOException e) { e.printStackTrace(); } return null; } @Override public OutputStream openOutputStream() throws IOException { return null; } @Override public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException { return null; } @Override public Writer openWriter() throws IOException { return null; } @Override public long getLastModified() { return 0; } @Override public boolean delete() { return false; } @Override public String getShortName() { return className.substring(className.lastIndexOf(".")); } @Override protected String inferBinaryName(Iterable<? extends File> iterable) { return className; } @Override public boolean equals(Object o) { return false; } @Override public int hashCode() { return 0; } @Override public boolean isNameCompatible(String simpleName, Kind kind) { return false; } }
// 自定义 springboot 的类加载器 class SpringJavaFileManager extends JavacFileManager { public SpringJavaFileManager(Context context, boolean b, Charset charset) { super(context, b, charset); } @Override public ClassLoader getClassLoader(Location location) { nullCheck(location); Iterable var2 = this.getLocation(location); if (var2 == null) { return null; } else { ListBuffer var3 = new ListBuffer(); Iterator var4 = var2.iterator(); while (var4.hasNext()) { File var5 = (File) var4.next(); try { var3.append(var5.toURI().toURL()); } catch (MalformedURLException var7) { throw new AssertionError(var7); } } return this.getClassLoader((URL[]) var3.toArray(new URL[var3.size()])); } } protected ClassLoader getClassLoader(URL[] var1) { ClassLoader var2 = this.getClass().getClassLoader(); try { Class loaderClass = Class.forName("org.springframework.boot.loader.LaunchedURLClassLoader"); Class[] var4 = new Class[]{URL[].class, ClassLoader.class}; Constructor var5 = loaderClass.getConstructor(var4); return (ClassLoader) var5.newInstance(var1, var2); } catch (Throwable var6) { } return new URLClassLoader(var1, var2); } }
总结
动态编译可能在日常工作中所使用的场景不多,但在特定的场景下能够很好的解决我们所遇到的问题,本篇文章可以给大家提供一些视野,当你遇到类似场景时或许动态编译能够很好的解决它!
最后希望大家都能在自己平凡的工作里从编程中收获一些快乐~
到此这篇关于Java 动态编译在项目中的实践分享的文章就介绍到这了,更多相关Java 动态编译内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!