Через два месяца после первого коммита в октябре 2022 года Питер Верхас, старший архитектор EPAM Systems, выпустил версию 2.0.0 SourceBuddy, новой утилиты, которая компилирует динамически исходный код Java, заданный в строке или файле, в файл класса.
Утилит SourceBuddy требует Java 17 и представляет собой упрощенный фасад для компилятора javac
, который обеспечивает ту же функциональность.
Версия 2.0.0 поддерживает комбинацию скрытых и нескрытых классов во время компиляции и выполнения. Кроме того, был упрощен API, включая критические изменения, такие как изменение метода loadHidden()
на метод hidden()
, поэтому и выпущен новый основной релиз. Полный обзор изменений для каждой версии доступен в документации по выпускам на GitHub.
SourceBuddy можно использовать после добавления следующей зависимости Maven:
<dependency>
<groupId>com.javax0.sourcebuddy</groupId>
<artifactId>SourceBuddy</artifactId>
<version>2.0.0</version>
</dependency>
В качестве альтернативы можно использовать следующую зависимость Gradle:
implementation 'com.javax0.sourcebuddy:SourceBuddy:2.0.0'
Чтобы продемонстрировать SourceBuddy, рассмотрим следующий пример интерфейса, который будет использоваться динамически создаваемым кодом:
package com.app;
public interface PrintInterface {
void print();
}
Простой API способен компилировать один класс за раз, используя статический метод com.javax0.sourcebuddy.Compiler.compile()
. Вот пример для компиляции нового класса, реализующего ранее упомянутый интерфейс PrintInterface
:
String source = """
package com.app;
public class CustomClass implements PrintInterface {
@Override
public void print() {
System.out.println("Hello world!");
}
}""";
Class<?> clazz = Compiler.compile(source);
PrintInterface customClass =
(PrintInterface) clazz.getConstructor().newInstance();
customClass.print();
Fluent API предлагает функции для решения более сложных задач, таких как компиляция нескольких файлов с помощью статического метода Compiler.java()
:
Compiler.java().from(source).compile().load().newInstance(PrintInterface.class);
При желании можно указать двоичное имя класса, хотя SourceBuddy уже определит имя, когда это возможно:
.from("com.app", source)
Для нескольких исходных файлов метод from()
может быть вызван несколько раз, или все исходные файлы в определенном каталоге могут быть загружены сразу:
.from(Paths.get("src/main/java/sourcefiles"))
При желании метод hidden()
может быть использован для создания скрытого класса, который не может быть использован другими классами напрямую, только посредством рефлексии с использованием объекта Class
, возвращаемого SourceBuddy.
Метод compile()
генерирует байт-коды для исходных файлов Java, но пока не загружает их в память.
final var byteCodes = Compiler.java()
.from("com.app", source)
.compile();
При желании байт-коды могут быть сохранены на локальном диске:
byteCodes.saveTo(Paths.get("./target/generated_classes"));
В качестве альтернативы можно использовать метод stream()
, который возвращает поток байтовых массивов и может использоваться для получения такой информации, как двоичное имя:
byteCodes.stream().forEach(
bytecode -> System.out.println(Compiler.getBinaryName(bytecode)));
Метод byteCodes.load()
загружает классы и преобразует байт-код в объекты типа Class
:
final var loadedClasses = compiled.load();
Доступ к классу возможен путем приведения к суперклассу или интерфейсу, который реализует класс, или с помощью API рефлексии. Вот пример как получить доступ к классу CustomClass
:
Class<?> customClass = loadedClasses.get("com.app.CustomClass");
В качестве альтернативы для создания экземпляра класса можно использовать метод newInstance()
:
Object customClassInstance = loadedClasses.newInstance("com.app.CustomClass");
Поток классов может быть использован для получения дополнительной информации о классах:
loadedClasses.stream().forEach(
clazz -> System.out.println(clazz.getSimpleName()));
Более подробную информацию о SourceBuddy можно найти в подробных пояснениях в файле README на GitHub.