На этот раз мы используем разделы

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

Для этого нам понадобится еще одна библиотека. Он называется JavaPoet. Если вы прочитаете документацию на этой странице GitHub, вы увидите, что она дает нам действительно отличный API для написания новых файлов Java: суть любого процессора аннотаций. Итак, давайте поместим compile 'com.squareup:javapoet:1.7.0' в зависимости нашего процессора. Синхронизируйтесь, и тогда мы сможем идти полным ходом!

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

* An annotation processing tool framework will {@linkplain
* Processor#process provide an annotation processor with an object
* implementing this interface} so that the processor can query for
* information about a round of annotation processing.

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

roundEnv.getElementsAnnotatedWith(ComparableField.class);

… мы можем получить список всех элементов в нашем коде, который помечен @ComparableField.

Итак, что такое элемент?

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

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

Существует несколько различных типов элементов, главными из которых являются:

  • TypeElement, который обычно является классом или параметром метода.
  • VariableElement, который сопоставляется с полем.
  • ExecutableElement, который будет отображаться в метод.
  • PackageElement, который, что неудивительно, соответствует пакету.

В нашем случае мы будем иметь дело только с TypeElements и простыми старыми элементами. Я также добавлю набор, чтобы мы могли придерживаться одной аннотации @ComparableField для каждого класса, а затем создам метод для обработки каждого из элементов в нашем наборе элементов. Итак, теперь наш метод process будет выглядеть примерно так:

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
  Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(ComparableField.class);
  Set<String> annotatedClasses = new HashSet<>();
  for (Element e : elements) {
    processElement(e, annotatedClasses);
  }
  return true; //return true to consume the annotations
}
private void processElement(Element e, Set<String> annotatedClasses){
}

Один метод стал двумя! Это уже кажется немного более мясистым, хотя мы все еще ничего не делаем :)

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

TypeElement classElement = (TypeElement) e.getEnclosingElement();

поэтому мы можем получить TypeElement класса, в котором находится поле. Затем, поскольку мы хотим убедиться, что мы когда-либо генерируем только одну реализацию Comparator для каждого класса, мы можем добавить в эту проверку

if (annotatedClasses.contains(
classElement.getQualifiedName().toString())) {
  //we do something here, but I'm not quite sure what yet!
} else {
  annotatedClasses.add(classElement.getQualifiedName().toString());
}

это что-то сделает , если у нас есть более одной аннотации в этом классе, остановив процесс компиляции.

Эта штука просто посылает сообщение!

ПроцессорСреда

Для этого нам нужно переопределить другой метод в нашем процессоре аннотаций под названием init() и получить экземпляр того, что называется Messager. Делаем это так:

@Override public synchronized void init(ProcessingEnvironment processingEnv) {
  super.init(processingEnv);
  messenger = processingEnv.getMessager();
}

Мы берем момент Messager из нашего processingEnv и сохраняем его в локальном поле. Этот экземпляр ProcessingEnvironment, по сути, является нашей связью с внешним миром — мы можем получить доступ к консоли, диску и даже получить от него несколько очень полезных небольших служебных классов. На данный момент мы хотим получить только Messager. Позже мы, возможно, захотим получить доступ к некоторым другим вещам, но это ненадолго.

Экземпляр Messager, удачно названный messenger, имеет метод printMessage, который позволяет нам печатать диагностическую информацию для вывода, например, на консоль. Этот метод принимает как минимум пару вещей: тип ошибки и само сообщение. Отправка типа ERROR также приведет к тому, что процесс компиляции прервется достаточно изящно, и это предпочтительнее, чем генерировать исключение.

Итак, теперь мы отправляем сообщение об ошибке с помощью мессенджера, поэтому наша небольшая проверка на дублирование теперь будет выглядеть следующим образом:

if (annotatedClasses.contains(
classElement.getQualifiedName().toString())) {
   String msg = "You may only have one ComparableField per class!";
   messenger.printMessage(Diagnostic.Kind.ERROR, msg);
} else {
  annotatedClasses.add(classElement.getQualifiedName().toString());
}

Получение наших метаданных

Как мы можем получить аннотацию от элемента? Это не сразу видно, но у каждого элемента есть метод с именем getAnnotation(), который принимает объект класса и возвращает аннотацию, прикрепленную к этому элементу. Так как мы хотим получить @ComparableField от элемента, мы будем вызывать

ComparableField annotation = e.getAnnotation(ComparableField.class);

и это даст нам ComparableField элемента. Если бы он не был прикреплен к нему, этот метод просто вернул бы null; однако, поскольку мы уже знаем, что элемент имеет эту аннотацию, нам не нужно выполнять проверку.

Первый TypeSpec для ребенка

Теперь мы готовы написать немного Java (с помощью Java — это мета или что?), и здесь действительно помогает JavaPoet. Итак, давайте напишем себе файл класса:

String className = String.format(NAME_FORMAT, classElement.getSimpleName());
TypeSpec.Builder comparatorTypeSpec = TypeSpec.classBuilder(className) //
    .addModifiers(Modifier.PUBLIC)
    .addSuperinterface( //
        ParameterizedTypeName.get(ClassName.get(Comparator.class),
            TypeName.get(classElement.asType())));

Разве это не маленький глоток?

Прежде всего, мы хотим получить имя для нашего класса — вы можете видеть, как оно используется в методе classBuilder() — поэтому мы быстро собираем его и вставляем туда. Этот TypeSpec.Builder формирует основу структуры нашего класса. Мы можем добавить поля, методы, дженерики, модификаторы и даже аннотации, если захотим. Вы увидите, что мы установили для нашего класса значение public и реализовали интерфейс с некоторым загадочным разнообразием.

