Java 如何使用動態編譯

Java 動態編譯在項目中的實踐

引言

或許大部分人工作至今都沒有使用過 Java 的動態編譯功能,當然我也是在機緣巧合之下才有機會去研究使用。

這就不得不說到我剛來我們部門的故事了,當時我接收了一個項目,主要是做部門各個業務與外部三方的對接,在接手後我遇到了一些問題:

1、項目就是一個大雜燴,包含了各個業務的代碼。經常來個需求但已經無法找到對應的負責人(要麼離職要麼已經不負責這塊業務),最後就要讓我修改,可我也不是很瞭解相關業務。我恨吶!

2、各個業務方每次改動都需要找我發版以及做分支管理,需要耗費精力來處理與我負責業務無關的事情。我煩吶!

爲了解決這些問題我就開動了我聰明的腦瓜子,爲何不將這項目裏的代碼分割成一塊塊小的代碼塊?然後只要對這些代碼塊做好管理就可以了,這樣就解決了這些代碼歸屬的問題。

但還存在一個問題就是每次來需求都需要改動併發版,這對於一個需要的穩定的組件系統的設計初衷來說肯定是背道而馳的。這個時候我就想到了動態編譯,它或許能解決!

1、什麼是動態編譯

在 Java 中,動態編譯是指在運行時動態地編譯 Java 源代碼,生成字節碼,並加載到 JVM 中執行。動態編譯可以用於實現動態代碼生成、動態加載、插件化等功能。

1.1、動態編譯的相關概念

1.2、如何簡單的實現動態編譯

下面是一個簡單的示例,演示如何使用動態編譯:

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);
    }
}

總結

動態編譯可能在日常工作中所使用的場景不多,但在特定的場景下能夠很好的解決我們所遇到的問題,本篇文章可以給大家提供一些視野,當你遇到類似場景時或許動態編譯能夠很好的解決它!

最後希望大家都能在自己平凡的工作裏從編程中收穫一些快樂~

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/ouquSyGmeZKoxEipHqUiUA