剛來公司就接了一個不發版直接改代碼的需求

前言

前幾天突然接到一個技術需求,想要做一個功能。前端有一個表單,在頁面上可以直接寫 java 代碼,寫完後就能保存到數據庫,並且這個代碼實時生效。這豈非是不用發版就可以隨時改代碼了嗎?而且有bug也不怕,隨時改。

適用場景:代碼邏輯需要經常變動的業務。

核心思想

  • 頁面改動 java 代碼字符串
  • java 代碼字符串編譯成 class
  • 動態加載到 jvm

實現重點

JDK 提供了一個工具包 javax.tools 讓使用者可以用簡易的 API 進行編譯。

這些工具包的使用步驟:

  1. 獲取一個 javax.tools.JavaCompiler 實例。
  2. 基於 Java 文件對象初始化一個編譯任務 CompilationTask 實例。
  3. 因爲JVM 裏面的 Class 是基於 ClassLoader 隔離的,所以編譯成功之後可以通過自定義的類加載器加載對應的類實例
  4. 使用反射 API 進行實例化和後續的調用。

1. 代碼編譯

這一步需要將 java 文件編譯成 class,其實平常的開發過程中,我們的代碼編譯都是由 IDEA、Maven 等工具完成。

內置的 SimpleJavaFileObject 是面向源碼文件的,而我們的是源碼字符串,所以需要實現 JavaFileObject 接口自定義一個 JavaFileObject。

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()方法即可獲取目標類編譯後的字節碼對應的字節數組(二進制內容)

  1. 實現 ClassLoader

因爲JVM 裏面的 Class 是基於 ClassLoader 隔離的,所以編譯成功之後得通過自定義的類加載器加載對應的類實例,否則是加載不了的,因爲同一個類只會加載一次。

主要關注 findClass 方法

public class JdkDynamicCompileClassLoader extends ClassLoader {

    public static final String CLASS_EXTENSION = ".class";

    private final static Map<String, JavaFileObject> javaFileObjectMap = new ConcurrentHashMap<>();

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

    @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());
    }
}
  1. 封裝了上面的 ClassLoader 和 JavaFileObject
public class JdkDynamicCompileJavaFileManager extends ForwardingJavaFileManager<JavaFileManager> {

    private final JdkDynamicCompileClassLoader classLoader;
    private final Map<URI, JavaFileObject> javaFileObjectMap = new ConcurrentHashMap<>();

    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 = new ArrayList<>();
        // 這裏要區分編譯的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);
    }
}
  1. 使用 JavaCompiler 編譯並反射生成實例對象
public final class JdkCompiler {

    static DiagnosticCollector<JavaFileObject> DIAGNOSTIC_COLLECTOR = new DiagnosticCollector<>();

    @SuppressWarnings("unchecked")
    public static <T> T compile(String packageName,
                                String className,
                                String sourceCode) throws Exception {
        // 獲取系統編譯器實例
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        // 設置編譯參數
        List<String> options = new ArrayList<>();
        options.add("-source");
        options.add("1.8");
        options.add("-target");
        options.add("1.8");
        // 獲取標準的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,
                Collections.singletonList(javaFileObject)
        );
        Boolean result = compilationTask.call();
        System.out.println(String.format("編譯[%s]結果:%s", qualifiedName, result));
        Class<?> klass = classLoader.loadClass(qualifiedName);
        return (T) klass.getDeclaredConstructor().newInstance();
    }
}

完成上面工具的搭建之後。我們可以接入數據庫的操作了。數據庫層面省略,只展示 service 層

service 層:

public class JavaService {

    public Object saveAndGetObject(String packageName,String className,String javaContent) throws Exception {
        Object object = JdkCompiler.compile(packageName, className, javaContent);
        return object;
    }

}

測試:

public class TestService {

    public static void main(String[] args) throws Exception {
        test();
    }

    static String content="package cn.mmc;\n" +
            "\n" +
            "public class SayHello {\n" +
            "    \n" +
            "    public void say(){\n" +
            "        System.out.println(\"11111111111\");\n" +
            "    }\n" +
            "}";

    static String content2="package cn.mmc;\n" +
            "\n" +
            "public class SayHello {\n" +
            "    \n" +
            "    public void say(){\n" +
            "        System.out.println(\"22222222222222\");\n" +
            "    }\n" +
            "}";

    public static void test() throws Exception {
        JavaService javaService = new JavaService();
        Object sayHello = javaService.saveAndGetObject("cn.mmc", "SayHello", content);
        sayHello.getClass().getMethod("say").invoke(sayHello);

        Object sayHello2 = javaService.saveAndGetObject("cn.mmc", "SayHello", content2);
        sayHello2.getClass().getMethod("say").invoke(sayHello2);
    }
}

我們在啓動應用時,更換了代碼文件內存,然後直接反射調用對象的方法。執行結果:

可以看到,新的代碼已經生效!!!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章