科普文:Java基础系列之[java动态编译]

141 篇文章 1 订阅
112 篇文章 1 订阅

1、什么是动态编译

在 Java 中,动态编译是指在运行时动态地编译 Java 源代码,生成字节码,并加载到 JVM 中执行。动态编译可以用于实现动态代码生成、动态加载、插件化等功能。

动态编译,简单来说就是在Java程序运行时编译源代码。

从JDK1.6开始,引入了Java代码重写过的编译器接口,使得我们可以在运行时编译Java源代码,然后再通过类加载器将编译好的类加载进JVM,这种在运行时编译代码的操作就叫做动态编译。

静态编译:编译时就把所有用到的Java代码全都编译成字节码,是一次性编译。

动态编译:在Java程序运行时才把需要的Java代码的编译成字节码,是按需编译。

静态编译示例:

静态编译实际上就是在程序运行前将所有代码进行编译,我们在运行程序前用Javac命令或点击IDE的编译按钮进行编译都属于静态编译。

比如,我们编写了一个xxx.java文件,里面是一个功能类,如果我们的程序想要使用这个类,就必须在程序启动前,先调用Javac编译器来生成字节码文件。

如果使用动态编译,则可以在程序运行过程中再对xxx.java文件进行编译,之后再通过类加载器对编译好的类进行加载,同样能正常使用这个功能类。

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.

1.3 动态编译应用场景:

  1. 代码生成:动态生成代码并编译执行,例如模板引擎。

  2. 在线 hot-swap:在不重启应用的情况下更新代码和重新加载类。

  3. 动态数据库访问:编译运行时生成的 SQL 或 NoSQL 查询。

  4. 安全环境:在安全沙箱中执行不受信任的代码。

  5. 动态语言支持:支持类似 Ruby、Groovy 等动态语言的特性。

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、动态编译的应用

(1)、从源码文件编译得到字节码文件

刚才我们使用动态编译完成了输入一个Java源文件(.java),再到输出字节码文件(.class)的操作。这是从源码文件编译得到字节码文件的方式,实质上也是从磁盘输入,再输出到磁盘的方式。

(2)、从源码字符串编译得到字节码文件

假如现在有一串字符串形式的Java代码,那如何使用动态编译将这些字符串代码编译成字节码文件?这是从源码字符串编译得到字节码文件的方式,实质上也是从内存中得到源码,再输出到磁盘的方式。

根据刚才的代码,我们知道编译任务getTask()这个方法一共有 6 个参数,它们分别是:

  • Writer out:编译器的一个额外的输出 Writer,为 null 的话就是 System.err;
  • JavaFileManager fileManager:文件管理器;
  • DiagnosticListener<? super JavaFileObject> diagnosticListener:诊断信息收集器;
  • Iterable<String> options:编译器的配置;
  • Iterable<String> classes:需要被 annotation processing 处理的类的类名;
  • Iterable<? extends JavaFileObject> compilationUnits:要被编译的单元们,就是一堆 JavaFileObject。

根据getTask()的参数,我们知道编译器执行编译所需要的对象类型并不是文件File对象,而是JavaFileObject对象。因此,要实现从字符串源码编译得到字节码文件,只需要把字符串源码变为JavaFileObject对象即可。

JavaFileObject是一个接口,它的标准实现类SimpleJavaFileObject提供的一些方法是面向类源码文件(.java)和字节码文件(.class)的,而我们进行动态编译时输入的是字符串源码,所以我们需要自行实现JavaFileObject,以使JavaFileObject对象能装入我们的字符串源码。

具体的实现方法就是可以直接继承SimpleJavaFileObject类,再重写其中的一些方法使它能够装入字符串即可。

可以通过查看compiler.getTask().call() 的源代码来查看具体用到了SimpleJavaFileObject 的那些方法,这样我们才知道需要重写 SimpleJavaFileObject 的哪些方法。

一篇大佬分析getTask().call()源代码执行流程的文章介绍得很十分详细,强烈推荐:Java 类运行时动态编译技术.https://seanwangjs.github.io/2018/03/13/java-runtime-compile.html

