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

Что такое обработка аннотаций?

Так что же такое обработка аннотаций? Аннотации - это своего рода механизм тегов, вы можете пометить некоторые мета на классах, методах или параметрах, а обработка аннотаций проанализирует эти аннотации во время компиляции и сгенерирует для вас классы в соответствии с вашими аннотациями. Вот как это работает.

  1. Создайте классы аннотаций.
  2. Создайте классы синтаксического анализатора аннотаций.
  3. Добавьте аннотации в свой проект.
  4. Анализаторы компиляции и аннотаций будут обрабатывать аннотации.
  5. Автоматически созданные классы будут добавлены в папку сборки.

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

Пример

Давайте попробуем реализовать простой заводской шаблон с помощью обработки аннотаций. (Часто используют отражение как еще один способ реализации).

Предположим, у нас есть Dog и Cat, которые нужно создать Factory Class.

public interface Animal {
  void bark();
}
public class Dog implement Animal {
  @Override
  public void bark() { System.out.println("woof"); }
}
public class Cat implement Animal {
  @Override
  public void bark() { System.out.println("meow"); }
}
public class AnimalFactory {
  public static Animal createAnimal() {
    switch(tag) {
      case "dog":
        return new Dog();
      case "cat":
        return new Cat();
      default:
        through new RuntimeException("not support animal");
  }
}
public static void main(String[] args) {
  Animal dog = AnimalFactory.createAnimal("dog");
  dog.bark(); // woof
  Animal cat = AnimalFactory.createAnimal("cat");
  cat.bark(); // meow
}

Нам не нужно заботиться о том, что такое Dog и Cat, когда мы создаем экземпляр с помощью AnimalFactory, но AnimalFactory довольно беспорядочный. Давайте попробуем использовать функцию обработки аннотаций для автоматического создания этого файла.

Создать класс аннотации

@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface AutoFactory {
}
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface AutoElement {
    String tag();
}

Здесь мы определяем две аннотации для Factory и Real Class. @Retention - это то место, где мы определяем жизненный цикл аннотаций. Есть SOURCE (используется только при компиляции), CLASS (будет упаковано в файл .class) и RUNTIME (используется во время выполнения). Здесь мы используем SOURCE, поскольку нам нужно только время компиляции. @Target - это индикатор того, какой тип цели нужно аннотировать. TYPE используется для класса и интерфейса.

Создать синтаксический анализатор аннотаций

Мы будем использовать следующую библиотеку для легкой настройки:

compile 'com.google.auto.service:auto-service:1.0-rc4'
compile 'com.squareup:javapoet:1.10.0'

Вот как выглядит базовая форма пользовательского процессора аннотаций.

@AutoService(Processor.class)
public class AutoFactoryProcesser extends AbstractProcessor {
  @Override
  public synchronized void init(ProcessingEnvironment env) { ... }
  @Override
  public Set<String> getSupportedAnnotationTypes() { ... }

  @Override
  public SourceVersion getSupportedSourceVersion() { ... }
  @Override
  public boolean process(Set<? extends TypeElement> set,          
                         RoundEnvironment env) { ... }
}

AbstractProcessor - это плагин, который мы можем добавить в конвейер компиляции, а также анализировать исходный код и генерировать код. @AutoService - это библиотека Google, которая помогает нам автоматически связывать наш процессор с компилятором. А другие методы мы обсудим ниже по очереди.

private Filer filer;
private Messager messager;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
    super.init(processingEnv);
    filer = processingEnv.getFiler();
    messager = processingEnv.getMessager();
}

Мы можем получить ProcessingEnvironment в функции инициализации, которая содержит Filer (файл записи) и Messager (цель журнала). Если вы не хотите добавлять функцию инициализации, кстати, переменная processingEnv будет храниться в родительском классе.

@Override
public Set<String> getSupportedAnnotationTypes() {
    Set<String> annotations = new LinkedHashSet<>();
    annotations.add(AutoFactory.class.getCanonicalName());
    annotations.add(AutoElement.class.getCanonicalName());
    return annotations;
}

Нам нужно определить, какие аннотации нам нужны в getSupportedAnnotationTypes для компилятора с точки зрения производительности. Здесь мы добавляем две наши аннотации.

@Override
public SourceVersion getSupportedSourceVersion() {
    return SourceVersion.latestSupported();
}

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

@Override
public boolean process(Set<? extends TypeElement> set,
                       RoundEnvironment roundEnvironment) {
  Map<ClassName, List<ElementInfo>> result = new HashMap<>();
  for (Element annotatedElement : roundEnvironment.getElementsAnnotatedWith(AutoFactory.class)) {
    if (annotatedElement.getKind() != ElementKind.INTERFACE) {
      error("Only interface can be annotated with AutoFactory", annotatedElement);
      return false;
    }
    TypeElement typeElement = (TypeElement) annotatedElement;
    ClassName className = ClassName.get(typeElement);
    if (!result.containsKey(className)) {
      result.put(className, new ArrayList<>());
    }
  }
  for (Element annotatedElement : roundEnvironment.getElementsAnnotatedWith(AutoElement.class)) {
    if (annotatedElement.getKind() != ElementKind.CLASS) {
      error("Only class can be annotated with AutoElement", annotatedElement);
      return false;
    }
    AutoElement autoElement = annotatedElement.getAnnotation(AutoElement.class);
    TypeElement typeElement = (TypeElement) annotatedElement;
    ClassName className = ClassName.get(typeElement);
    List<? extends TypeMirror> list = typeElement.getInterfaces();
    for (TypeMirror typeMirror : list) {
      ClassName typeName = getName(typeMirror);
        if (result.containsKey(typeName)) {
          result.get(typeName).add(new ElementInfo(autoElement.tag(), className));
          break;
        }
      }
  }
  try {
    new FactoryBuilder(filer, result).generate();
  } catch (IOException e) {
    error(e.getMessage());
  }
  return false;
}

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

public void generate() throws IOException {
  for (ClassName key : input.keySet()) {
    MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("create" + key.simpleName())
        .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
        .addParameter(String.class, "type")
        .beginControlFlow("switch(type)");
    for (ElementInfo elementInfo : input.get(key)) {
      methodBuilder
          .addStatement("case $S: return new $T()", elementInfo.tag, elementInfo.className);
    }

    methodBuilder
        .endControlFlow()
        .addStatement("throw new RuntimeException(\"not support type\")")
        .returns(key);
    MethodSpec methodSpec = methodBuilder.build();
    TypeSpec helloWorld = TypeSpec.classBuilder(key.simpleName() + "Factory")
        .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
        .addMethod(methodSpec)
        .build();
    JavaFile javaFile = JavaFile.builder(key.packageName(), helloWorld)
        .build();

    javaFile.writeTo(filer);
  }
}

generate в FactoryBuilder с использованием javapoet для простой генерации кода Java, а filer сделает все остальное для записи файлов в папку сборки.

Вот и все, мы можем вернуться к нашему основному проекту.

@AutoFactory
public interface Animal {
  void bark();
}
@AutoElement(tag = "dog")
public class Dog implement Animal {
  @Override
  public void bark() { System.out.println("woof"); }
}
@AutoElement(tag = "cat")
public class Cat implement Animal {
  @Override
  public void bark() { System.out.println("meow"); }
}

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

public final class AnimalFactory {
  public static Animal createAnimal(String type) {
    switch(type) {
      case "cat": return new Cat();
      case "dog": return new Dog();
    }
    throw new RuntimeException("not support type");
  }
}

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