Внедрение AspectJ в Android-приложение

от автора

Хочу поделиться с вами своим опытом изучения парадигмы АОП и разработки с её использованием в крупном проекте.

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

Допустим, что у нас есть довольно тривиальная задача — обеспечить приложению поддержку некоторых языков (russian, english, italian, french, etc.). Вы скажете, что у нас есть языковая и региональная дифференциация всех ресурсов, и будете правы. За исключением случая, когда приложение пользуется не встроенными ресурсами, а «тянет» их с сервера. В общем-то, такая ситуация встречается часто и решается тривиально — добавляем в абстрактный класс BaseActivity, который у нас наследуется от системного, пару строчек на обработчик, и всё работает. А можно обойтись и без этих пары строчек. И даже без базового класса. А при необходимости — просто скопировать в приложение один файл или добавить зависимость в gradle, которая всё сделает сама.

Итак, задача ясна, пишем.

package com.archinamon.example.xpoint;  import android.support.v7.app.AppCompatActivity; import android.app.Application;  public aspect LocaleMonitor {      pointcut saveLocale(): execution(* MyApplication.onCreate());     pointcut checkLocale(AppCompatActivity activity): this(activity) && execution(* AppCompatActivity+.onCreate(..));      after(): saveLocale() {         saveCurrentLocale();     }      before(AppCompatActivity activity): checkLocale(activity) {         if (isLocaleChanged()) {             saveCurrentLocale();             restartApplication(activity);         }     }      void saveCurrentLocale() {/* implementation */}     void restartApplication(AppCompatActivity context) {/* implementation */}     boolean isLocaleChanged() {/* implementation */} } 

Добавив этот класс в наше приложение, мы научим его перезапускать само себя при смене языка в системе.

Некоторое время я искал готовые решения и пробовал их на вкус. В итоге написал свой вариант. Как это всё работает, зачем пришлось изобретать велосипед и что из всего этого вышло — под катом!

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

Философия

Из этой оговорки следует, что аспектный класс представляет собой декоратор над объектным классом. Подходя к задаче реализации некоторой функциональности на аспектах, важно не забывать об объектно-ориентированных механизмах языка и идеологии как таковой. Bad way — когда аспект превращается в нечитабельный и напичканный служебными инструментами класс. Best way — описать всю логику отдельным объектным модулем, максимально изолированным от внешнего мира, который подключается к приложению через аспектный декоратор.

Говоря об аспектном подходе к разработке как о некоем декорирующем механизме, следует отметить его основные возможности, которые были частично продемонстрированы в примере. Понятие процедур, функций и методов заменяется термином «совет» (advice). Совет может быть применён до (before), после (after) или вместо (around) того участка кода, куда мы встроились через срез. В свою очередь, понятие среза (pointcut) скрывает под собой некоторое описание точки(ек) в нашей программе, куда аспектный класс подключается. Это описание представляет собой целый набор параметров — имя класса и/или сигнатура метода, место его вызова или исполнения, и т.п. Один срез может описывать минимум одну точку соединения (joinPoint), но максимум не ограничивается и может пронизывать всё приложение.

Тестирование

Еще одна область, где аспекты могут себя проявить во всей красе — тестирование и отладка. Написать универсальный профайлер методов и классов легче, чем может показаться.

aspect MyProfilerImpl extends Profiler

package com.archinamon.example.xpoint;  aspect MyProfilerImpl extends Profiler {      private pointcut strict(): within(com.archinamon.example.*) && !within(*.xpoint.*);      pointcut innerExecution(): strict() && execution(!public !static * *(..));     pointcut constructorCall(): strict() && call(*.new(..));     pointcut publicExecution(): strict() && execution(public !static * *(..));     pointcut staticsOnly(): strict() && execution(static * *(..));      private pointcut catchAny(): innerExecution() || constructorCall() || publicExecution() || staticsOnly();      before(): catchAny() {         writeEnterTime(thisJoinPointStaticPart);     }      after(): catchAny() {         writeExitTime(thisJoinPointStaticPart);     } }  abstract aspect Profiler issingleton() {      abstract pointcut innerExecution();     abstract pointcut constructorCall();     abstract pointcut publicExecution();     abstract pointcut staticsOnly();      protected static final Map<String, Long> sTimeData = new ConcurrentHashMap<>();      protected void writeEnterTime(JoinPointю.StaticPart jp) {/* implementation */}     protected void writeExitTime(JoinPoint.StaticPart jp) {/* implementation */} } 

Срез strict() урезает обход точек соединения, чтобы исключить обход самих аспектных классов. В остальном описанная структура предельна проста и интуитивно понятна. Я намеренно разделяю выборку методов, конструкторов и статических методов в разные срезы — это позволит гибко настраивать профайлер под конкретное приложение и конкретную задачу. Маркер issingleton() в описании абстрактного аспектного класса явно декларирует, что каждый наследник будет являться синглтоном. По сути, запись ненужная, т.к. все аспектные классы являются синглтонами по умолчанию. В нашем же случае этот маркер здесь нужен, чтобы проинформировать об этом свойстве стороннего разработчика. В своей практике я предпочитаю маркировать неявный функционал — таким образом повышая понимание и читаемость модуля для других.

