万洋《Java动态编译》

简介: 我们都知道Java属于编译型语言,即源码需要经过编译成字节码然后运行于JVM我们也知道,代码一旦编写完成,编译出的.class文件是一定的。这里也就是静态编译。那我们需要在运行时编译并加载应该怎么办呢,存在如下场景 我们熟知的类似LeetCode这种测评平台,需要执行用户输入的代码。 服务器需要动态加载某些类文件进行编译。那么我们就要使用Java的动态编译能力,在运行时编译代码并加载进jvm。

原理

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

主要类库

l  JavaCompiler -表示java编译器, run方法执行编译操作. 还有一种编译方式是先生成编译任务(CompilationTask), 让后调用CompilationTaskcall方法执行编译任务

l  JavaFileObject -表示一个java源文件对象

l  JavaFileManager - Java源文件管理类, 管理一系列JavaFileObject

l  Diagnostic -表示一个诊断信息

l  DiagnosticListener -诊断信息监听器, 编译过程触发. 生成编译task(JavaCompiler#getTask())或获取FileManager(JavaCompiler#getStandardFileManager())时需要传递DiagnosticListener以便收集诊断信息

流程图

image.png

源码文件 -> 字节码文件

public static void fromJavaFile() {

   // 获取Javac编译器对象

   JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();

   // 获取文件管理器:负责管理类文件的输入输出

   StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);

   // 获取要被编译的Java源文件

   File file = new File("/Users/oneyoung/oneyoung/project/my/code/src/main/java/top/oneyoung/dynamic/TestHello.java");

   // 通过源文件获取到要编译的Java类源码迭代器,包括所有内部类,其中每个类都是一个JavaFileObject

   Iterable<? extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjects(file);

   // 生成编译任务

   JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, null, null, null, compilationUnits);

   // 执行编译任务

   task.call();

}

我们这里准备了TestHello.java

public class TestHello {

   public static void main(String[] args) {

       System.out.println("this is a test");

   }

}

执行后在源文件同目录下生成了编译的class文件HelloTest.class

image.png

我们试着手动加载该class文件,使用类加载器的defineClass方法,可以直接加载字节码文件。

public static Class<?> loadClassFromDisk(String path) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {

   // defineClass ClassLoader 类的一个方法,用于加载类

   // 但是这个方法是 protected 的,所以需要通过反射的方式获取这个方法的权限

   Class<ClassLoader> classLoaderClass = ClassLoader.class;

   Method defineClass = classLoaderClass.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);

   defineClass.setAccessible(true);

   // 读取文件系统的 file byte 数组

   File file = new File(path);

   byte[] bytes = new byte[(int) file.length()];

   try (FileInputStream fileInputStream = new FileInputStream(file)) {

       fileInputStream.read(bytes);

   } catch (IOException e) {

       e.printStackTrace();

   }

   // 执行 defineClass 方法 返回 Class 对象

   return (Class<?>) defineClass.invoke(Thread.currentThread().getContextClassLoader(), bytes, 0, bytes.length);

}

由于TestHello中的方法为静态方法,使用class反射机制执行方法

// 执行编译任务

Boolean call = task.call();

if (call) {

   Class<?> o = loadClassFromDisk("/Users/oneyoung/oneyoung/project/my/code/src/main/java/top/oneyoung/dynamic/TestHello.class");

   Method main = o.getMethod("main", String[].class);

   main.invoke(null, new Object[]{new String[]{}});

}

执行结果

this is a test

 

源码字符串 -> 字节码文件

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

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

public class SourceJavaFileObject extends SimpleJavaFileObject {


   /**

    * The source code of this "file".

    */

   private final String code;


   SourceJavaFileObject(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;

   }

}

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

// 通过源代码字符串获取到要编译的Java类源码迭代器,包括所有内部类,其中每个类都是一个JavaFileObject

SourceJavaFileObject javaFileObject = new SourceJavaFileObject("TestHello", "public class TestHello { public static void main(String[] args) { System.out.println(\"Hello World\"); } }");

// 生成编译任务

JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, null, null, null, Collections.singleton(javaFileObject));

执行后,同样编译出了class文件,不过由于没有指定编译的class输出路径,他会默认放在源文件的根目录下

image.png

后面同样可以通过defindClass加载字节码完成加载。

源码字符串 -> 字节码数组

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

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

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

