- How to Compile a Class at Runtime with Java 8 and 9
- What happens behind the scenes?
- Tech Tutorials
- Monday, March 15, 2021
- How to Compile Java Program at Runtime
- Java code to compile Java program at runtime
- Компилирование и исполнение Java-кода в Runtime
- Зачем?
- Последовательность действий
- Шаг 1. Генерация кода
- Шаг 2. Компиляция кода
- Шаг 3. Загрузка и выполнение кода
- Итог
How to Compile a Class at Runtime with Java 8 and 9
In some cases, it’s really useful to be able to compile a class at runtime using the java.compiler module. You can e.g. load a Java source file from the database, compile it on the fly, and execute its code as if it were part of your application. In the upcoming jOOR 0.9.8, this will be made possible through https://github.com/jOOQ/jOOR/issues/51. As always with jOOR (and our other projects), we’re wrapping existing JDK API, simplifying the little details that you often don’t want to worry about. Using jOOR API, you can now write:
// Run this code from within the com.example package Supplier supplier = Reflect.compile( "com.example.CompileTest", "package com.example;\n" + "class CompileTest\n" + "implements java.util.function.Supplier \n" + ">\n" ).create().get(); System.out.println(supplier.get());
Supplier supplier = Reflect.compile( `org.joor.test.CompileTest`, `package org.joor.test; class CompileTest implements java.util.function.Supplier < public String get() < return "Hello World!" >>` ).create().get(); System.out.println(supplier.get());
What happens behind the scenes?
Again, as in our previous blog post, we need to ship two different versions of our code. One that works in Java 8 (where reflecting and accessing JDK internal API was possible), and one that works in Java 9+ (where this is forbidden). The full annotated API is here:
package org.joor; import java.io.ByteArrayOutputStream; import java.io.OutputStream; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodHandles.Lookup; import java.net.URI; import java.util.ArrayList; import java.util.List; import javax.tools.*; import static java.lang.StackWalker.Option.RETAIN_CLASS_REFERENCE; class Compile < static Classcompile0( String className, String content, Lookup lookup) throws Exception < JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); ClassFileManager manager = new ClassFileManager( compiler.getStandardFileManager(null, null, null)); Listfiles = new ArrayList<>(); files.add(new CharSequenceJavaFileObject( className, content)); compiler.getTask(null, manager, null, null, null, files) .call(); Class caller = StackWalker .getInstance(RETAIN_CLASS_REFERENCE) .walk(s -> s .skip(2) .findFirst() .get() .getDeclaringClass()); // If the compiled class is in the same package as the // caller class, then we can use the private-access // Lookup of the caller class if (className.startsWith(caller.getPackageName() )) < result = MethodHandles .privateLookupIn(caller, lookup) .defineClass(fileManager.o.getBytes()); >// Otherwise, use an arbitrary class loader. This // approach doesn't allow for loading private-access // interfaces in the compiled class's type hierarchy else < result = new ClassLoader() < @Override protected Class>findClass(String name) throws ClassNotFoundException < byte[] b = fileManager.o.getBytes(); int len = b.length; return defineClass(className, b, 0, len); >>.loadClass(className); > > return result; > // These are some utility classes needed for the JavaCompiler // ---------------------------------------------------------- static final class JavaFileObject extends SimpleJavaFileObject < final ByteArrayOutputStream os = new ByteArrayOutputStream(); JavaFileObject(String name, JavaFileObject.Kind kind) < super(URI.create( "string:///" + name.replace('.', '/') + kind.extension), kind); >byte[] getBytes() < return os.toByteArray(); >@Override public OutputStream openOutputStream() < return os; >> static final class ClassFileManager extends ForwardingJavaFileManager < JavaFileObject o; ClassFileManager(StandardJavaFileManager m) < super(m); >@Override public JavaFileObject getJavaFileForOutput( JavaFileManager.Location location, String className, JavaFileObject.Kind kind, FileObject sibling ) < return o = new JavaFileObject(className, kind); >> static final class CharSequenceJavaFileObject extends SimpleJavaFileObject < final CharSequence content; public CharSequenceJavaFileObject( String className, CharSequence content ) < super(URI.create( "string:///" + className.replace('.', '/') + JavaFileObject.Kind.SOURCE.extension), JavaFileObject.Kind.SOURCE); this.content = content; >@Override public CharSequence getCharContent( boolean ignoreEncodingErrors ) < return content; >> >
- Find the caller class of our method
- Get a private method handle lookup for that class if the class being compiled is in the same package as the class calling the compilation
- Otherwise, use an arbitrary class loader to define the class
Tech Tutorials
Tutorials and posts about Java, Spring, Hadoop and many more. Java code examples and interview questions. Spring code examples.
Monday, March 15, 2021
How to Compile Java Program at Runtime
This post talks about how you can compile a java program at runtime. You may have a case where you get a Java file path by reading a property file and you need to compile and run that Java file or you may have a scenario where at run time a program file is created rather than some script which you need to compile and run.
In such cases you need to compile your code at run time from another Java program. It can be done using JavaCompiler interface and ToolProvider class. Note that these classes are provided from Java 6.
Also note that you will need JDK to run it not JRE, so you need to have JDK libraries not JRE. If you are using eclipse and your JRE System library is pointing to JRE path make sure it points to JDK. You can do that by right clicking on your project and going to Java Build Path through properties. There click on Libraries tab and select the JRE System Library which points to jre path and click Edit.
In the next dialog box you can select the path to JDK after selecting Alternate JRE.
Java code to compile Java program at runtime
Suppose there is a Java file HelloWorld.java which you need to compile at run time and execute its method displayMessage.
public class HelloWorld < public static void main(String[] args) < >public void displayMessage() < System.out.println("Hello world from displayMessage method"); >>
This is the class where runtime compilation is done.
import java.io.File; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; import javax.tools.JavaCompiler; import javax.tools.ToolProvider; public class RTComp < public static void main(String[] args) < JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); // Compiling the code int result = compiler.run(null, null, null, "C:\\workspace\\Test\\src\\org\\test\\HelloWorld.java"); System.out.println("result " + result); // Giving the path of the class directory where class file is generated.. File classesDir = new File("C:\\workspace\\Test\\bin\\org\\test"); // Load and instantiate compiled class. URLClassLoader classLoader; try < // Loading the class classLoader = URLClassLoader.newInstance(new URL[] < classesDir.toURI().toURL() >); Class cls; cls = Class.forName("org.test.HelloWorld", true, classLoader); HelloWorld instance = (HelloWorld)cls.newInstance(); instance.displayMessage(); > catch (MalformedURLException e) < // TODO Auto-generated catch block e.printStackTrace(); >catch (ClassNotFoundException e) < // TODO Auto-generated catch block e.printStackTrace(); >catch (IllegalAccessException e) < // TODO Auto-generated catch block e.printStackTrace(); >catch (InstantiationException e) < // TODO Auto-generated catch block e.printStackTrace(); >> >
result 0 Hello world from displayMessage method
Here it can be seen that compiler.run method is provided with the path of the class HelloWorld. Here I have used the package as org.test.
Also in eclipse, by default, bin is the location for putting .class files so that path is provided for the generated class. Once the java file is compiled it is loaded using the class loader and an instance of that class is created. Using that instance method of that class is called at runtime.
That’s all for this topic How to Compile Java Program at Runtime. If you have any doubt or any suggestions to make please drop a comment. Thanks!
Компилирование и исполнение Java-кода в Runtime
Сегодня я хотел бы поговорить про динамическое компилирование и исполнение Java-кода, подобно скриптовым языкам программирования. В этой статье вы найдете пошаговое руководство как скомпилировать Java в Bytecode и загрузить новые классы в ClassLoader на лету.
Зачем?
В разработке все чаще возникают типовые задачи, которые можно было бы закрыть простой генерацией кода. Например, сгенерировать DTO классы по имеющейся спецификации по стандартам OpenAPI или AsyncAPI. В целом, для генерации кода нет необходимости компилировать и выполнять код в runtime, ведь можно сгенерировать исходники классов, а собрать уже вместе с проектом. Однако при написании инструментов для генерации кода, было бы не плохо покрыть это тестами. А при проверке самый очевидный сценарий: сгенерировал-скомпилировал-загрузил-проверил-удалил. И вот тут-то и возникает задача генерации и проверки кода “на лету”.
Также иногда возникают потребности выполнять какой-то код удаленно. Как правило это какие-то распределенные облачные вычисления. В этом случае можно отправлять исходный код на вычислительный узел, а там уже происходит динамическая сборка и выполнение.
Последовательность действий
Для выполнения Java-кода в Runtime нам потребуется:
- Динамически создать и сохранить наш код в .java файл.
- Скомпилировать исходники в Bytecode (файлы .class).
- Загрузить скомпилированные классы в ClassLoader.
- Использовать reflection api для получения методов и выполнения их.
Шаг 1. Генерация кода
Вообще для генерации исходников можно конечно просто написать текст через StringBuider в файл и быть довольным. Но мне хотелось бы показать более прикладные решения, поэтому рассмотрим вариант генерации кода с использованием пакета com.sun.codemodel, а вот тут есть неплохой туториал по этому пакету. Так же на его основе есть библиотека jsonschema2pojo для генерации кода на основе jsonschema. Итак к коду:
public void generateTestClass() throws JClassAlreadyExistsException, IOException < //создаем модель, это своего рода корень вашего дерева кода JCodeModel codeModel = new JCodeModel(); //определяем наш класс Habr в пакете hello JDefinedClass testClass = codeModel._class("hello.Habr"); // определяем метод helloHabr JMethod method = testClass.method(JMod.PUBLIC + JMod.STATIC, codeModel.VOID, "helloHabr"); // в теле метода выводим строку "Hello Habr!" method.body().directStatement("System.out.println(\"Hello Habr!\");"); //собираем модель и пишем пакеты в currentDirectory codeModel.build(Paths.get(".").toAbsolutePath().toFile()); >
Пример выше сгенерирует класс Habr.java с одним методом:
package hello; public class Habr < public static void helloHabr() < System.out.println("Hello Habr!"); >>
Шаг 2. Компиляция кода
Для компиляции в Bytecode обычно используется javac и выполняется он простой командой:
javac -sourcepath src -d build\classes hello\Habr.java
Однако, нам надо скомпилировать наш класс прямо из кода. И для этого есть библиотека компилятора, до которой можно достучаться через javax/tools/JavaCompiler. Это реализация javax/tools/Tool (которая лежит в /lib/tools.jar). Выглядеть это будет как-то так:
Path srcPath = Paths.get("hello"); List files = Files.list(srcPath) .map(Path::toFile) .collect(Collectors.toList()); //получаем компилятор JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); //получаем новый инстанс fileManager для нашего компилятора try(StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null)) < //получаем список всех файлов описывающих исходники Iterable extends JavaFileObject>javaFiles = fileManager.getJavaFileObjectsFromFiles(files); DiagnosticCollector diagnostics = new DiagnosticCollector<>(); //заводим задачу на компиляцию JavaCompiler.CompilationTask task = compiler.getTask( null, fileManager, diagnostics, null, null, javaFiles ); //выполняем задачу task.call(); //выводим ошибки, возникшие в процессе компиляции for (Diagnostic diagnostic : diagnostics.getDiagnostics()) < System.out.format("Error on line %d in %s%n", diagnostic.getLineNumber(), diagnostic.getSource()); >>
Шаг 3. Загрузка и выполнение кода
Для выполнения кода нам надо загрузить его через ClassLoader и через reflection api вызвать наш метод.
//получаем ClassLoader, лучше получать лоадер от текущего класса, //я сделал от System только чтоб пример был рабочий ClassLoader classLoader = System.class.getClassLoader(); //получаем путь до нашей папки со сгенерированным кодом URLClassLoader urlClassLoader = new URLClassLoader( new URL[], classLoader); //загружаем наш класс Class helloHabrClass = urlClassLoader.loadClass("hello.Habr"); //находим и вызываем метод helloHabr Method methodHelloHabr = helloHabrClass.getMethod("helloHabr"); //в параметре передается ссылка на экземпляр класса для вызова метода //либо null при вызове статического метода methodHelloHabr.invoke(null);
Итог
В этой статье я постарался показать полноценный сценарий генерации и выполнения кода в Runtime. Самому мне это пригодилось при написании unit-тестов для библиотеки по генерации DTO классов на базе документации сгенерированной библиотекой springwolf. Реализацию тестов в моем проекте можно посмотреть тут.