Через два месяца после первого коммита в октябре 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.
aleksandy
Вещь, интересная и в каких-то случаях даже полезная. Но, блин,
var
... Вот не зря говорили, что от него больше вреда, чем пользы.Как мне, ни разу не пользовавшемуся данной библиотекой понять, что есть эти самые байт-коды? Учитывая, что они не загружаются в память, я логично предположил, что
byte[]
. Но буквально парой строк ниже вижуbyteCodes.saveTo()
, т.е. таки некийByteCode(s?)
.Читаем далее и видим
Опять же, полагаем, что
loadedClasses
- этоClass
. Но и тут мы оказываемся неправы, потому чтоClass<?> customClass = loadedClasses.get("com.app.CustomClass")
, т.е. по факту это опять же какой-то библиотечный класс.Понятно, что это перевод, но ужель у самого переводчика не возникло таких вопросов?
З.Ы.
Кто-нибудь знает как в этом редакторе вставлять блоки кода внутрь цитаты?
breninsul
вы здесь не правы, под интерфейсом тоже не видно реализации.
Всё-таки это автовыведение, а не динамическая типизация, IDE подсказывает тип переменной.
А без IDE, читая код с хабра.... вам имя класса собственно зачем?
aleksandy
Я тебе один умный вещь скажу, но ты не обижайся. (с) Мне не имя класса нужно, а понимание, что вообще данный метод возвращает. Хотя бы затем, чтобы можно было сравнить подходы в новом читаемом и уже известном кодах.
Это автовыведение, в данном конкретном случае, только всё усложняет и по большей части грозит потенциальными проблемами. Например, такой код не компилируется, потому что list - ArrayList, а не List.
Free_ze
Пример точно так же бы не скомпилировался, будь там:
То есть проблема здесь не в var, а в неверном выборе типа переменной.
Тут могут быть какие-то настоящие проблемы, от которых не защитит компилятор?
Напротив - спасает от необходимости искать отличия слева и справа в тривиальных случаях.
aleksandy
И как часто вы работаете с переменными типа ArrayList или HashMap? Или всё-таки это List или Map? А если мне всё равно придётся писать
List list = new ArrayList()
, то какой смысл вvar
?Я считаю, что
var
должен был бытьval
: сахарком для final переменной, для которой тип можно вывести и будет по барабану List там или ArrayList, потому что записать в переменную всё равно ничего не получится.А зачем их искать? Тип определён слева, что там справа в подавляющем большинстве случаев неважно. От несоответствия типов опять же защитит компилятор.
Free_ze
Всегда, когда справа
new
. Вообще, для локальных переменных редко имеет смысл ставить тип, отличный от того, что справа (или он подразумевается очевидным образом).То есть "тупой"
var
называете усложнением, а (нетривиальный) вывод типа - ок?Тип слева может сужать функционал объекта справа. Чтобы знать насколько, очевидно)
aleksandy
Подозреваю, что меня неправильно поняли. Во-первых, вывод типа - это дело компилятора, а не разработчика. И он нужен и в первую очередь совсем не для
var
-а. Во-вторых, под усложнением я имел ввиду именно понимание кода при чтении, а не то, как это реализовано.Вообще нет. Утверждение истинно только при условии, что эти переменные как минимум effective final.
Может, но как я писал ранее в подавляющем большинстве случаев это неважно. Как часто вы использовали
ArrayList#trimToSize(int)
илиConcurrentHashMap.contains(Object)
?Free_ze
Понял вас так: компилятор смотрит вниз по сценариям использования и выводит наименее общий тип, удовлетворяющий всем этим сценариям. То есть если тип объекта — ArrayList, а используется он лишь в рамках List, то выводится List. Это не выглядит очевидным.
Почему увидев
var
сложно понять, скажем, что тип переменной идентичен типу справа отnew
?Ну раскройте) Что им мешает быть мутабельными, если нам это необходимо?
Не вижу особо проблем, кроме того, что второй метод задепрекейчен. Давайте сразу к сути проблемы, пожалуйста.
aleksandy
Ничего не мешает. Но это неудобно. В первую очередь потому, что ломаются основные паттерны работы с java-кодом. Как я уже писал ранее такой код невалиден.
Нет проблемы, на вопрос-то ответите: как часто вы в повседневном коде используете методы, определённые для конкретной реализации общего интерфейса?
Free_ze
Как уже говорил выше, я всегда использую
var
, когда справаnew
и зачастую, когда справа метод с очевидным типом. То есть использую конкретные реализации.Можно написать
new ArrayList<>()
и код будет валиден. Мы же знаем тип, мы сами его тут создаем, к чему тут интерфейс?Если "основные паттерны" не решают какие-либо проблемы, то это просто привычки.
aleksandy
Разницу между
и
принципиально не замечаете?
Ага, а потом "java тормозит, жрёт память".
К тому, что в java принято работать с максимально возможно узким типом.
Free_ze
Нет, подсветите разницу.
Сомневаюсь, что нельзя обойтись без перезаписывания переменной. Если возврат — ранний
return Collections.emptyList()
, если добавляем в какую-то коллекциюList
, тоcollection.add( Collections.emptyList())
.Мешать инлайнингу вызовов интерфейсами и параллельно рассуждать о производительности — это странно)
В Java принято контракты делать максимально гибкими, что логично. Делая то же самое для локальных переменных — это уже похоже культ карго.
aleksandy
Возможно вы правы.