这里我使用类委托的方式,把大部分功能委托给了传入的StandardJavaFileManager,主要是重写了getJavaFileForOutput,使输出编译完成的字节码文件为字节数组。

然后增加了方法getBytesByClassName获取编译完成的字节码字节数组

public class ByteArrayJavaFileManager implements JavaFileManager {

   private static final Logger LOG = LoggerFactory.getLogger(ByteArrayJavaFileManager.class);



   private final StandardJavaFileManager fileManager;


   /**

    * synchronizing due to ConcurrentModificationException

    */

   private final Map<String, ByteArrayOutputStream> buffers = Collections.synchronizedMap(new LinkedHashMap<>());


   public ByteArrayJavaFileManager(StandardJavaFileManager fileManager) {

       this.fileManager = fileManager;

   }


   @Override

   public ClassLoader getClassLoader(Location location) {

       return fileManager.getClassLoader(location);

   }


   @Override

   public synchronized Iterable<JavaFileObject> list(Location location, String packageName, Set<Kind> kinds, boolean recurse) throws IOException {

       return fileManager.list(location, packageName, kinds, recurse);

   }


   @Override

   public String inferBinaryName(Location location, JavaFileObject file) {

       return fileManager.inferBinaryName(location, file);

   }


   @Override

   public boolean isSameFile(FileObject a, FileObject b) {

       return fileManager.isSameFile(a, b);

   }


   @Override

   public synchronized boolean handleOption(String current, Iterator<String> remaining) {

       return fileManager.handleOption(current, remaining);

   }


   @Override

   public boolean hasLocation(Location location) {

       return fileManager.hasLocation(location);

   }


   @Override

   public JavaFileObject getJavaFileForInput(Location location, String className, Kind kind) throws IOException {


       if (location == StandardLocation.CLASS_OUTPUT) {

           boolean success;

           final byte[] bytes;

           synchronized (buffers) {

               success = buffers.containsKey(className) && kind == Kind.CLASS;

               bytes = buffers.get(className).toByteArray();

           }

           if (success) {


               return new SimpleJavaFileObject(URI.create(className), kind) {

                   @Override

                   public InputStream openInputStream() {

                       return new ByteArrayInputStream(bytes);

                   }

               };

           }

       }

       return fileManager.getJavaFileForInput(location, className, kind);

   }


   @Override

   public JavaFileObject getJavaFileForOutput(Location location, final String className, Kind kind, FileObject sibling) {

       return new SimpleJavaFileObject(URI.create(className), kind) {

           @Override

           public OutputStream openOutputStream() {

               // 字节输出流用与FileManager输出编译完成的字节码文件为字节数组

               ByteArrayOutputStream bos = new ByteArrayOutputStream();

               // 将每个需要加载的类的输出流进行缓存

               buffers.putIfAbsent(className, bos);

               return bos;

           }

       };

   }


   @Override

   public FileObject getFileForInput(Location location, String packageName, String relativeName) throws IOException {

       return fileManager.getFileForInput(location, packageName, relativeName);

   }


   @Override

   public FileObject getFileForOutput(Location location, String packageName, String relativeName, FileObject sibling) throws IOException {

       return fileManager.getFileForOutput(location, packageName, relativeName, sibling);

   }


   @Override

   public void flush() {

       // Do nothing

   }


   @Override

   public void close() throws IOException {

       fileManager.close();

   }


   @Override

   public int isSupportedOption(String option) {

       return fileManager.isSupportedOption(option);

   }


   public void clearBuffers() {

       buffers.clear();

   }



   public Map<String, byte[]> getAllBuffers() {

       Map<String, byte[]> ret = new LinkedHashMap<>(buffers.size() * 2);

       Map<String, ByteArrayOutputStream> compiledClasses = new LinkedHashMap<>(ret.size());

       synchronized (buffers) {

           compiledClasses.putAll(buffers);

       }

       compiledClasses.forEach((k, v) -> ret.put(k, v.toByteArray()));

       return ret;

   }


   public byte[] getBytesByClassName(String className) {

       return buffers.get(className).toByteArray();

   }

}

然后我们修改下之前的执行流程

