В последние несколько дней у меня внезапно появилось вдохновение узнать, как работают обработчики аннотаций, и написать его сам. Почему? Что ж, я использую их ежедневно в Robinhood (Dagger, Butterknife, support libs и т. Д.), И, давайте посмотрим правде в глаза, они потрясающие. Если все сделано правильно, обработчики аннотаций могут значительно сократить объем стандартного кода, который вам нужно написать, обеспечить настраиваемую проверку ошибок времени компилятора и легко добавить другие утилиты.

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

Конечная цель

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

public class MyFragment extends BaseFragment {
    private static final String EXTRA_FOO = "foo";
    
    public interface Callbacks {
        ...
    }
    private String foo;
 
    public static MyFragment newInstance(String foo) {
        final Bundle args = new Bundle(1);
        args.putString(EXTRA_FOO, foo);
        final MyFragment fragment = new MyFragment();
        fragment.setArguments(args);
        return fragment;
    }
    public MyFragment() {}
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        final Bundle args = getArguments();
        this.foo = args.getString(EXTRA_FOO);
    }
    @Override
    public void onAttach(Activity activity) {
        if (!(activity instanceof Callbacks.class)) {
            throw new IllegalStateException(...);
        }
        // Dagger 2 injection
        App.getModules(activity).inject(this);
        super.onAttach(activity);
    }
    @Override
    public void onCreateView(...) {
        return inflater.inflate(...);
    }
    // This is something custom we have in BaseFragment
    @Override
    public void configureToolbar(...) {
        super.configureToolbar(...);
        toolbar.setTitle(...);
    }
}

Я хочу удалить ВСЕ ЭТО код в каждом отдельном классе фрагментов, который у нас есть, и вместо этого иметь что-то вроде этого:

@RhFragment(
        callback = MyFragment.Callbacks.class,
        layoutRes = R.layout.my_fragment,
        toolbarTitle = R.string.my_fragment_title
)
public class MyFragment extends BaseFragment {
    public interface Callbacks {
        ...
    }
    @InjectExtra String foo;
}

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

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

По сути, вы регистрируете обработчик аннотаций для своего модуля Android app / lib и указываете, какие аннотации вы хотите обрабатывать, а затем во время компиляции все элементы (классы, методы, поля и т. Д.), Которые аннотируются этими аннотациями. будет скармливаться вашему процессору. Затем вы можете взять эти элементы и делать с ними все, что хотите (проверка предупреждений / ошибок компиляции и создание новых файлов классов - единственные два основных варианта использования, которые я обнаружил). Однако имейте в виду, что обработчик аннотаций НЕ МОЖЕТ изменять существующие файлы. Эффект от этого вы увидите позже.

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

Написание обработчика аннотаций

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

getSupportedAnnotationTypes

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

в этом

Этот метод получает ProcessingEnvironment в качестве параметра, который предоставляет вам несколько полезных служебных классов:

  • Messager: позволяет ретранслировать предупреждения и сообщения об ошибках обратно на консоль во время компиляции.
  • Filer: позволяет записывать сгенерированные файлы классов в файловую систему.
  • Elements: служебный класс для работы с элементами, например проверка аннотаций.
  • Типы: служебный класс для работы с типами, например проверка на супер / подклассы.

Распространенный шаблон, который я видел в некоторых популярных библиотеках процессоров аннотаций, - это сохранение ссылки на эти классы из init в вашем классе процессора.

процесс

Вот куда пойдет твоя логика. Структура будет выглядеть примерно так:

@Override
public boolean process(Set<? extends TypeElement> annotations,
        RoundEnvironment roundEnv) {
    final Set<? extends Element> elements =
            roundEnv.getElementsAnnotatedWith(RhFragment.class);
    // Do work on the set of elements
}

Подключаем его к вашему основному модулю

Когда ваш процессор будет готов, просто добавьте его в build.gradle вашего основного модуля, как показано ниже:

apply plugin: 'com.neenbedankt.android-apt'
dependencies {
    apt project(':my-processor')
}
OR IF YOU ARE USING JACK COMPILER WITH THE NEW ANDROID STUDIO
annotationProcessor project(':my-processor')

Полезные библиотеки

АвтоСервис

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

@AutoService(Processor.class)
public class MyProcessor extends AbstractProcessor {
    ...
}

JavaPoet

Поскольку я искал вдохновения во многих библиотеках Square, я решил следовать их шаблону и использовать JavaPoet, чтобы помочь мне сгенерировать результирующие классы. Библиотека имеет очень хороший API-интерфейс для построения шаблонов и некоторые интересные функции, такие как автоматический импорт соответствующих файлов классов.

