Как реализовать сохранение специфичных для сборки аннотаций в Java - PullRequest
4 голосов
/ 11 июня 2019

У меня есть аннотация, которую я в настоящее время использую только для внутренней сборки и в целях документации. Он не предлагает никакого значения во время выполнения, поэтому я выбрал @Retention(SOURCE):

@Retention(SOURCE)
public @interface X

Однако, чтобы проверить правильность его использования, я хотел бы реализовать модульный тест, который перемещается по всему API, чтобы проверить, применяется ли аннотация везде, где она должна применяться. Этот модульный тест было бы довольно легко реализовать с использованием обычных API-интерфейсов отражения Java, но я не могу этого сделать, поскольку тесты не могут отражать аннотацию, учитывая ее @Retention(SOURCE).

Чтобы использовать отражение в тестах, мне нужно изменить его на @Retention(RUNTIME), чего я хотел бы избежать из-за перегрузки в байтовом коде во время выполнения.

Обходные пути, о которых я знаю:

Есть обходные пути, как всегда. Я знаю об этом:

  • Мы могли бы использовать процессор аннотаций, который не проходит сборку, вместо выполнения модульных тестов. Это выполнимо, но менее оптимально, поскольку тесты довольно сложны и их гораздо сложнее реализовать с использованием процессоров аннотаций, а не модульных тестов с использованием API-интерфейсов junit и более удобного API-интерфейса отражения. Я хотел бы использовать этот обходной путь только в качестве крайней меры.
  • Мы могли бы изменить @Retention на RUNTIME в наших источниках, создать источники с помощью этих дополнительных тестов, затем предварительно обработать API, чтобы снова удалить сохранение, а затем собрать API во второй раз для производственного использования. Это досадный обходной путь, поскольку он усложнит и замедлит сборку.

Вопрос:

Есть ли более удобный способ сохранить аннотацию во время выполнения только для тестов, но не в фактически созданном jar-файле, используя Maven?

Ответы [ 4 ]

6 голосов
/ 12 июня 2019

Вот гибридный подход, который может сработать.

Напишите процессор аннотаций, который не реализует полное тестирование, которое вы хотите выполнить, а просто записывает в дополнительный файл, в котором произошли аннотации. Если вы аннотируете классы, методы и поля, местоположение может быть записано довольно просто, используя имя класса с указанием пакета плюс дескриптор метода или поля. (Однако это может оказаться более сложным, если ваша аннотация может появиться в более непонятных местах, например, на параметрах метода или на типовых сайтах использования.) Затем вы можете сохранить политику хранения как SOURCE.

Далее, напишите свои тесты junit, чтобы выполнить любой рефлексивный анализ, который вы собираетесь делать. Вместо того, чтобы пытаться найти аннотации рефлексивно, хотя (так как их там не будет), прочитайте файл коляски и посмотрите там.

2 голосов
/ 12 июня 2019

Я думаю, что вы достаточно хорошо покрыли пространство решения.

Еще два, которые вы не охватили:

  • Удалите аннотацию позже на этапе последующей обработки, используя такой инструмент, как proguard.

  • Взломайте ваш компилятор, чтобы переключить сохранение аннотации в зависимости от флага.Уверен, что вы можете переключить какой-либо флаг во внутренних метаданных.Может быть, введен другим процессором аннотаций, вызванным аннотацией @DynamicRetention ("flag")?

1 голос
/ 23 июня 2019

Если @Retention(CLASS) приемлемо, то я бы рекомендовал использовать ArchUnit . Задание, которое вы описываете, звучит так, как будто оно подходит. ArchUnit может использоваться для определения и проверки правил для вашей архитектуры. Например, его можно использовать для ограничения доступа между определенными классами / пакетами или, например, проверить иерархии классов, имена типов - или аннотации.

Обычно он выполняется как модульный тест JUnit или любым другим фреймворком. Он работает путем анализа байт-кода, поэтому нет необходимости переключаться на сохранение во время выполнения.

Свободный API приятен и, на мой взгляд, более удобочитаем, чем использование отражения или обработки аннотаций для этого варианта использования. Например, чтобы определенные классы всегда имели определенную аннотацию, вы должны написать это правило в модульном тесте:

classes().that().areAssignableTo(MyService.class).should().beAnnotatedWith(MyAnnotation.class)

Также возможно создание пользовательских правил для утверждения более сложных ограничений.

1 голос
/ 12 июня 2019

Один из других обходных путей может включать:

  1. Оставить значение по умолчанию retention = CLASS.
  2. Использование библиотеки, которая будет читать байт-код напрямую .
@interface X {
}

@X
public class Main {
  public static void main(String[] args) throws IOException {
    ClassPathResource classResource = new ClassPathResource("com/caco3/annotations/Main.class");
    try (InputStream is = classResource.getInputStream()) {
      ClassReader classReader = new ClassReader(is);
      AnnotationMetadataReadingVisitor visitor = new AnnotationMetadataReadingVisitor(Main.class.getClassLoader());
      classReader.accept(visitor, 0);
      System.out.println(visitor.getAnnotationTypes());
    }
  }
}

выход:

[com.caco3.annotations.X]

Используемая библиотека: ASM :

ASM - универсальная среда манипулирования и анализа байт-кода Java

В этом коде используются некоторые классы из Spring Framework:

Однако этот подход страдает тем же недостатком, что и описанный вами:

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

, потому что (from javadoc ):

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

public static void main(String[] args) throws IOException {
    X x = AnnotationUtils.findAnnotation(Main.class, X.class);
    System.out.println(x);
}

выходы: null

...