简单的流程如下:

在这里插入图片描述
在上图中,getTask().call()会通过调用作为参数传入的JavaFileObject对象的getCharContent()方法获得字符串序列,即源码的读取是通过 JavaFileObject的 getCharContent()方法,那我们只需要重写getCharContent()方法,即可将我们的字符串源码装进JavaFileObject了。

构造SourceJavaFileObject实现定制的JavaFileObject对象,用于存储字符串源码:

public class SourceJavaFileObject extends SimpleJavaFileObject {
	private String source; //源码字符串
	
	//返回源码字符串
	public SourceJavaFileObject(String name, String sourceStr){ 
            super(URI.create("String:///" + name + Kind.SOURCE.extension),Kind.SOURCE);
            this.source = sourceStr;
    }
    @Override
    public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException{
        if(source == null) throw new IllegalArgumentException("source == null");
        else return source;
    }
}

则创建JavaFileObject对象时,变为了:

//使用重写getCharContent方法后的JavaFileObject构造参数
JavaFileObject sourceFileObject = new SourceJavaFileObject(className, source);
//执行编译
Boolean result = compiler.getTask(null, fileManager, null, null, null, Arrays.asList(sourceFileObject)).call(); 

由于我们自定了JavaFileObject,文件管理器 fileManager更像是一个工具类用于把 File对象数组自动转换成JavaFileObject 列表,换成手动生成 compilationUnits列表并传入也是可行的。(上述代码就是使用了Arrays.asList()手动生成 compilationUnits列表)。

至此,只需要调用getTask().call()就能将字符串形式的源码编译成字节码文件了。

(3)、从源码字符串编译得到字节码数组

如果我们进行动态编译时,想要直接输入源码字符串并且输出的是字节码数组,而不是输出字节码文件,又该如何实现?实际上,这是从内存中得到源码,再输出到内存的方式。

getTask().call()源代码执行流程图中,我们可以发现JavaFileObject 的 openOutputStream()方法控制了编译后字节码的输出行为,编译完成后会调用openOutputStream获取输出流,并写数据(字节码)。所以我们需要重写JavaFileObject 的 openOutputStream()方法。

同时在执行流程图中,我们还发现用于输出的JavaFileObject 对象是JavaFileManagergetJavaFileForOutput()方法提供的,所以为了让编译器编译完成后,将编译得到的字节码输出到我们自己构造的JavaFileObject 对象,我们还需要重写JavaFileManager

构造ClassFileObject,实现定制的JavaFileObject对象,用于存储编译后得到的字节码:

public static class ClassFileObject extends SimpleJavaFileObject {
	private ByteArrayOutputStream byteArrayOutputStream; //字节数组输出流
	//编译完成后会回调OutputStream,回调成功后,我们就可以通过下面的getByteCode()方法获取编译后的字节码字节数组
    @Override
    public OutputStream openOutputStream() throws IOException {
        return byteArrayOutputStream;
    }
    //将输出流中的字节码转换为字节数组
    public byte[] getCompiledBytes() {
        return byteArrayOutputStream.toByteArray();
    }
}

这样,我们就拥有了自定义的用于存储字节码的JavaFileObject。同时还通过添加getByteCode()方法来获得JavaFileObject对象中用于存放字节码的输出流,并将其转换为字节数组。

接下来,就需要重写JavaFileManager,使编译器编译完成后,将字节码存放在我们的ClassFileObject。具体做法是直接继承ForwardingJavaFileManager,再重写需要的getJavaFileForOutput()方法即可。

public static class MyJavaObjectManager extends ForwardingJavaFileManager<JavaFileManager>{
	 private ClassFileObject classObject; //我们自定义的JavaFileObject
	 //重写该方法,使其返回我们的ClassJavaFileObject
    @Override
    public JavaFileObject getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind,
                                               FileObject sibling) throws IOException {
        classObject= new ClassJavaFileObject (className, kind);
        return classObject;
    }
}