MethodSpec.methodBuilder("onCreateView")
        .addAnnotation(Override.class)
        .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
        .returns(CLASS_VIEW)
        .addParameter(CLASS_LAYOUT_INFLATER, "inflater")
        .addParameter(CLASS_VIEW_GROUP, "container")
        .addParameter(CLASS_BUNDLE, "savedInstanceState")
        .addStatement("inflater.inflate(...)")
        .build();

Возможные препятствия и ловушки

Библиотека Java

Сделайте модуль библиотеки Java, народ. Может быть, это всего лишь я, но по какой-то странной причине я решил использовать модуль библиотеки Android, а потом не мог понять, почему мой код не может ссылаться ни на один из классов javax. Как я сказал выше, создайте два библиотечных модуля - один для компилятора и один для объявлений аннотаций.

Ненужные зависимости

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

private static final ClassName CLASS_LAYOUT_INFLATER =
        ClassName.get("android.view", "LayoutInflater");

ЕСЛИ вы пишете тесты (и вам, безусловно, следует), и вы не можете разрешить специфические зависимости Android (поскольку они не находятся в пути к классам для тестов в этом модуле, если вы не создаете отдельный модуль библиотеки Android только для тестов), вы можете добавить в свой build.gradle следующее:

testCompile 'com.google.android:android:4.1.1.4'

А если вам нужны библиотеки поддержки, вам придется вручную скопировать файлы JAR библиотеки поддержки в папку libs, поскольку библиотеки Java не могут компилировать зависимости AAR.

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

Динамическое создание типов

Один из препятствий, с которым я столкнулся, заключался в том, что я пытался проверить, унаследованы ли аннотированные классы от Fragment (и выдавать ошибку компиляции, если это не так). Типы имеют удобный метод isAssignable, который может проверять логику за нас. Но подождите, для этого требуется TypeMirror ?? Лично у меня до сих пор очень смутное представление о TypeMirror, TypeElement и т. Д., Поэтому я не буду пытаться объяснять это здесь. Но вы можете создать TypeMirror или TypeElement для требуемого класса следующим образом:

elements.getTypeElement("android.app.Fragment").asType();

Отладка

Если я не убедил вас в том, что тестирование вашего процессора аннотаций - это хорошо, то сейчас вы убедитесь в этом. Одним из огромных преимуществ написания тестов для вашего процессора аннотаций является то, что ВЫ МОЖЕТЕ УСТАНОВИТЬ РАЗРЫВ В ПРОЦЕССОРЕ АННОТАЦИИ ВО ВРЕМЯ ТЕСТОВ. Это делает все намного проще, так как вы можете проверить все атрибуты сумасшедших объектов, связанных с типом / элементом, и выяснить, что с ними делать.

Результат

Учитывая этот класс:

@RhFragment(
        callback = MyFragment.Callbacks.class,
        layoutRes = R.layout.my_fragment,
        toolbarTitle = R.string.my_fragment_title
)
public class MyFragment extends BaseFragment {
    public interface Callbacks {
        ...
    }
    @InjectExtra String foo;
}

Сгенерированный файл выглядит примерно так:

public final class MyFragment_RhImpl extends MyFragment {
    private static final String EXTRA_PROCESSOR_FOO =
            "extra_rhprocessor_foo";

    public static final MyFragment newInstance(String foo) {
        final MyFragment_RhImpl fragment = new MyFragment_RhImpl();
        final Bundle args = new Bundle(1);
        if (foo == null) {
            throw new IllegalStateException("foo is null!");
        }
        args.putString(EXTRA_PROCESSOR_FOO, foo);
        fragment.setArguments(args);
        return fragment;
    }
    public MyFragment_RhImpl() {}
    @Override
    public final void onCreate(Bundle savedInstanceState) {
        final Bundle args = getArguments();
        this.foo = args.getString(EXTRA_PROCESSOR_FOO);
        super.onCreate(savedInstanceState);
    }

    @Override
    public final void onAttach(Activity activity) {
        App.getModules(activity).inject(this);
        if (!(activity instanceof MyFragment.Callbacks)) {
            throw new IllegalStateException(
                    "Host must implement MyFragment.Callbacks");
        }
        super.onAttach(activity);
    }

    @Override
    public final View onCreateView(LayoutInflater inflater,
            ViewGroup container, Bundle savedInstanceState) {
        return inflater.inflate(18574932102, container, false);
    }

    @Override
    public final void configureToolbar(Toolbar toolbar) {
        super.configureToolbar(toolbar);
        toolbar.setTitle(432743825231);
    }
}

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

// Nope
MyFragment.newInstance(foo);
// Have to do this now
MyFragment_RhImpl.newInstance(foo);

Это один из немногих недостатков, с которыми я столкнулся до сих пор.

В общем, обработчики аннотаций не такие сложные, как я думал, поэтому не стоит пугаться! Я всегда хотел писать программы, которые пишут программы за меня, и теперь я на шаг ближе к этой цели :)