public static void fromJavaSourceToByteArray1() throws Exception {

   // 获取Javac编译器对象

   JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();

   // 获取文件管理器:负责管理类文件的输入输出

   StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);

   // 创建自定义的FileManager

   ByteArrayJavaFileManager byteArrayJavaFileManager = new ByteArrayJavaFileManager(fileManager);

   // 通过源代码字符串获取到要编译的Java类源码迭代器,包括所有内部类,其中每个类都是一个JavaFileObject

   JavaFileObject javaFileObject = new SourceJavaFileObject("TestHello", "public class TestHello { public static void say(String args) { System.out.println(args); } }");


   JavaCompiler.CompilationTask task = compiler.getTask(null, byteArrayJavaFileManager, null, null, null, Collections.singletonList(javaFileObject));

   // 执行编译任务

   Boolean call = task.call();

   if (Boolean.TRUE.equals(call)) {

       byte[] testHellos = byteArrayJavaFileManager.getBytesByClassName("TestHello");

       Class<ClassLoader> classLoaderClass = ClassLoader.class;

       Method defineClass = classLoaderClass.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);

       defineClass.setAccessible(true);

       Object invoke = defineClass.invoke(TestHello.class.getClassLoader(), testHellos, 0, testHellos.length);

       Class clazz = (Class) invoke;

       clazz.getMethod("say", String.class).invoke(null, "你好");


   }

}

成功输出

你好

相关文章
|
3月前
|
Java 编译器 API
Java中的动态编译与运行
Java中的动态编译与运行
|
Java 缓存 测试技术
Groovy&Java动态编译执行
Groovy&Java动态编译执行 工作中,遇到部分业务经常动态变化,或者在不发布系统的前提下,对业务规则进行调整。那么可以将这部分业务逻辑改写成Groovy脚本来执行,那么就可以在业务运行过程中动态更改业务规则,达到快速响应。
2003 0
|
SQL Java 关系型数据库
深入理解Java的动态编译(下)
笔者很久之前就有个想法:参考现有的主流ORM框架的设计,造一个ORM轮子,在基本不改变使用体验的前提下把框架依赖的大量的反射设计去掉,这些反射API构筑的组件使用「动态编译」加载的实例去替代,从而可以得到接近于直接使用原生JDBC的性能。于是带着这样的想法,深入学习Java的动态编译。编写本文的时候使用的是JDK11。
182 0
深入理解Java的动态编译(下)
|
缓存 前端开发 Java
深入理解Java的动态编译(上)
笔者很久之前就有个想法:参考现有的主流ORM框架的设计,造一个ORM轮子,在基本不改变使用体验的前提下把框架依赖的大量的反射设计去掉,这些反射API构筑的组件使用「动态编译」加载的实例去替代,从而可以得到接近于直接使用原生JDBC的性能。于是带着这样的想法,深入学习Java的动态编译。编写本文的时候使用的是JDK11。
236 0
深入理解Java的动态编译(上)
|
Java 容器
动态编译生成Java类
动态创建bean,前面一篇介绍了通过cglib来创建的方式,虽然实现了动态创建java bean,但是有一个问题,java bean中的field name和我们预期的不太一致 接下来我们介绍一种直接通过拼接java代码,然后再将其编译成class并加载,从而实现动态类的创建
450 0
|
XML 设计模式 Java
玩转 Java 动态编译,秀了秀了~!
问题 之前的文章从Spring 的环境到 Spring Cloud 的配置中提到过,我们在使用 Spring Cloud 进行动态化配置,它的实现步骤是先将动态配置通过 @Value 注入到一个动态配置 Bean,并将这个 Bean 用注解标记为 @RefreshScope,在配置变更后,这些动态配置 Bean 会被统一销毁。
224 0
玩转 Java 动态编译,秀了秀了~!
|
Java 编译器 API
Java 动态编译
一、使用 JavaCompiler 接口来编译 java 源程序(最简单的) 使用 Java API 来编译 Java 源程式有非常多方法,目前让我们来看一种最简单的方法,通过 JavaCompiler 进行编译。
788 0
|
JavaScript 前端开发 Java
|
4天前
|
安全 Java UED
Java中的多线程编程:从基础到实践
本文深入探讨了Java中的多线程编程,包括线程的创建、生命周期管理以及同步机制。通过实例展示了如何使用Thread类和Runnable接口来创建线程,讨论了线程安全问题及解决策略,如使用synchronized关键字和ReentrantLock类。文章还涵盖了线程间通信的方式,包括wait()、notify()和notifyAll()方法,以及如何避免死锁。此外,还介绍了高级并发工具如CountDownLatch和CyclicBarrier的使用方法。通过综合运用这些技术,可以有效提高多线程程序的性能和可靠性。