Статьи

Процессоры аннотаций Java

Эта статья является частью нашего Академического курса под названием Advanced Java .

Этот курс призван помочь вам наиболее эффективно использовать Java. В нем обсуждаются сложные темы, включая создание объектов, параллелизм, сериализацию, рефлексию и многое другое. Он проведет вас через ваше путешествие в мастерство Java! Проверьте это здесь !

1. Введение

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

Быть плагином компилятора также означает, что процессоры аннотаций немного низкоуровневые и сильно зависят от версии Java. Однако знание аннотаций из пятой части учебного пособия Как и когда использовать Enums and Annotations и Java Compiler API из 13-й части учебного пособия, Java Compiler API , очень пригодится для понимания внутренних деталей как работают процессоры аннотаций.

2. Когда использовать процессоры аннотаций

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

  • генерировать набор исходных или ресурсных файлов
  • изменить (изменить) существующий исходный код
  • анализировать исходный код и генерировать диагностические сообщения

Полезность аннотационных процессоров трудно переоценить. Они могут значительно сократить объем кода, который должны писать разработчики (путем его создания или изменения), или, выполняя статический анализ, подсказывать разработчикам, если предположения, выраженные конкретной аннотацией, не выполняются.

Будучи несколько невидимыми для разработчиков, процессоры аннотаций полностью поддерживаются всеми современными Java IDE и популярными инструментами построения и, как правило, не требуют какого-либо особого вмешательства. В следующем разделе руководства мы собираемся создать собственные несколько наивные процессоры аннотаций, которые, тем не менее, продемонстрируют всю мощь этой функции компилятора Java.

3. Обработка аннотации под капотом

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

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

По сути, любой Java-класс может стать полноценным процессором аннотации, просто реализовав единый интерфейс: javax.annotation.processing.Processor . Однако, чтобы стать действительно удобным, каждая реализация javax.annotation.processing.Processor должна предоставлять общедоступный конструктор без аргументов (более подробную информацию см. В части 1 руководства « Как создавать и уничтожать объекты» ), которая может использоваться для создания экземпляра процессора. Инфраструктура обработки будет следовать набору правил для взаимодействия с процессором аннотаций, и процессор должен соблюдать этот протокол:

  • экземпляр процессора аннотаций создается с использованием конструктора без аргументов класса процессора
  • метод init вызывается с соответствующим экземпляром javax.annotation.processing.ProcessingEnvironment
  • getSupportedSourceVersion методы getSupportedAnnotationTypes , getSupportedOptions и getSupportedSourceVersion (эти методы вызываются только один раз за прогон, а не в каждом раунде)
  • и, наконец, в зависимости от javax.annotation.processing.Processor вызывается метод процесса в javax.annotation.processing.Processor (учтите, что для каждого раунда не будет создаваться новый экземпляр процессора аннотаций)

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

4. Написание собственного обработчика аннотаций

Мы собираемся разработать несколько видов процессоров аннотаций, начиная с самого простого, средства проверки неизменности. Давайте определим простую аннотацию Immutable которую мы собираемся использовать, чтобы аннотировать класс, чтобы гарантировать, что он не позволяет изменять его состояние.

1
2
3
4
@Target( ElementType.TYPE )
@Retention( RetentionPolicy.CLASS )
public @interface Immutable {
}

Следуя политике хранения, аннотация будет сохраняться компилятором Java в файле классов на этапе компиляции, однако она не будет (и не должна быть) доступна во время выполнения.

