Java 源代码动态编译、类加载和代码执行(Java 8)

createh53周前 (12-10)技术教程17

#头条创作挑战赛#

Java 的一个重要特性是动态的类加载机制。通过在运行时动态地加载类,Java 程序可以实现很多强大的功能。下面通过一个具体的实例来说明 Java 程序中,如何动态地编译 Java 源代码、加载类和执行类中的代码。这里的代码示例适用的版本是 Java 8。

示例所实现的功能很简单,就是对表达式求值。输入的是类似 1 + 1 或 3 * (2 + 3) 这样的表达式,返回的是表达式的值。示例的做法是动态创建一个 Java 源文件,编译该文件生成 class 文件,加载 class 文件之后再执行。比如,需要求值的表达式是 1 + 1,那么所生成的 Java 源文件如下所示,其中 1 + 1 的部分是动态的。

public class Calculator {
    public static Object calculate() {
        return 1 + 1;
    }
}

我们只需要编译该源文件,加载编译之后的 class 文件,再通过反射 API 来调用其中的 calculate 方法就可以得到表达式求值的结果。

编译

第一步是动态生成 Java 源代码并编译。生成 Java 源代码比较简单,直接用字符串连接就可以了。当然了,在生成逻辑比较复杂时,推荐的做法是使用字符串模板引擎,如 Handlebars。在下面的代码中,getJavaSource 方法生成 Java 源代码,compile 方法进行编译。

在进行编译的时候,使用的是 JDK 标准的 JavaCompiler 接口。从源代码字符串中创建了一个 JavaFileObject 对象作为编译时的源代码单元。编译时的选项 -d 指定了编译结果的输出路径,这里是一个临时文件夹。compile 方法的返回值是一个 Pair 对象,包含了 class 文件的路径,以及随机生成的 Java 包的名称。

public class DynamicCompilation {

  private static final String CLASS_NAME = "Calculator";

  public static Pair<Path, String> compile(String expr) throws IOException {
    String packageName = "z" + UUID.randomUUID().toString().replace("-", "");
    Path outputPath = Files.createTempDirectory("expr");
    JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
    StandardJavaFileManager fileManager = compiler.getStandardFileManager(null,
        null, null);
    compiler.getTask(null, fileManager, null, ImmutableList.of(
                "-d", outputPath.toAbsolutePath().toString()
            ), null,
            Collections.singletonList(
                new StringContentJavaFileObject(CLASS_NAME,
                    getJavaSource(packageName, expr))))
        .call();
    return Pair.of(outputPath, packageName + "." + CLASS_NAME);
  }

  private static String getJavaSource(String packageName, String expr) {
    return "package " + packageName + "; "
        + "public class " + CLASS_NAME
        + " { public static Object calculate() {  "
        + "return " + expr + "; }" +
        "}";
  }
}

上面的代码用到了一个帮助类 StringContentJavaFileObject,表示从字符串创建的 JavaFileObject 对象。

public class StringContentJavaFileObject extends SimpleJavaFileObject {

  private final String content;

  public StringContentJavaFileObject(String name, String content) {
    super(URI.create("string:///" + name + Kind.SOURCE.extension),
        Kind.SOURCE);
    this.content = content;
  }

  @Override
  public CharSequence getCharContent(boolean ignoreEncodingErrors) {
    return content;
  }
}


加载

编译完成之后的第二步是动态加载类。这一步并没有实现自定义的类加载器,而且使用内置的系统类加载器。系统类加载器通过 ClassLoader.getSystemClassLoader() 方法来获取。系统类加载器在 classpath 上查找类。这里用了一个比较 hack 的技巧来动态修改系统类加载器的 classpath。

在下面的代码中,ClasspathUpdater 的 addPath 方法可以把一个 Path 对象表示的路径,添加到系统类加载器的查找路径中。这是因为系统类加载器自身是 URLClassLoader 类型的加载器,其中的 addURL 方法可以添加新的查找路径。只不过 addURL 方法是 protected,这里通过反射 API 来进行调用。

public class ClasspathUpdater {

  public static void addPath(Path path) {
    URLClassLoader classLoader = (URLClassLoader) ClassLoader.getSystemClassLoader();
    try {
      Method method = URLClassLoader.class.getDeclaredMethod("addURL",
          URL.class);
      method.setAccessible(true);
      method.invoke(classLoader, path.toUri().toURL());
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

}

上面介绍的 ClasspathUpdater 类中的使用技巧,只对 Java 8 生效。在 Java 9 引入模块系统时,对系统类加载器进行了修改。系统类加载器被替换成了应用类加载器。应用类加载器不再是 URLClassLoader 类型了,就不能使用这个技巧了。

执行

最后一步就是执行动态加载的 Java 类。这一步比较简单,只需要用 Class.forName 方法来查找 Java 类,再找到对应的 Method 对象,直接调用即可。下面的代码给出了示例。

public class Invoker {

  public static Object invoke(String className) {
    try {
      Method method = Class.forName(className).getDeclaredMethod("calculate");
      return method.invoke(null);
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }
}

完整的执行过程

最后把整个流程串起来。在下面的代码中,需要求值的表达式是 (1 + 1) * 3 / 5.0。首先调用 DynamicCompilation.compile 方法进行动态编译,得到 class 文件的路径和完整的类名。class 文件的路径通过 ClasspathUpdater.addPath 方法添加到 classpath 中。完整的类名则传递给 Invoker.invoke 方法来执行。最后输出的结果是表达式的值。

public class Main {

  public static void main(String[] args) throws IOException {
    Pair<Path, String> result = DynamicCompilation.compile("(1 + 1) * 3 / 5.0");
    ClasspathUpdater.addPath(result.getLeft());
    System.out.println(Invoker.invoke(result.getRight()));
  }
}

相关文章

初学者关于JAVA语言中的类的理解

最近对编程挺有兴趣,于是开始了JAVA的学习,身为一个初学者,在学习的过程中,有一些理解,特发表记录。本人编程近乎零基础,若有理解不对,请批评指正。从学习的内容可知,在JAVA中,类是基础,是基本单位...

SpringBoot使用不同的策略动态地调用某个接口的实现类

1、前言经常遇到这样的一个需求,前端传的实体类型相同,后端需要根据实体类中的某一个字符串,动态地调用某一个类的方法。在SpringBoot中,我们可以理解成,一个Controller接口对应多个Ser...

Java类与对象(知识点+练习+扩展)

一.初识面向对象1.什么是面向对象?面向对象是一种符合人类思维习惯的编程思想。现实生活中存在各种形态不同的事物,这些事物之间存在着各种各样的联系。在程序中使用对象来映射现实中的事物,使用对象的关系来描...

深度理解Java动态代理:为何MyBatis Mapper接口无需实现类?

引言Java动态代理是一个强大的特性,它允许在运行时动态生成代理类来拦截和处理方法调用。因此,它在诸如MyBatis这样的持久层框架中得到了广泛应用,使得我们能够通过简单的Mapper接口来进行数据库...

Java程序设计试卷

JAVA程序设计试卷库(第5套)一、单选题(每小题 2 分,共 20 分)1、Java Application源程序的主类是指包含有( A )方法的类。A. main方法 B. toString方法C...