Как добавить метод к существующему классу, используя обработку аннотаций в java/kotlin?

Я новичок в обработке аннотаций и генерации кода. Я хочу узнать, как я могу выполнить такую ​​​​операцию, как добавление нового метода в существующий класс. Вот пример того, что я хочу сделать:

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

class SourceClass {
    @CustomAnnotation
    fun annotatedFun1(vararg argument: Any) {
        //Do something
    }

    @CustomAnnotation
    fun annotatedFun2(vararg argument: Any) {
        //Do something
    }

    fun someOtherFun() {
        //Do something
    }
}

И результат, который я хочу получить - расширенная копия этого класса:

class ResultClass {
    fun hasFunWithName(name: String): Boolean {
        return (name in arrayOf("annotatedFun1", "annotatedFun2"))
    }

    fun callFunByName(name: String, vararg arguments: Any) {
        when (name) {
            "annotatedFun1" -> annotatedFun1(*arguments)
            "annotatedFun2" -> annotatedFun2(*arguments)
        }
    }

    fun annotatedFun1(vararg argument: Any) {
        //Do something
    }

    fun annotatedFun2(vararg argument: Any) {
        //Do something
    }

    fun someOtherFun() {
        //Do something
    }
}

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

Если бы можно было модифицировать класс, не создавая новый - было бы идеально, но во всех туториалах создаются только новые классы, и я не нашел ни одного примера, где бы все содержимое исходного класса копировалось в другой.

Пожалуйста, не советуйте использовать отражение. Мне нужно это для Android, поэтому отражение не является причиной стоимости ресурсов. Я ищу решение времени компиляции.

Это необходимо для пользовательского языка сценариев, реализованного в приложении, и его следует использовать для упрощения структуры классов-оболочек. Когда эта работа выполняется непосредственно в коде - это выглядит ужасно, когда количество таких методов превышает 20 на класс.


person Andrei Vinogradov    schedule 10.05.2018    source источник
comment
Вам удалось найти решение? В моем случае мне нужно добавить аннотацию к существующему методу/полю на этапе обработки аннотации.   -  person Sasha Shpota    schedule 24.12.2020
comment
@SashaShpota Нет, на данный момент для меня нет решения. Пишите, если найдете что-нибудь.   -  person Andrei Vinogradov    schedule 24.12.2020


Ответы (4)


Вот хороший пример обработки аннотаций Java, с которым я недавно работал. Это реализация @Immutable аннотация.

Ознакомьтесь с ByteBuddy или Kotlin Poet, чтобы понять, как работает дополнительная генерация кода.

Для Kotlin вы делаете почти то же самое, проверьте это руководство для Шаги, специфичные для Kotlin.

person Sergei Rybalkin    schedule 25.12.2020
comment
Спасибо (+1). Не могли бы вы подробнее рассказать о том, как этого можно достичь? Насколько я понимаю, Kotlin Poet помогает генерировать новый код, а мне нужно изменить синтаксическое дерево уже существующего класса. Я не нашел в исходном коде, что ByteBuddy использует обработку аннотаций. Мне понятно, как создать обработчик аннотаций, непонятно, как модифицировать существующий файл, чтобы он скомпилировался в расширенный байт-код. - person Sasha Shpota; 26.12.2020
comment
Чтобы добавить новый метод, вам нужно расширить этап определения класса или реализовать метод process в своем собственном AnnotationProcessor. Затем вы можете getElementsAnnotatedWith что-то и применить генерацию кода для этих элементов. Ознакомьтесь с разделом 5 здесь: baeldung.com/byte-buddy . Также возможно, что вам потребуется переопределить весь класс, см. Раздел 6 для получения более подробной информации. - person Sergei Rybalkin; 26.12.2020
comment
Спасибо, Сергей. Правильно ли я понимаю, что изменения, которые я вношу с помощью ByteBuddy на этапе обработки аннотаций, останутся в байт-коде сгенерированных файлов классов? - person Sasha Shpota; 27.12.2020
comment
Вы получите объявленные методы только в байт-коде (файлы .class) - person Sergei Rybalkin; 27.12.2020
comment
Возможно, я что-то упускаю, но на этапе обработки аннотации я не могу получить доступ к объекту Class класса, который мне нужно изменить (он еще не скомпилирован). В то же время мне нужно, чтобы ByteBuddy работал. Как я могу справиться с этим? - person Sasha Shpota; 28.12.2020
comment
Я пытался найти что-то, не слишком углубляясь. Здесь есть хорошая иллюстрация процесса adrianbartnik.de/blog/annotation-processing. в вашей ситуации есть два (возможно, больше) способа: 1. Сгенерировать код, который будет генерировать и добавлять методы во время выполнения. 2. Создайте свои методы (и передайте обратно в javac) - person Sergei Rybalkin; 29.12.2020

