Компилирование и исполнение Java-кода в Runtime

—

от автора

Привет Хабр! Сегодня я хотел бы поговорить про динамическое компилирование и исполнение Java-кода, подобно скриптовым языкам программирования. В этой статье вы найдете пошаговое руководство как скомпилировать Java в Bytecode и загрузить новые классы в ClassLoader на лету.

Зачем?

В разработке все чаще возникают типовые задачи, которые можно было бы закрыть простой генерацией кода. Например, сгенерировать DTO классы по имеющейся спецификации по стандартам OpenAPI или AsyncAPI. В целом, для генерации кода нет необходимости компилировать и выполнять код в runtime, ведь можно сгенерировать исходники классов, а собрать уже вместе с проектом. Однако при написании инструментов для генерации кода, было бы не плохо покрыть это тестами. А при проверке самый очевидный сценарий: сгенерировал-скомпилировал-загрузил-проверил-удалил. И вот тут-то и возникает задача генерации и проверки кода «на лету».

Также иногда возникают потребности выполнять какой-то код удаленно. Как правило это какие-то распределенные облачные вычисления. В этом случае можно отправлять исходный код на вычислительный узел, а там уже происходит динамическая сборка и выполнение.

Последовательность действий

Для выполнения Java-кода в Runtime нам потребуется:

  1. Динамически создать и сохранить наш код в .java файл.

  2. Скомпилировать исходники в Bytecode (файлы .class).

  3. Загрузить скомпилированные классы в ClassLoader.

  4. Использовать 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 (которая лежит в <JDK_HOME>/lib/tools.jar). Выглядеть это будет как-то так:

Path srcPath = Paths.get("hello");  List<File> 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<JavaFileObject> 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[]{Paths.get(".").toUri().toURL()},         classLoader); //загружаем наш класс Class<?> helloHabrClass = urlClassLoader.loadClass("hello.Habr"); //находим и вызываем метод helloHabr Method methodHelloHabr = helloHabrClass.getMethod("helloHabr"); //в параметре передается ссылка на экземпляр класса для вызова метода  //либо null при вызове статического метода methodHelloHabr.invoke(null);

Итог

В этой статье я постарался показать полноценный сценарий генерации и выполнения кода в Runtime. Самому мне это пригодилось при написании unit-тестов для библиотеки по генерации DTO классов на базе документации сгенерированной библиотекой springwolf. Реализацию тестов в моем проекте можно посмотреть тут.


ссылка на оригинал статьи https://habr.com/ru/articles/688462/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *