Обложка: Пример использования аспектно-ориентированного программирования + GRADLE + DB + Intellij

Пример использования аспектно-ориентированного программирования + GRADLE + DB + Intellij

Руслан Кондратьев
Руслан Кондратьев

backend-разработчик «Инфомаксимум»

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

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

Зачем нужно аспектно-ориентированное программирование

Прежде всего выясним, для чего нам понадобилось задействовать механизм АОП. Допустим, поставлена задача: оценить производительность таблиц с движком ReplaceMergeTree базы данных ClickHouse (движок в фоне производит попытку удаления дубликатов записей на основе упорядочения определенного набора полей). Выполнить задачу нужно на базе существующего backend-модуля, в котором уже есть готовые сервисные слои для записи в базу.

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

Уже набегает на целый класс или сервис, который необходимо подключать. С одним-двумя. классами можно справиться. А если их десять? А если при этом надо ещё что-то менять и «подкручивать»? И что делать, если код оказался унаследованным в виде библиотеки и неизменным? Обычных принципов ООП тут не хватает. И нужно применить способ, позволяющий реализовывать сквозной функционал. Аспектно-ориентированное программирование как раз для этих целей.

Пример работы с АОП

Как только разобрались, зачем нам нужен АОП, переходим к поиску существующих инструментов. В сети находим:

  • Spring AOP. Он на базе динамического заместителя CGLIB и заместителя JDK. Само связывание динамическое. Хорошо, но подходит, если модуль создан на Spring, у нас — нет;
  • Веб-контейнер и EJB-контейнер, но у нас не Java EE;
  • AspectJ. Библиотека от Eclipse. Здесь уже статическое связывание, но пересобрать проект всегда есть возможность. Останавливаемся на этой библиотеке, как удовлетворяющей нашим требованиям.

Не путать с AssertJ! Это тоже крутая штука, заслуживающая отдельного внимания. 

Смотрим на чём основан проект. Так как у нас в качестве билдера будет использоваться Gradle 4.8 (старый, но ничего страшного), то будем рассматривать на его примере. Для него есть плагин freefair — как раз для аспектов. Подключим его в наш билд, а именно, в репозиторий buildscript добавим блок:

maven {
	url "https://plugins.gradle.org/m2/"
}

В зависимости dependencies того же блока добавим строку:

classpath "io.freefair.gradle:aspectj-plugin:6.0.0-m2"

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

apply plugin: 'io.freefair.aspectj.post-compile-weaving'

Вот, собственно, и всё. Больше ничего добавлять нет необходимости, никаких конфигураций и источников объявлять не нужно.

Обратимся к интересующему нас участку кода:

Есть метод, записывающий коллекцию данных в таблицу, который надо отпрофилировать. Для реализации профайлера напишем класс и применим в нём необходимые аннотации и ключевые слова (рис.1). Разберём, что получилось.

На рисунке 1 представлен класс, перехватывающий вызов метода writeDomainsToCHTable. Для этого был описан так называемый PointCut-срез (строка 25). Реализация срезов позволяет перехватывать вызовы методов по сигнатурам, аннотациям, генерируемым исключениям, названиям и так далее.

Функционал очень развит, позволяет делать много интересного. Применяется для этого Expression Language (EL). Описав необходимые срезы (а их может быть более одного), нужно описать необходимые интерцепторы с нужной логикой. В данном примере описано поведение до вызова метода (аннотация @Before), перехват вызова (аннотация @Around) и после вызова (аннотация @After).

Рисунок 1. Реализация профилировщика на базе концепций АОП и библиотеки AspectJ.

До вызова метода необходимо сделать запрос в базу данных и выяснить сколько там записей, а также сбросить таймер. Во время вызова включить таймер, «прогнать» метод (строка 42) и зафиксировать время выполнения метода, а после выполнения вывести в лог отчёт (строка 48). Внутри аннотаций перехватов указывается тот срез или комплект срезов, на базе которого будут срабатывать интерцепторы.

Стоит отметить, что поведение интерцепторов очень схоже со слежением за жизненным циклом или процессом валидации (javax.validation) в EJB-контейнерах. Поэтому тут нет никакой магии.

Что получилось?

Разберём, как работает разработанный профайлер. Когда выполнение кода доходит до точки вызова метода ActivityAppendableSynchronizer::writeDomainsToCHTable происходит перехват этого вызова, так как после компиляции у нас уже есть некий класс, который фактически проксирует прокинутые срезы. Непосредственно перед вызовом испытуемого метода выполнение попадает на строку 30. Выполнившись до конца, переход осуществиться на строку 41. Выйдя из метода aroundSynch, управление передается в строку 48.

Таким образом, правильно описав срезы и определив логику в событийных точках к этим срезам, можно создавать аспекты (аннотация @Aspect), которые будут применять сквозной функционал к обозначенным срезам. Данный пример можно применять на любом реальном методе, никаких изменений не потребуется. На мой взгляд, применение принципов АОП не сложнее, чем ООП.

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

Хинт для программистов: если зарегистрируетесь на соревнования Huawei Cup, то бесплатно получите доступ к онлайн-школе для участников. Можно прокачаться по разным навыкам и выиграть призы в самом соревновании.

Перейти к регистрации