С Kotlin вы можете использовать функции расширения, и это рекомендуемый способ добавления новых функций в существующие классы, которые вы не контролируете. https://kotlinlang.org/docs/reference/extensions.html

person Chirdeep Tomar    schedule 10.05.2018
comment
Я просил о другом. Это не ответ на мой вопрос. - person Andrei Vinogradov; 11.05.2018

Вы можете следовать шаблону, используемому Project Lombok. См. Как работает ломбок? или ссылку исходный код для получения подробной информации.

Другим вариантом было бы написать новый класс, который расширяет исходный класс:

class ResultClass : SourceClass {
    fun hasFunWithName(name: String): Boolean {
        return (name in arrayOf("annotatedFun1", "annotatedFun2"))
    }

    fun callFunByName(name: String, vararg arguments: Any) {
        when (name) {
            "annotatedFun1" -> annotatedFun1(*arguments)
            "annotatedFun2" -> annotatedFun2(*arguments)
        }
    }
}

Или, возможно, вместо этого используйте композицию и реализуйте методы покрытия для всех общедоступных методов в SourceClass.

Если вы не привязаны к этому с помощью обработки аннотаций, вы можете использовать отдельный фрагмент пользовательского кода для обработки файлов исходного кода перед компиляцией. Возможно, используйте регулярное выражение, например /@CustomAnnotation\s+.*fun (\w+)\s*\(([^)]*)\)/gm (Проверить на Regex101), чтобы найти аннотированные методы.

person Rangi Keen    schedule 25.12.2020
comment
Спасибо (+1). Не могли бы вы подробнее рассказать о том, как Ломбок это делает? В связанном вопросе SO упоминается, что в Ломбоке они приводят javax.lang.model.element.Element к базовому внутреннему API, чтобы изменить существующие источники, но я не смог найти его в базе кода. Возможно, мне нужно хорошо изучить кодовую базу, но если вы знаете, как/где это делается, пожалуйста, дайте мне знать. - person Sasha Shpota; 25.12.2020
comment
Извините, я не знаю, как работает Project Lombok, чтобы объяснить. - person Rangi Keen; 25.12.2020

Если я правильно понял требование, цель состоит в том, чтобы реализовать что-то вроде описанного ниже.

У вас есть исходный файл C.java, который определяет класс C следующим образом:

public final class C
{
    @Getter
    @Setter
    private int m_IntValue;

    @Getter
    @Constructor
    private final String m_Text;
}

А теперь вы хотите знать, как написать обработчик аннотаций, который подключается во время компиляции и изменяет исходный код из C.java, который видит компилятор, примерно так:

public final class C
{
    private int m_IntValue;
    public final int getIntValue() { return m_IntValue; }
    public final void setIntValue( final int intValue ) { m_IntValue = intValue; }

    private final String m_Text;
    public final String getText() { return m_Text; }

    public C( final String text ) { m_Text = text; }
}

Плохая новость в том, что это невозможно… ни с процессором аннотаций, ни с Java 15.

Для Java 8 был способ использовать некоторые внутренние классы с отражением, чтобы убедить AP каким-то образом манипулировать уже загруженным исходным кодом и позволить компилятору скомпилировать его во второй раз. К сожалению, чаще это не срабатывало, чем срабатывало…

В настоящее время обработчик аннотаций может только создать новый (в смысле дополнительный) исходный файл. Таким образом, одним из решений может быть расширение класса (конечно, это не сработает для примера класса C выше, потому что сам класс является окончательным, а все атрибуты закрытыми…

Поэтому написание препроцессора было бы еще одним решением; у вас нет файла C.java на жестком диске, но есть файл с именем C.myjava, который будет использоваться этим препроцессором для генерации C.java, а тот, в свою очередь, используется компилятором. Но обработчик аннотаций этого не делает, но таким образом им можно злоупотреблять.

Также можно поиграться с байт-кодом, который сгенерировал компилятор, и добавить туда недостающий (или дополнительный) функционал. Но это было бы очень далеко от обработки аннотаций…


В качестве резюме: сегодня (начиная с Java 15) процессор аннотаций не позволяет манипулировать существующим исходным кодом (вы даже не можете исключить какой-либо исходный код из компиляции); вы можете создавать дополнительные исходные файлы только с помощью процессора аннотаций.

person tquadrat    schedule 29.12.2020
comment
Спасибо (+1). Я предполагаю, что Lombok решает эту проблему и с Java 15. - person Sasha Shpota; 29.12.2020