Перейдём непосредственно к тестированию. Чем аспекты эффективны?

  • В первую очередь своей внутренней механикой. Мы тестируем секцию кода в её привычной среде обитания, а не пытаемся эмулировать контекст применения конкретного функционала (функции, процедуры, самого объекта и т.п.).
  • Второй важный плюс — богатство отладочной информации, имеющейся в точке соединения благодаря инъекциям на этапе компиляции.
  • Третий и очень важный плюс кроется в силе так называемого NamePattern, которым описываются все параметры среза. Нейм-паттерном можно покрыть огромное количество однотипных и похожих участков (например, захватить все геттеры-сеттеры срезом в одну строчку).

Всё это даёт большой профит при написании юнит- и функциональных тестов. Но всегда есть важное «но». Тестирование на аспектах скорее является анализом в режиме реального времени. Для реализации классического цикла тестирования всё равно необходимо описать некоторое окружение или контекст, в котором будут исполняться тестировочные декораторы. А значит, в качестве замены привычным фреймворкам АОП не подойдёт.

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

P.S. Полезные мелочи

А еще на аспектах можно покрывать классы и/или методы обработчиками исключений одним взмахом пальцев:

abstract aspect NetworkProtector {      abstract pointcut myClass();      Response around(): myClass() && execution(* executeRequest(..)) {         try {             return proceed();         } catch (NetworkException ex) {             Response response = new Response();             response.addError(new Error(ex));             return response;         }     } } 

Это самый простой вариант, можно и сложнее, описывая детально и по шагам все этапы.

Велосипед v3.14: зачем и как?

Освоить АОП можно и без Андроида. Чтобы использовать технологию в своём проекте, я занялся написанием собственного плагина к билд-системе Gradle. Но ведь уже есть готовые решения, скажет осведомлённый читатель! И будет прав. И будет не полностью прав, т.к. все имеющиеся подходили лишь под какой-то узкий спектр условий, но не покрывали все необходимые и возможные комбинации. Например, ни один плагин не умел корректно работать с флаворами или создавать свой source-set для декомпозиции исходного кода. А некоторые позволяли писать аспекты только в стиле java-аннотаций. Вот я и собрал все грабли на пути к внедрению собственного плагина. Который, в результате, покрыл все узкие места.

И даже обзавёлся хакнутым семантическим плагином (привет Spring@AOP’у из Ultimate версии!) для личного удобства.

Ждём официальный релиз в будущих версиях Android Studio

Issue Title: AspectJ support
Labels: Type-Enhancement
Subcomponent-Tools-Studio Subcomponent-Tools-gradle-ide
Priority-Small Target-1.6

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

  • Организация стэка задач компилятора

    Здесь и поддержка инструментов препроцессинга, и Retrolambda доставила головной боли. Это была первая и, субъективно, самая сложная грабля. Я впервые взялся за написание расширения для Gradle, и все подводные камни с управлением стэком задач собрал на первом же этапе разработки. По итогу плагин явным образом проверяет проект на подключение Retrolambda к нему и, если результат положительный, — встраивается в очередь перед его таском. В противном случае — становится в очередь сразу за java-компилятором.
  • Перфоманс и incremental build

    Правильно организовать стэк задач — половина дела. Оптимизировать его и дать фору перфомансу — задача другого уровня. AspectJ подключается вместе с собственным компилятором — ajc. И поэтому всё обрабатывается руками. Скажу честно, здесь еще есть куда развиваться плагину. Задачи кодогенерации препроцессором, сборки java-исходников, сборка dex-файлов (исполняемые файлы Android среды) работают в привычно оптимизированных условиях. При этом ajc всё еще работает не в инкрементальном режиме.
  • Адаптация воркспейса и Gradle-инструментов

    Сначала я реализовал базовые фичи и поддержку популярных плагинов. Исходный код компилируется, аспекты встраиваются, приложение билдится. В проекте, который стал тестовым полигоном, подступала страшная дата внедрения флаворов. Понял, что весь плагин станет неактуальным, если не сумеет подружиться с этими инструментами. А параллельно хотелось удобно и лаконично обустроить рабочую область с исходниками. Вскоре плагин научился работать с билд-вариантами, правильно встраиваться в их задачи. А в конце обзавёлся собственной папкой в ресурсах — aspectj, наравне с java, groovy, aidl и другими.

Что взять с собой?

До кучи к аспектам берём еще синтаксис Java 8 и StreamAPI из той же восьмёрки (это нужно для функциональной работы с массивами, коллекциями и списками) — ведь в Android API уже включён Java API, и новшествами из восьмёрки он, увы, не хвастается.

build.gradle файл проекта преображается.

buildscript {     repositories {         mavenCentral()         maven { url 'https://raw.github.com/Archinamon/GradleAspectJ-Android/master/' }         maven { url 'https://raw.github.com/Archinamon/RetroStream/master/' }     }      dependencies {         //retrolambda         classpath 'me.tatarka:gradle-retrolambda:3.2.3'         //aspectj         classpath 'com.archinamon:AspectJ-gradle:1.0.16'     } }  // Required because retrolambda is on maven central repositories {     mavenCentral() }  apply plugin: 'com.android.application' //or apply plugin: 'java' or even apply plugin: 'com.android.library' apply plugin: 'me.tatarka.retrolambda' apply plugin: 'com.archinamon.aspectj'  dependencies {     compile 'com.archinamon:RetroStream:1.0.4' } 

На этом — всё! Подробную настройку я оставлю за кулисами, но все детали вы сможете найти в исходниках на гитхабе.

Ссылки

Демонстрационный проект.
Gradle-плагин для Android Studio.

ссылка на оригинал статьи http://habrahabr.ru/post/269371/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *