#java #annotation-processing
#java #обработка аннотаций
Вопрос:
Я написал пользовательский обработчик аннотаций, который собирает все аннотируемые классы, организует их лексикографически и генерирует новый класс для каждого аннотируемого класса.
В Intellij Idea, когда проект создается постепенно, не все аннотированные классы из project передаются моему процессору аннотаций, а только те, которые были изменены / добавлены. Это нарушает логику упорядочения.
Как мне убедиться, что обработчик аннотаций всегда применяется ко всем аннотируемым элементам в каждой сборке?
Я также нашел эту статью, но, похоже, она работает только с Gradle: https://docs.gradle.org/current/userguide/java_plugin.html#sec:incremental_annotation_processing
Возможно ли заставить обработчик аннотаций агрегировать инкрементные для любого инструмента сборки?
Можно ли сделать такой процессор аннотаций для выделения инкрементных?
Исходный код моего процессора аннотаций: https://github.com/ElegantNetworking/ElegantNetworkingAnnotationProcessor
Ответ №1:
Вы задаете неправильный вопрос. Вот мыслительный процесс, который приводит вас к неправильному вопросу:
- Хм, моя точка доступа видит только часть всего исходного кода при компиляции, это странно! Это приводит к ошибкам, и я хочу исправить эти ошибки.
- О, подождите, я понял, это из-за инкрементной компиляции.
- Я знаю! Я отключу инкрементную компиляцию!
- Хм, так как же мне это сделать? Я лучше спрошу ТАК.
Сначала я дам прямой ответ, но вам это не понравится: вы в принципе не можете. Повторная компиляция всей кодовой базы каждый раз, когда система хочет скомпилировать, невероятно неэффективна; никому не нравится, когда одно простое изменение в исходном файле приводит к необходимости ждать 250 секунд, чтобы увидеть эффект этого. Вскоре вы обвините инструменты (будь то gradle или intellij) в том, что они невероятно враждебны к вашим продуктивным часам. Инструменты знают это и не позволят (легко) такому невинному действию (например, включению некоторого обработчика аннотаций) сделать инструмент непригодным для использования.
Вы также не хотите узнавать, как это «исправить», потому что, ну, я только что сказал «пограничный непригодный для использования». Вы, конечно, не хотите, чтобы время обработки изменений составляло от половины секунды до 5 минут.
Однако есть хорошее решение, но только если вы вернетесь на несколько шагов назад.
Суть инкрементной компиляции в том, что вещи, которые не компилируются (потому что они не изменились / не должны быть)? Они БЫЛИ скомпилированы ранее. Все, что вам нужно сделать, это следовать дальше: точно так же, как компиляция исходного файла приводит к результату, который является «постоянным» и подразумевает, что вам не нужно его переделывать, пока не возникнет какое-либо условие, указывающее, что вам нужно повторно применить процесс, вам нужно сделать то же самое с вашей точкой доступа: еслинекоторый исходный файл обрабатывается вашей точкой доступа, который должен оставлять постоянный эффект; этого эффекта должно быть достаточно для всех будущих запусков без использования исходного исходного дерева, по крайней мере, до тех пор, пока указанное исходное дерево не будет изменено.
Это проще, чем кажется, потому что у вас есть файлер.
Я собираюсь описать процессор аннотаций в качестве примера:
Этот процессор будет сканировать все типы, аннотированные с @Provides(com.pkg.Foo.class)
помощью, проверяет, реализует ли аннотированный таким образом тип или расширяет Foo
его, а затем создает файл META-INF/services/com.pkg.Foo
, в котором указан тип. Это точно описывает, как работает процессор SPI: например, это то, что делает процессор автоматического обслуживания Google (вокруг есть куча таких проектов).
Этот процесс тривиален для полного запуска компиляции: AP может просто создать Map<String, List<String>>
это сопоставление, например "com.pkg.Foo"
, с ["com.company.FooImpl1", "com.company.FooImpl2"]
, заполняя его по мере выполнения раундов и при посещении исходных файлов, а затем во время конечного раунда выгружать эти карты в виде служебных файлов. AP — это коды на 2 страницы, почти тривиальные, и все же весьма полезные.
Проблема в том, что эта модель на самом деле не работает при инкрементной компиляции: при инкрементной компиляции FooImpl1
обнаруживается only , таким образом, map Foo
сопоставляется только с FooImpl1
, и когда пришло время выгружать файл с диска, FooImpl2
он просто исчез из вашего файла services, хотя класс FooImpl2 по-прежнему доступен.вокруг — это просто не было в инкрементном запуске компиляции, поскольку оно не было изменено.
Однако решение простое: у вас есть файлер!
Вместо того, чтобы просто сбрасывать каждую из этих встроенных карт в файл services и завершать работу, вам нужно сначала прочитать файл services. Если его там нет, достаточно просто, просто вернитесь к коду «выгрузить список». Но если он есть, прочитайте каждую запись в нем, запросите файл для этих классов. Если файл не может найти один из них, удалите строку из файла служб. Если это возможно, сохраните его.
Итак, теперь наша точка доступа увеличилась, возможно, с 2 страниц до 3 страниц, но теперь она полностью способна выполнять инкрементную компиляцию. Он может определить разницу между тем, что кто-то удаляет FooImpl2
и выполняет полную перекомпиляцию (что должно привести к созданию файла, содержащего только FooImpl1
службы), и тем, что кто-то сначала выполняет полный запуск (в результате в файле служб будут как 1, так и 2), а затем изменяет только FooImpl1.java и выполнение инкрементной компиляции:
class MyProcessor extends javax.annotation.processing.AbstractProcessor {
@Override public void init(ProcessingEnvironment env) {
// you need these:
Filer filer = env.getFiler();
Elements elementUtils = processingEnv.getElementUtils();
}
}
с помощью filer вы можете сделать:
FileObject resource = filer.getResource(StandardLocation.CLASS_OUTPUT,
"", pathToServicesFile);
и оттуда вы можете прочитать этот файл (если он есть), чтобы проверить, какие классы уже есть в этом файле services: при инкрементной компиляции это даст вам com.company.FooImpl1
and com.company.FooImpl2
. Затем вы можете проверить, существуют ли эти типы (все еще):
elements.getTypeElement("com.company.FooImpl1")
если это возвращается null
, оно больше не существует, и вы можете удалить его из своего файла служб. Если это так, сохраните его — если только вы не нажмете на этот файл, выполняя свои раунды, и окажется, что он больше не аннотирован. Дело в том, что если вы вообще никогда не попадали в этот файл во время ваших раундов, это означает, что он был исключен, потому что процесс инкрементной компиляции не учитывал его изменения, и, следовательно, последнее известное состояние (которое FooImpl1
реализует Foo
и аннотируется @Provides(Foo.class)
, следовательно, почему оно находится в уже существующих службахfile) по-прежнему корректен, поэтому действуйте соответствующим образом.
Если выходные данные / результаты вашего процессора аннотаций не содержат ничего, что можно было бы использовать для определения этого при последующем инкрементном запуске компиляции, создайте такой файл: создайте файл, который «отслеживает» состояние, о котором вам нужно знать.
Комментарии:
1. Вау! Спасибо за такой полный ответ, Rzwitserloot. Я имел в виду, что я хочу, чтобы мой обработчик аннотаций мог видеть все классы, отмеченные моей аннотацией, потому что сгенерированные классы основаны на всех аннотированных классах. Насколько я понимаю, процессор аннотаций должен быть совокупным инкрементным. Я не упомянул термин «совокупный инкрементный», потому что я не уверен, что это именно то, что мне нужно. Я немного улучшил вопрос, чтобы он выглядел понятнее. Для рассмотрения всех элементов в проекте рекомендуется использовать файл со списками ранее обработанных элементов
2. иметь возможность видеть все классы, отмеченные моей аннотацией — ну, вы не можете. Я надеюсь, что эта часть была ясна из моего ответа. Вы можете подделать это, используя filer для записи файла, в котором перечислены все, что вы видели в предыдущих прогонах компиляции, и использовать
getTypeElement
код, как я показал в примере, чтобы убедиться, что эти типы все еще существуют. Это именно то, что вам нужно. Или, я надеюсь, что это так, потому что, если это не так, ответ гораздо проще одним словом: невозможно .3. @hohserg дважды проверьте, что — gTE должен обязательно возвращать материал, даже если не в этой инкрементной компиляции.
4. Да, когда файл удаляется, затронутые файлы классов удаляются, а компилятор даже не запускается. Это слегка раздражает. Однако, используя трюк с файлом, если какой- либо исходный файл скомпилирован по какой-либо причине, ваша точка доступа запускается и может использовать файл, чтобы выяснить, что ему нужно удалить несколько строк. Теперь это лишь незначительное неудобство (перекомпилируйте что-нибудь. что угодно — точка доступа запустится и увидит, что один из исходных файлов теперь исчез), и, насколько я знаю, не может быть устранен.
5. Да, это звучит раздражающе. Вы можете создать таймер или просто записать файл во время инициализации (после использования filer для проверки И подтверждения того, что вам нужно внести обновление), а затем снова после раундов, это.. не слишком беспокоит; вы пишете дважды, когда могли бы написать один раз, но это в пределах разумного, нет?