构造完毕,接下来直接传入getTask执行即可:

//执行编译
Boolean result = compiler.getTask(null,new MyJavaObjectManager(), null, null, null, Arrays.asList(sourceFileObject)).call(); 

注意这里传入的JavaFileObject,是前面构造的存储字符串源码的sourceFileObject,而不是我们用来存储字节码的sourceFileObject

至此,我们使用动态编译完成了将字符串源码编译成字节码数组。随后我们可以使用类加载器加载 byte[]中的字节码即可。

2.3、代码示例一

  // 通过调用这个方法即可实现 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);
    }
}

2.4、代码示例二

现在定义一个接口MysqlInfoMapper,用于动态执行一条已知的SQL,很简单,就是查询MySQL的系统表mysql里面的用户信息SELECT Host,User FROM mysql.user:

假设现在只提供一个MySQL的驱动包(mysql:mysql-connector-java:jar:8.0.20),暂时不能依赖任何高层次的框架,要动态实现MysqlInfoMapper接口,优先整理需要的组件:

需要一个连接管理器去管理MySQL的连接。需要一个SQL执行器用于执行查询SQL。需要一个结果处理器去提取和转换查询结果。为了简单起见,笔者在定义这三个组件接口的时候顺便在接口中通过单例进行实现(部分配置完全写死):

接着需要动态编译MysqlInfoMapper的实现类,它的源文件的字符串内容如下(注意不要在类路径下新建这个DefaultMysqlInfoMapper类):

然后编写一个客户端进行动态编译和执行:

最终的输出结果是:

编译[club.throwable.compile.DefaultMysqlInfoMapper]结果:true[{"host":"%","user":"canal"},{"host":"%","user":"doge"},{"host":"localhost","user":"mysql.infoschema"},{"host":"localhost","user":"mysql.session"},{"host":"localhost","user":"mysql.sys"},{"host":"localhost","user":"root"}]

然后笔者查看本地安装的MySQL中的结果,验证该查询结果是正确的。

这里笔者为了简化整个例子,没有在MysqlInfoMapper#selectAllMysqlUsers()方法中添加查询参数,可以尝试一下查询的SQL是SELECT Host,User FROM mysql.user WHERE User = 'xxx'场景下的编码实现。

如果把动态实现的DefaultMysqlInfoMapper注册到IOC容器中,就可以实现MysqlInfoMapper按照类型自动装配。如果把SQL和参数处理可以抽离到单独的文件中,并且实现一个对应的文件解析器,那么就可以把类文件和SQL隔离,Mybatis和Hibernate都是这样做的。

总结

动态编译或者更底层的面向字节码层面的编程,其实是一个十分有挑战性但是可以创造无限可能的领域,本文只是简单分析了一下Java源码编译的过程,并且通过一些简单的例子进行动态编译的模拟,离使用于实际应用中还有不少距离,后面需要花更多的时间去分析一下相关领域的知识。

动态编译是在Java程序运行时编译源代码,动态编译配合类加载器就可以在程序运行时编译源代码,并动态加载。

JDK提供了对应的JavaComplier接口来实现动态编译。

动态编译中存放源码和字节码的对象都是JavaFileObject ,因此如果我们想要修改源码的输入方式或者字节码的输出方式的,可以自主实现JavaFileObject 接口。同时,由于编译器是通过JavaFileManager来管理输入输出的,因此也需要自主实现JavaFileManager接口。

由于能力有限,可能存在错误,感谢指出。以上内容为本人在学习过程中所做的笔记。参考的书籍、文章或博客如下:
[1]seanwangjs. Java 类运行时动态编译技术.https://seanwangjs.github.io/2018/03/13/java-runtime-compile.html
[2]Throwable.深入理解Java的动态编译.博客园.https://www.cnblogs.com/throwable/p/13053582.html
[3]执笔记忆的空白.java动态编译实现.腾讯云云社区.https://cloud.tencent.com/developer/article/1764721?from=information.detail

  • 14
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

-无-为-

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值