Как мы уже знаем из третьей части руководства « Как проектировать классы и интерфейсы» , неизменность в Java очень сложна. Для простоты наш процессор аннотаций собирается проверить, что все поля класса объявлены как final. К счастью, стандартная библиотека Java предоставляет абстрактный процессор аннотаций, javax.annotation.processing.AbstractProcessor , который разработан как удобный суперкласс для большинства конкретных процессоров аннотаций. Давайте посмотрим на реализацию процессора аннотаций SimpleAnnotationProcessor .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@SupportedAnnotationTypes( "com.javacodegeeks.advanced.processor.Immutable" )
@SupportedSourceVersion( SourceVersion.RELEASE_7 )
public class SimpleAnnotationProcessor extends AbstractProcessor {
  @Override
  public boolean process(final Set< ? extends TypeElement > annotations,
      final RoundEnvironment roundEnv) {
         
    for( final Element element: roundEnv.getElementsAnnotatedWith( Immutable.class ) ) {
      if( element instanceof TypeElement ) {
        final TypeElement typeElement = ( TypeElement )element;
                 
        for( final Element eclosedElement: typeElement.getEnclosedElements() ) {
       if( eclosedElement instanceof VariableElement ) {
           final VariableElement variableElement = ( VariableElement )eclosedElement;
         
           if( !variableElement.getModifiers().contains( Modifier.FINAL ) ) {
             processingEnv.getMessager().printMessage( Diagnostic.Kind.ERROR,
               String.format( "Class '%s' is annotated as @Immutable,
                 but field '%s' is not declared as final",
                 typeElement.getSimpleName(), variableElement.getSimpleName()           
               )
             );                    
           }
         }
       }
    }
         
    // Claiming that annotations have been processed by this processor
    return true;
  }
}

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

Из-за предоставленного скаффолдинга наш SimpleAnnotationProcessor должен реализовывать только один метод — process . Сама реализация довольно проста и в основном просто проверяет, есть ли у обрабатываемого класса какое-либо поле, объявленное без модификатора final . Давайте посмотрим на пример класса, который нарушает этот наивный контракт неизменности.

01
02
03
04
05
06
07
08
09
10
11
12
@Immutable
public class MutableClass {
    private String name;
     
    public MutableClass( final String name ) {
        this.name = name;
    }
     
    public String getName() {
        return name;
    }
}

Запуск SimpleAnnotationProcessor этого класса SimpleAnnotationProcessor к выводу на консоль следующей ошибки:

1
Class 'MutableClass' is annotated as @Immutable, but field 'name' is not declared as final

Таким образом подтверждается, что процессор аннотаций успешно обнаружил неправильное использование Immutable аннотации в изменчивом классе.

В общем, выполнение некоторого самоанализа (и генерации кода) — это область, где процессоры аннотаций используются большую часть времени. Давайте немного усложним задачу и применим некоторые знания API Java Compiler из части 13 руководства, Java Compiler API . Процессор аннотаций, который мы собираемся написать на этот раз, будет изменять (или изменять) сгенерированный байт-код, добавляя final модификатор непосредственно в объявление поля класса, чтобы убедиться, что это поле не будет переназначено где-либо еще.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
@SupportedAnnotationTypes( "com.javacodegeeks.advanced.processor.Immutable" )
@SupportedSourceVersion( SourceVersion.RELEASE_7 )
public class MutatingAnnotationProcessor extends AbstractProcessor {
  private Trees trees;
     
  @Override
  public void init (ProcessingEnvironment processingEnv) {
    super.init( processingEnv );
    trees = Trees.instance( processingEnv );       
  }
     
