导读: 最近在看 Flink 源码的时候发现到一段实用的代码,该代码实现了 java 动态编译以及生成 jar 文件。将其进行改进后可以应用到我们的平台上,实现在平台页面上编写 java 代码语句,提交后由后台进行编译和打成 Jar 包再上传到指定的文件存储系统,从而代替之前在本地自己手动打 UDF 包的方式。下面我将对这段代码做一些简单分析,希望对各位有所帮助。
核心代码
public class TestUserClassLoaderJar { private static final String GENERATED_UDF_CLASS = "LowerUDF"; private static final String GENERATED_UDF_CODE = "public class " + GENERATED_UDF_CLASS + " extends extends org.apache.flink.table.functions.ScalarFunction {" + " public String eval(String str) {" + " return str.toLowerCase();" + " }" + "}"; /** * 将生成的 UDF class 打包到 JAR 中并且返回 JAR 所在的路径. */ public static File createJarFile(File tmpDir, String jarName) throws IOException { // 创建一个 java 文件 File javaFile = Paths.get(tmpDir.toString(), GENERATED_UDF_CLASS + ".java").toFile(); javaFile.createNewFile(); // 将代码写入 java 文件中 FileUtils.writeFileUtf8(javaFile, GENERATED_UDF_CODE); // 编译 java文件生成 class 文件 DiagnosticCollector diagnostics = new DiagnosticCollector<>(); JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnostics, null, null); Iterable extends JavaFileObject> compilationUnit = fileManager.getJavaFileObjectsFromFiles(Collections.singletonList(javaFile)); JavaCompiler.CompilationTask task = compiler.getTask( null, fileManager, diagnostics, Collections.emptyList(), null, compilationUnit); // 此处结果返回一个布尔值,可用于判断是否编译成功及是否执行下面的打包操作 task.call(); // 将 class 文件打包到 Jar 中 File classFile = Paths.get(tmpDir.toString(), GENERATED_UDF_CLASS + ".class").toFile(); File jarFile = Paths.get(tmpDir.toString(), jarName).toFile(); JarOutputStream jos = new JarOutputStream(new FileOutputStream(jarFile)); JarEntry jarEntry = new JarEntry(GENERATED_UDF_CLASS + ".class"); jos.putNextEntry(jarEntry); byte[] classBytes = FileUtils.readAllBytes(classFile.toPath()); jos.write(classBytes); jos.closeEntry(); jos.close(); return jarFile; } public static void main(String[] args) throws IOException { createJarFile(new File("G:jarSave"),"test.jar"); }}
以上代码主要完成以下三步操作:
- 创建一个 .java 文件,并将外部输入的 java 语句写入到该文件中
- 对 java 文件进行编译并生成 class 文件
- 将 class 文件打包到 JAR 中并返回 JAR 的路径
下图是动态编译的几个关键类的创建方式及作用:
JavaCompiler 的由来
在上面代码中通过 ToolProvider.getSystemJavaCompiler() 获取到 JavaCompiler。深入内部 findSystemToolClass() 方法发现其最终先是通过 System.getProperty("java.home") 获取到 /jdk1.8.0_241/jre 目录,再获取其上级目录中 lib 目录下的 tools.jar(也就是/jdk1.8.0_241/lib/ tools.jar),并进行动态加载 Jar 获取到 JavaCompiler。
findSystemToolClass 代码片段:
private Class> findSystemToolClass(String toolClassName) throws MalformedURLException, ClassNotFoundException { // try loading class directly, in case tool is on the bootclasspath try { return Class.forName(toolClassName, false, null); } catch (ClassNotFoundException e) { trace(FINE, e); // if tool not on bootclasspath, look in default tools location (tools.jar) ClassLoader cl = (refToolClassLoader == null ? null : refToolClassLoader.get()); if (cl == null) { File file = new File(System.getProperty("java.home")); if (file.getName().equalsIgnoreCase("jre")) file = file.getParentFile(); for (String name : defaultToolsLocation) file = new File(file, name); // if tools not found, no point in trying a URLClassLoader // so rethrow the original exception. if (!file.exists()) throw e; URL[] urls = { file.toURI().toURL() }; trace(FINE, urls[0].toString()); cl = URLClassLoader.newInstance(urls); refToolClassLoader = new WeakReference(cl); } return Class.forName(toolClassName, false, cl); } }
补充:FileUtils 工具类(已删减,只保留所需部分)
public class FileUtils { public static void writeFileUtf8(File file, String contents) throws IOException { writeFile(file, contents, "UTF-8"); } public static void writeFile(File file, String contents, String encoding) throws IOException { byte[] bytes = contents.getBytes(encoding); Files.write(file.toPath(), bytes, new OpenOption[]{StandardOpenOption.WRITE}); } private static byte[] read(InputStream source, int initialSize) throws IOException { int capacity = initialSize; byte[] buf = new byte[initialSize]; int nread = 0; while (true) { int n; while ((n = source.read(buf, nread, Math.min(capacity - nread, 4096))) > 0) { nread += n; } if (n < 0 || (n = source.read()) < 0) { return capacity == nread ? buf : Arrays.copyOf(buf, nread); } if (capacity <= 2147483639 - capacity) { capacity = Math.max(capacity << 1, 4096); } else { if (capacity == 2147483639) { throw new OutOfMemoryError("Required array size too large"); } capacity = 2147483639; } buf = Arrays.copyOf(buf, capacity); buf[nread++] = (byte) n; } } public static byte[] readAllBytes(Path path) throws IOException { SeekableByteChannel channel = Files.newByteChannel(path); Throwable var2 = null; byte[] var7; try { InputStream in = Channels.newInputStream(channel); Throwable var4 = null; try { long size = channel.size(); if (size > 2147483639L) { throw new OutOfMemoryError("Required array size too large"); } var7 = read(in, (int) size); } catch (Throwable var30) { var4 = var30; throw var30; } finally { if (in != null) { if (var4 != null) { try { in.close(); } catch (Throwable var29) { var4.addSuppressed(var29); } } else { in.close(); } } } } catch (Throwable var32) { var2 = var32; throw var32; } finally { if (channel != null) { if (var2 != null) { try { channel.close(); } catch (Throwable var28) { var2.addSuppressed(var28); } } else { channel.close(); } } } return var7; }}
最后
以上就是动态编译 Java 代码以及生成 Jar 文件的方式。
感谢您的阅读,如果喜欢本文欢迎关注和转发,本头条号将坚持持续分享IT技术知识。对于文章内容有其他想法或意见建议等,欢迎提出共同讨论共同进步。