ParameterizedTypeName позволяет нам расширить класс или интерфейс, который использует дженерики, предоставив конкретный класс для использования в качестве типа параметра. В этом случае мы используем ClassName, TypeName... вариант get() — первый параметр — это имя интерфейса, который мы расширяем, а varargs — это набор типов, которые мы хотим подключить к нашим дженерикам. Если вы посмотрите на определение компаратора, вы увидите, что оно только одно, поэтому мы хотим предоставить только одно. В этом случае мы используем тип classElement. Это сгенерирует класс со следующим типом шаблона объявления:

public class ${elementName}Comparator extends Comparator<${elementName}>{}

Написание нашего сравнения()

Сейчас нам не хватает одной очень, действительно важной вещи: определения нашего метода сравнения. Давайте начнем с этого, создав пару ParameterSpec, а затем поместив их в хорошо сконструированный конструктор MethodSpec. Этим строителям ParameterSpec действительно нужны только тип и имя метода.

ParameterSpec lhsParameterSpec =
    ParameterSpec.builder(ClassName.get(classElement.asType()), LHS).build();

ParameterSpec rhsParameterSpec =
    ParameterSpec.builder(ClassName.get(classElement.asType()), RHS).build();

MethodSpec.Builder methodSpecBuilder = MethodSpec.methodBuilder(COMPARE)
    .addModifiers(Modifier.PUBLIC)
    .addAnnotation(Override.class)
    .addParameter(lhsParameterSpec)
    .addParameter(rhsParameterSpec)
    .returns(int.class);

где

private final static String COMPARE = "compare"; //our function name

Обратите внимание, что нам, кажется, не хватает фактического кода. Хотя это круто. У нас есть определение метода, так что теперь нам просто нужно заполнить этот пробел.

Поскольку мы хотим иметь возможность работать как с примитивными, так и с более сложными типами, нам нужно уметь их различать. Использование оператора > будет не слишком удачным, если мы вызовем его с парой строк, а вызов compareTo для примитива просто загрузит сборку. К счастью, есть способ проверить, является ли Элемент примитивным, даже если он немного многословен: вызвав e.asType().getKind().isPrimitive(), мы просто получим истинный или ложный ответ.

Я помещу содержимое для каждого возможного метода в соответствующие ветки для простоты понимания. Это явно не оптимально, но сгенерирует понятный код и продемонстрирует пару возможностей JavaPoet:

Кроме того, определение isPositive:

boolean isPositive(ComparableField f) {
  return f.greaterThan() == ComparableField.GreaterThan.IS_POSITIVE;
}

Большинство из них, вероятно, довольно просты, за исключением, возможно, следующего:

  • beginControlFlow звонок. Требуется строка, которая использует ее для добавления любых if, for, while и т. Д., Которые мы могли бы определить, с заключенными в скобки и отступами;
  • странное форматирование в вызовах addStatement, которые просто позволяют нам подключить итерал $L. Вы также можете использовать $S для вставки строки или $T для типа, но мы не не нужно делать это здесь;
  • и вызовы endControlFlow, которые завершают блок управления, инициированный соответствующим вызовом beginControlFlow;

Наконец, мы можем добавить наш код в наш метод:

methodSpecBuilder.addCode(compareBuilder.build());

а затем наш метод для нашего класса:

comparatorTypeSpec.addMethod(methodSpecBuilder.build());

и тогда мы можем — подождите минутку…

Как мы сохраняем файл?

Финишная полоса

Так получилось, что из этого экземпляра ProcessingEnvironment мы можем получить больше вещей:

@Override public synchronized void init(ProcessingEnvironment processingEnv) {
  super.init(processingEnv);
  filer = processingEnv.getFiler();
  elementUtils = processingEnv.getElementUtils();
  messenger = processingEnv.getMessager();
}

Это дает нам все необходимые связи с внешним миром. Вместе с нашим Messager мы получаем экземпляры Filer и Elements. Мне нужен Filer, потому что я хочу записать свой класс Java на диск. Экземпляр Elements используется в следующем методе, который даст нам имя пакета элемента:

String getPackageName(Element element) {
  return elementUtils.getPackageOf(element).getQualifiedName().toString();
}

и мы используем Filer для записи нашего java-файла на диск сразу после последнего вызова addMethod:

JavaFile file =
    JavaFile.builder(getPackageName(classElement), comparatorTypeSpec.build()).build();
try {
  file.writeTo(filer);
} catch (IOException ex) {
  messenger.printMessage(Diagnostic.Kind.ERROR, ex.getMessage());
}

и вот оно у нас! Весь наш обработчик аннотаций выглядит так довольно длинно.

Добавьте processor в свой модуль app, как и любой другой процессор аннотаций, добавьте lib в качестве зависимости компиляции, затем создайте тестовый класс и аннотируйте поле с помощью @ComparableField. После сборки вы можете увидеть, что создается класс, подобный следующему:

О нас

В jtribe мы с гордостью создаем программное обеспечение для iOS, Android и Интернета и увлечены тем, что делаем. Мы работаем с платформами iOS и Android с самого первого дня и являемся одной из самых опытных команд мобильных разработчиков в Австралии. Мы измеряем успех по влиянию, которое мы оказываем, и с более чем шестью миллионами конечных пользователей мы знаем, что наша работа имеет смысл. Это продолжает оставаться нашей движущей силой.