  @Override
  public boolean process( final Set< ? extends TypeElement > annotations,
      final RoundEnvironment roundEnv) {
         
    final TreePathScanner< Object, CompilationUnitTree > scanner =
      new TreePathScanner< Object, CompilationUnitTree >() {
        @Override
         public Trees visitClass(final ClassTree classTree,
           final CompilationUnitTree unitTree) {
 
         if (unitTree instanceof JCCompilationUnit) {
           final JCCompilationUnit compilationUnit = ( JCCompilationUnit )unitTree;
                         
           // Only process on files which have been compiled from source
           if (compilationUnit.sourcefile.getKind() == JavaFileObject.Kind.SOURCE) {
             compilationUnit.accept(new TreeTranslator() {
               public void visitVarDef( final JCVariableDecl tree ) {
                 super.visitVarDef( tree );
                                     
                 if ( ( tree.mods.flags & Flags.FINAL ) == 0 ) {
                   tree.mods.flags |= Flags.FINAL;
                 }
               }
             });
           }
         }
     
        return trees;
      }
    };
         
    for( final Element element: roundEnv.getElementsAnnotatedWith( Immutable.class ) ) {   
      final TreePath path = trees.getPath( element );
      scanner.scan( path, path.getCompilationUnit() );
    }
         
    // Claiming that annotations have been processed by this processor
    return true;
  }
}

Реализация стала более сложной, однако многие классы (например, TreePathScanner , TreePath ) должны быть уже знакомы. Запуск процессора аннотаций для того же класса MutableClass сгенерирует следующий байт-код (что можно проверить, выполнив javap -p MutableClass.class ):

1
2
3
4
5
public class com.javacodegeeks.advanced.processor.examples.MutableClass {
  private final java.lang.String name;
  public com.javacodegeeks.advanced.processor.examples.MutableClass(java.lang.String);
  public java.lang.String getName();
}

В самом деле, в поле name присутствует final модификатор, тем не менее, он был опущен в исходном исходном файле Java. В последнем примере мы продемонстрируем возможности генерации кода процессорами аннотаций (и завершим обсуждение). Продолжая в том же духе, давайте реализуем процессор аннотаций, который будет генерировать новый исходный файл (и новый класс соответственно), добавляя суффикс Immutable к имени класса, Immutable аннотацией Immutable .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@SupportedAnnotationTypes( "com.javacodegeeks.advanced.processor.Immutable" )
@SupportedSourceVersion( SourceVersion.RELEASE_7 )
public class GeneratingAnnotationProcessor extends AbstractProcessor {
  @Override
  public boolean process(final Set< ? extends TypeElement > annotations,
      final RoundEnvironment roundEnv) {
         
    for( final Element element: roundEnv.getElementsAnnotatedWith( Immutable.class ) ) {
      if( element instanceof TypeElement ) {
        final TypeElement typeElement = ( TypeElement )element;
        final PackageElement packageElement =
          ( PackageElement )typeElement.getEnclosingElement();
 
        try {
          final String className = typeElement.getSimpleName() + "Immutable";
          final JavaFileObject fileObject = processingEnv.getFiler().createSourceFile(
            packageElement.getQualifiedName() + "." + className);
                     
          try( Writer writter = fileObject.openWriter() ) {
            writter.append( "package " + packageElement.getQualifiedName() + ";" );
            writter.append( "\\n\\n");
            writter.append( "public class " + className + " {" );
            writter.append( "\\n");
            writter.append( "}");
          }
        } catch( final IOException ex ) {
          processingEnv.getMessager().printMessage(Kind.ERROR, ex.getMessage());
        }
      }
    }
         
    // Claiming that annotations have been processed by this processor
    return true;
  }
}

В результате внедрения этого процессора аннотаций в процесс MutableClass класса MutableClass будет сгенерирован следующий файл:

1
2
3
4
package com.javacodegeeks.advanced.processor.examples;
 
public class MutableClassImmutable {
}

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

5. Запуск процессоров аннотаций

Компилятор Java позволяет легко подключать любое количество процессоров аннотаций к процессу компиляции, поддерживая аргумент командной строки –processor. Например, вот один из способов запустить MutatingAnnotationProcessor , передав его в качестве аргумента инструмента javac во время компиляции исходного файла MutableClass.java :

1
2
3
4
javac -cp processors/target/advanced-java-part-14-java7.processors-0.0.1-SNAPSHOT.jar
  -processor com.javacodegeeks.advanced.processor.MutatingAnnotationProcessor   
  -d examples/target/classes
  examples/src/main/java/com/javacodegeeks/advanced/processor/examples/MutableClass.java

Компиляция только одного файла не выглядит очень сложной, но реальные проекты содержат тысячи исходных файлов Java, и использование инструмента javac из командной строки для их компиляции является просто излишним. Вероятно, сообщество разработало множество отличных инструментов сборки (таких как Apache Maven , Gradle , sbt , Apache Ant и т. Д.), Которые заботятся о вызове Java-компилятора и выполняют много других задач, поэтому в настоящее время большинство Java-проектов существует. используйте хотя бы один из них. Вот, например, способ вызова MutatingAnnotationProcessor из файла сборки Apache Maven ( pom.xml ):

01
02
03
04
05
06
07
08
09
10
11
12
<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-compiler-plugin</artifactId>
  <version>3.1</version>
  <configuration>
    <source>1.7</source>
    <target>1.7</target>
    <annotationProcessors>
<proc>com.javacodegeeks.advanced.processor.MutatingAnnotationProcessor</proc>
    </annotationProcessors>
  </configuration>
</plugin>

6. Что дальше

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

7. Загрузите исходный код

Вы можете скачать исходный код этого урока здесь: advanced-java-part-14