Через два месяца после первого коммита в октябре 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.

Комментарии (13)


  1. aleksandy
    00.00.0000 00:00
    +7

    Вещь, интересная и в каких-то случаях даже полезная. Но, блин, var... Вот не зря говорили, что от него больше вреда, чем пользы.

    Метод compile() генерирует байт-коды для исходных файлов Java, но пока не загружает их в память.

    final var byteCodes = Compiler.java()
    .from("com.app", source)
    .compile();

    Как мне, ни разу не пользовавшемуся данной библиотекой понять, что есть эти самые байт-коды? Учитывая, что они не загружаются в память, я логично предположил, что byte[]. Но буквально парой строк ниже вижу byteCodes.saveTo(), т.е. таки некий ByteCode(s?).

    Читаем далее и видим

    Метод byteCodes.load() загружает классы и преобразует байт-код в объекты типа Class:

    final var loadedClasses = compiled.load();

    Опять же, полагаем, что loadedClasses- это Class . Но и тут мы оказываемся неправы, потому что Class<?> customClass = loadedClasses.get("com.app.CustomClass"), т.е. по факту это опять же какой-то библиотечный класс.

    Понятно, что это перевод, но ужель у самого переводчика не возникло таких вопросов?

    З.Ы.

    Кто-нибудь знает как в этом редакторе вставлять блоки кода внутрь цитаты?


    1. breninsul
      00.00.0000 00:00
      -4

      вы здесь не правы, под интерфейсом тоже не видно реализации.

      Всё-таки это автовыведение, а не динамическая типизация, IDE подсказывает тип переменной.

      А без IDE, читая код с хабра.... вам имя класса собственно зачем?


      1. aleksandy
        00.00.0000 00:00
        -1

        А без IDE, читая код с хабра.... вам имя класса собственно зачем?

        Я тебе один умный вещь скажу, но ты не обижайся. (с) Мне не имя класса нужно, а понимание, что вообще данный метод возвращает. Хотя бы затем, чтобы можно было сравнить подходы в новом читаемом и уже известном кодах.

        Всё-таки это автовыведение, а не динамическая типизация

        Это автовыведение, в данном конкретном случае, только всё усложняет и по большей части грозит потенциальными проблемами. Например, такой код не компилируется, потому что list - ArrayList, а не List.

        var list = new ArrayList<String>();
        // 100500 строк кода
        list = Collections.emptyList();


        1. Free_ze
          00.00.0000 00:00
          -1

          такой код не компилируется, потому что list - ArrayList, а не List.

          Пример точно так же бы не скомпилировался, будь там:

          ArrayList<String> list = new ArrayList<>();

          То есть проблема здесь не в var, а в неверном выборе типа переменной.

          грозит потенциальными проблемами.

          Тут могут быть какие-то настоящие проблемы, от которых не защитит компилятор?

          только всё усложняет

          Напротив - спасает от необходимости искать отличия слева и справа в тривиальных случаях.


          1. aleksandy
            00.00.0000 00:00

            проблема здесь не в var, а в неверном выборе типа переменной.

            И как часто вы работаете с переменными типа ArrayList или HashMap? Или всё-таки это List или Map? А если мне всё равно придётся писать List list = new ArrayList(), то какой смысл в var?

            Я считаю, что var должен был быть val: сахарком для final переменной, для которой тип можно вывести и будет по барабану List там или ArrayList, потому что записать в переменную всё равно ничего не получится.

            искать отличия слева и справа

            А зачем их искать? Тип определён слева, что там справа в подавляющем большинстве случаев неважно. От несоответствия типов опять же защитит компилятор.


            1. Free_ze
              00.00.0000 00:00

              И как часто вы работаете с переменными типа ArrayList или HashMap? Или всё-таки это List или Map?

              Всегда, когда справа new. Вообще, для локальных переменных редко имеет смысл ставить тип, отличный от того, что справа (или он подразумевается очевидным образом).

              для которой тип можно вывести

              То есть "тупой" var называете усложнением, а (нетривиальный) вывод типа - ок?

              А зачем их искать?

              Тип слева может сужать функционал объекта справа. Чтобы знать насколько, очевидно)


              1. aleksandy
                00.00.0000 00:00

                То есть "тупой" var называете усложнением, а (нетривиальный) вывод типа - ок?

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

                Вообще, для локальных переменных редко имеет смысл ставить тип, отличный от того, что справа

                Вообще нет. Утверждение истинно только при условии, что эти переменные как минимум effective final.

                Тип слева может сужать функционал объекта справа. 

                Может, но как я писал ранее в подавляющем большинстве случаев это неважно. Как часто вы использовали ArrayList#trimToSize(int) или ConcurrentHashMap.contains(Object)?


                1. Free_ze
                  00.00.0000 00:00

                  он нужен и в первую очередь совсем не для var-а.

                  Понял вас так: компилятор смотрит вниз по сценариям использования и выводит наименее общий тип, удовлетворяющий всем этим сценариям. То есть если тип объекта — ArrayList, а используется он лишь в рамках List, то выводится List. Это не выглядит очевидным.


                  под усложнением я имел ввиду именно понимание кода при чтении

                  Почему увидев varсложно понять, скажем, что тип переменной идентичен типу справа от new?


                  Вообще нет. Утверждение истинно только при условии, что эти переменные как минимум effective final.

                  Ну раскройте) Что им мешает быть мутабельными, если нам это необходимо?


                  Как часто вы использовали ArrayList#trimToSize(int) или ConcurrentHashMap.contains(Object)?

                  Не вижу особо проблем, кроме того, что второй метод задепрекейчен. Давайте сразу к сути проблемы, пожалуйста.


                  1. aleksandy
                    00.00.0000 00:00

                    Что им мешает быть мутабельными, если нам это необходимо?

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

                    var list = new ArrayList();
                    list = Collections.emptyList();

                    Давайте сразу к сути проблемы, пожалуйста.

                    Нет проблемы, на вопрос-то ответите: как часто вы в повседневном коде используете методы, определённые для конкретной реализации общего интерфейса?


                    1. Free_ze
                      00.00.0000 00:00

                      как часто вы в повседневном коде используете методы, определённые для конкретной реализации общего интерфейса?

                      Как уже говорил выше, я всегда использую var, когда справа new и зачастую, когда справа метод с очевидным типом. То есть использую конкретные реализации.


                      Как я уже писал ранее такой код невалиден.

                      Можно написать new ArrayList<>() и код будет валиден. Мы же знаем тип, мы сами его тут создаем, к чему тут интерфейс?


                      Нет проблемы

                      Если "основные паттерны" не решают какие-либо проблемы, то это просто привычки.


                      1. aleksandy
                        00.00.0000 00:00

                        Разницу между

                        используете методы, определённые для конкретной реализации

                        и

                        использую конкретные реализации.

                        принципиально не замечаете?

                        Можно написать new ArrayList<>() и код будет валиден.

                        Ага, а потом "java тормозит, жрёт память".

                        Мы же знаем тип, мы сами его тут создаем, к чему тут интерфейс?

                        К тому, что в java принято работать с максимально возможно узким типом.


                      1. Free_ze
                        00.00.0000 00:00
                        +1

                        принципиально не замечаете?

                        Нет, подсветите разницу.


                        а потом "java тормозит, жрёт память".

                        Сомневаюсь, что нельзя обойтись без перезаписывания переменной. Если возврат — ранний return Collections.emptyList(), если добавляем в какую-то коллекцию List, то collection.add( Collections.emptyList()).


                        Мешать инлайнингу вызовов интерфейсами и параллельно рассуждать о производительности — это странно)


                        в java принято работать с максимально возможно узким типом

                        В Java принято контракты делать максимально гибкими, что логично. Делая то же самое для локальных переменных — это уже похоже культ карго.


                      1. aleksandy
                        00.00.0000 00:00

                        Возможно вы правы.