Java 动态编译在项目中的实践

Java 动态编译在项目中的实践

引言

或许大部分人作业至今都没有运用过 Java 的动态编译功用,当然我也是在机缘巧合之下才有机会去研讨运用。

这就不得不说到我刚来咱们部分的故事了,其时我接收了一个项目,主要是做部分各个事务与外部三方的对接,在接手后我遇到了一些问题:

1、项目便是一个大杂烩,包含了各个事务的代码。经常来个需求但现已无法找到对应的担任人(要么离任要么现已不担任这块事务),最终就要让我修改,可我也不是很了解相关事务。我恨呐!

2、各个事务方每次改动都需求找我发版以及做分支办理,需求耗费精力来处理与我担任事务无关的作业。我烦呐!

为了处理这些问题我就开动了我聪明的脑瓜子,为何不将这项目里的代码分割成一块块小的代码块?然后只要对这些代码块做好办理就可以了,这样就处理了这些代码归属的问题。

但还存在一个问题便是每次来需求都需求改动并发版,这关于一个需求的稳定的组件系统的设计初衷来说肯定是背道而驰的。这个时分我就想到了动态编译,它或许能处理!

1、什么是动态编译

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

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.
​

2、怎么结合 springboot 项目运用

上面展示了怎么简略运用 Java 的动态编译功用,可是在日常项目开发中,会面对更多的场景。结合前言中我所遇到的问题,我简略的给我们介绍下我在项目中是怎么运用 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);
   }
}

总结

动态编译可能在日常作业中所运用的场景不多,但在特定的场景下可以很好的处理咱们所遇到的问题,本篇文章可以给我们供给一些视野,当你遇到类似场景时或许动态编译可以很好的处理它!

最终期望我们都能在自己普通的作业里从编程中收获一些高兴~

引荐阅读

RocketMQ DLedger 初识

一种基于布隆过滤器的大表核算优化办法

事务系统 hystrix 实践应用

图数据技能调研以及事务实践

BeanCopy坑到MapStruct技巧

招贤纳士

政采云技能团队(Zero),Base 杭州,一个富有热情和技能匠心精力的生长型团队。规划 500 人左右,在日常事务开发之外,还分别在云原生、区块链、人工智能、低代码平台、中间件、大数据、物料系统、工程平台、性能体验、可视化等领域进行技能探究和实践,推动并落地了一系列的内部技能产品,持续探究技能的新鸿沟。此外,团队还纷繁投身社区建造,目前现已是 google flutter、scikit-learn、Apache Dubbo、Apache Rocketmq、Apache Pulsar、CNCF Dapr、Apache DolphinScheduler、alibaba Seata 等很多优异开源社区的贡献者。

假如你想改动一向被事折腾,期望开始折腾事;假如你想改动一向被劝诫需求多些主意,却无从破局;假如你想改动你有才能去做成那个成果,却不需求你;假如你想改动你想做成的事需求一个团队去支撑,但没你带人的方位;假如你想改动原本悟性不错,但总是有那一层窗户纸的模糊……假如你信任信任的力气,信任普通人能成果非凡事,信任能遇到更好的自己。假如你期望参与到跟着事务腾飞的进程,亲手推动一个有着深入的事务理解、完善的技能系统、技能发明价值、影响力外溢的技能团队的生长进程,我觉得咱们该聊聊。任何时间,等着你写点什么,发给 zcy-tc@cai-inc.com

微信大众号

文章同步发布,政采云技能团队大众号,欢迎重视

Java 动态编译在项目中的实践