Аспектно-ориентированное программирование (АОП) это подход, при котором сквозная функциональность (логирование, безопасность, транзакции, кеширование) выносится из основного кода в отдельные модули, аспекты, и подключается к нужным методам без правки самих методов. Ключевые понятия: совет (что выполнить), точка соединения (где), срез (правило выбора точек) и плетение (как аспект соединяется с кодом). В Java эталонная реализация это AspectJ, а Spring AOP строит на тех же понятиях свои прокси. Ниже: словарь терминов, пять видов советов и три примера на AspectJ с подключением через Maven.
Это тема не из легких и нечасто применяется напрямую, но во многих фреймворках и технологиях она используется под капотом. Ну и конечно, иногда на собеседованиях вас могут попросить рассказать в общих чертах, что это за зверь такой и где его можно применить.
Поэтому давайте рассмотрим основные концепции и несколько несложных примеров AOП на Java.
Итак, АОП, аспектно-ориентированное программирование, это парадигма, направленная на повышение модульности различных частей приложения за счет разделения сквозных задач. Для этого к уже существующему коду добавляется дополнительное поведение, без изменений в изначальном коде.
Иными словами, мы как бы навешиваем сверху на методы и классы дополнительную функциональность, не внося поправки в модифицируемый код.
Зачем это нужно? Рано или поздно мы приходим к тому, что обычный объектно-ориентированный подход не всегда может эффективно решить те или иные задачи. В такой момент на помощь приходит АОП и дает нам дополнительные инструменты для постройки приложения. А дополнительные инструменты это увеличение гибкости при разработке, благодаря которой появляется больше вариантов решения той или иной задачи.
Также АОП используется для обработки исключений, кеширования, выноса некоторого функционала, чтобы сделать его переиспользуемым.
Сразу отмечу, что в наших примерах будем использовать плетение во время компиляции.
Почитать об исключениях и их обработке можно в этих статьях: Исключения в Java и Исключения и их обработка. На этом у меня сегодня все.
Сегодня мы познакомились с АОП, и вы смогли увидеть, что сей зверь не так страшен, как его рисуют.
Goodbye everyone!
Кратко
- АОП выносит повторяющийся служебный код (логирование, проверки безопасности, транзакции, кеширование) из бизнес-логики в аспекты, не меняя исходные классы.
- Словарь: совет (advice) это код, который добавляем; точка соединения (join point) это место в программе; срез (pointcut) это правило, которое выбирает точки; аспект это совет плюс срез.
- Пять видов советов: @Before, @After, @AfterReturning, @AfterThrowing, @Around. Только @Around сам решает, вызывать ли целевой метод через proceed().
- Плетение (weaving) бывает во время компиляции (компилятор ajc), после компиляции (в готовые .class и jar) и во время загрузки классов (агент).
- AspectJ это эталонная реализация АОП для Java: аспекты пишут либо в файлах .aj, либо обычными классами с аннотацией @Aspect.
- Примеры статьи: приветствие перед printName, «псевдотранзакция» вокруг метода и логирование всех методов через @AfterReturning и @AfterThrowing; подключение через aspectjrt и aspectj-maven-plugin.
Это тема не из легких и нечасто применяется напрямую, но во многих фреймворках и технологиях она используется под капотом. Ну и конечно, иногда на собеседованиях вас могут попросить рассказать в общих чертах, что это за зверь такой и где его можно применить.
Поэтому давайте рассмотрим основные концепции и несколько несложных примеров AOП на Java.
Итак, АОП, аспектно-ориентированное программирование, это парадигма, направленная на повышение модульности различных частей приложения за счет разделения сквозных задач. Для этого к уже существующему коду добавляется дополнительное поведение, без изменений в изначальном коде.
Иными словами, мы как бы навешиваем сверху на методы и классы дополнительную функциональность, не внося поправки в модифицируемый код.
Зачем это нужно? Рано или поздно мы приходим к тому, что обычный объектно-ориентированный подход не всегда может эффективно решить те или иные задачи. В такой момент на помощь приходит АОП и дает нам дополнительные инструменты для постройки приложения. А дополнительные инструменты это увеличение гибкости при разработке, благодаря которой появляется больше вариантов решения той или иной задачи.Применение АОП
Аспектно-ориентированное программирование предназначено для решения сквозных задач, которые могут представлять собой любой код, многократно повторяющийся разными методами, который нельзя полностью структурировать в отдельный модуль. Соответственно, с помощью АОП мы можем оставить это за пределами основного кода и определить его по вертикали. В качестве примера можно привести применение политики безопасности в каком-либо приложении. Как правило, безопасность проходит сквозь многие элементы приложения. Тем более, политика безопасности приложения должна применяться одинаково ко всем существующим и новым частям приложения. При этом используемая политика безопасности может и сама развиваться. Вот тут нам отлично может пригодиться использование АОП. Также в качестве еще одного примера можно привести логирование. У использования АОП подхода к логированию есть несколько преимуществ по сравнению с ручной вставкой логирования:- Код для логирования легко внедрять и удалять: всего-то нужно добавить или удалить пару конфигураций некоторого аспекта.
- Весь исходный код для логирования хранится в одном месте и не нужно находить вручную все места использования.
- Код, предназначенный для логирования, можно добавить в любое место, будь то уже написанные методы и классы или же новый функционал. Это уменьшает количество ошибок разработчика.
Также при удалении аспекта из конфигурации конструкции можно быть абсолютно уверенным, что весь код трассировки удален и ничего не пропущено. - Аспекты это вынесенный отдельно код, который можно многократно переиспользовать и улучшать.
Также АОП используется для обработки исключений, кеширования, выноса некоторого функционала, чтобы сделать его переиспользуемым.
Основные понятия АОП
Чтобы продвинуться дальше в разборе темы, для начала познакомимся с главными понятиями АОП. Совет (advice) это дополнительная логика, код, который вызывается из точки соединения. Совет может быть выполнен до, после или вместо точки соединения (о них ниже). Возможные виды советов:- Перед (Before): советы данного типа запускаются перед выполнением целевых методов, точек соединения. При использовании аспектов в виде классов мы берем @Before аннотацию, чтобы пометить тип совета как идущий перед. При использовании аспектов в виде файлов .aj это будет метод before().
- После (After): советы, которые выполняются после завершения выполнения методов (точек соединения), как в обычных случаях, так и при бросании исключения.
При использовании аспектов в виде классов мы можем использовать @After аннотацию для указания, что это совет, идущий после.
При использовании аспектов в виде файлов .aj это будет метод after(). - После возврата (After Returning): данные советы выполняются только в том случае, когда целевой метод отрабатывает нормально, без ошибок.
Когда аспекты представлены в виде классов, мы можем использовать аннотацию @AfterReturning, чтобы пометить совет как выполняемый после успешного завершения.
При использовании аспектов в виде файлов .aj это будет метод after() returning (Object obj). - После бросания (After Throwing): данный вид советов предназначен для тех случаев, когда метод, то есть точка соединения выдает исключение. Мы можем использовать этот совет для некой обработки неудачного выполнения (к примеру, для отката всей транзакции или логирования с необходимым уровнем трассировки).
Для аспектов-классов аннотация @AfterThrowing используется, чтобы указать, что этот совет используется после броска исключения.
При использовании аспектов в виде файлов .aj это будет метод after() throwing (Exception e). - Вокруг (Around): пожалуй, один из самых важных видов советов, который окружает метод, то есть точку соединения, и с помощью которого мы можем, к примеру, выбрать, выполнять данный метод точки соединения или нет.
Можно написать код совета, который будет выполняться до и после выполнения метода точки соединения.
В обязанности around advice входит вызов метода точки соединения и возвращение значений, если метод что-то возвращает. То есть в этом совете можно попросту сымитировать работу целевого метода, не вызывая его, и в качестве результата вернуть что-то свое.
При аспектах в виде классов используем @Around аннотацию для создания советов, оборачивающих точку соединения. При использовании аспектов в виде файлов .aj это будет метод around().
Пять видов советов: сводная таблица
Те же пять видов советов в одной таблице:| Совет | Когда выполняется | Аннотация в классе-аспекте | Метод в файле .aj |
|---|---|---|---|
| Перед (Before) | до целевого метода | @Before | before() |
| После (After) | после метода: и при успехе, и при исключении | @After | after() |
| После возврата (After Returning) | только после успешного завершения, с доступом к результату | @AfterReturning | after() returning (Object obj) |
| После бросания (After Throwing) | только если метод бросил исключение | @AfterThrowing | after() throwing (Exception e) |
| Вокруг (Around) | оборачивает метод: сам решает, вызывать ли его через proceed(), и возвращает результат | @Around | around() |
Точка соединения, срез и аспект
Точка соединения (join point) это точка в выполняемой программе (вызов метода, создание объекта, обращение к переменной), где следует применить совет. Сама по себе она ничего не ищет: выбирает нужные точки срез, о котором ниже. В руководстве AspectJ это сформулировано так: точка соединения это четко определенная точка в потоке выполнения программы, а срез выбирает определенные точки соединения и значения в них. Срез (pointcut) это правило (выражение), которое выбирает набор точек соединения, например execution(* Main.printName(..)). Срез определяет, подходит ли данная точка соединения к данному совету. Аспект (aspect) это модуль или класс, реализующий сквозную функциональность. Аспект изменяет поведение остального кода, применяя совет в точках соединения, определенных некоторым срезом. Иными словами, это комбинация советов и точек соединения. Внедрение (introduction) это изменение структуры класса и/или изменение иерархии наследования для добавления функциональности аспекта в инородный код. Цель (target) это объект, к которому будут применяться советы.Плетение: как аспект соединяется с кодом
Плетение (weaving) это процесс связывания аспектов с другими классами или объектами, в результате которого получается объект с примененными советами (advised object). В AspectJ аспект вплетается прямо в байткод, а Spring AOP для этого создает прокси-объект (документация Spring). Это можно сделать во время компиляции, загрузки или во время выполнения. Есть три вида плетения:- плетение во время компиляции: если у вас есть исходный код аспекта и код, в котором вы используете аспекты, вы можете скомпилировать исходный код и аспект напрямую с помощью компилятора AspectJ;
- посткомпиляционное плетение (бинарное плетение): если вы не можете или не хотите использовать преобразования исходного кода для вплетения аспектов в код, вы можете взять уже скомпилированные классы или jar-файлы и внедрить аспекты;
- плетение во время загрузки: это просто бинарное плетение, отложенное до момента, когда загрузчик классов загрузит файл класса и определит класс для JVM.
Для поддержки этого требуется один или несколько «загрузчиков классов плетения». Они либо явно предоставляются средой выполнения, либо активируются с помощью «агента плетения».
AspectJ
AspectJ это конкретная реализация парадигмы АОП для Java (проект Eclipse), которая добавляет в язык срезы, советы и аспекты. Документацию можно найти вот тут.Примеры в Java
Далее для большего понимания АОП мы рассмотрим небольшие примеры уровня Hello World.
Сразу отмечу, что в наших примерах будем использовать плетение во время компиляции.
Подключение AspectJ через Maven
Сперва нам нужно прописать следующую зависимость в нашем pom.xml:
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.9.24</version>
</dependency>
Как правило, для использования аспектов применяют особый компилятор ajc. В IntelliJ IDEA по умолчанию его нет, поэтому при выборе его как компилятора приложения нужно указать путь к дистрибутиву AspectJ. Подробнее о способе выбора ajc как компилятора можно почитать в документации JetBrains.
Это был первый способ, а второй (которым я и воспользовался): прописать следующий плагин в pom.xml:
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>aspectj-maven-plugin</artifactId>
<version>1.15.0</version>
<configuration>
<complianceLevel>17</complianceLevel>
<source>17</source>
<target>17</target>
<showWeaveInfo>true</showWeaveInfo>
<verbose>true</verbose>
<Xlint>ignore</Xlint>
<encoding>UTF-8</encoding>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
После этого желательно сделать реимпорт у Мавена и запустить mvn clean compile.
А теперь перейдем непосредственно к примерам.Пример №1
Давайте создадим класс Main. В нем у нас будет точка запуска и метод, который печатает в консоли переданные ему имена:
public class Main {
public static void main(String[] args) {
printName("Толя");
printName("Вова");
printName("Саша");
}
public static void printName(String name) {
System.out.println(name);
}
}
Ничего сложного: передали имя, вывели его в консоли.
Если мы сейчас запустим, в консоли будет выведено:
Толя
Вова
Саша
Что ж, пришло время воспользоваться возможностями АОП.
Сейчас нам нужно создать файл-аспект. Они бывают двух видов: первый это файл с расширением .aj, второй это обычный класс, который реализует возможности АОП при помощи аннотаций.
Давайте сперва рассмотрим файл с расширением .aj:
public aspect GreetingAspect {
pointcut greeting() : execution(* Main.printName(..));
before() : greeting() {
System.out.print("Привет ");
}
}
Данный файл чем-то похож на класс. Разберемся, что здесь происходит:
pointcut: срез, правило выбора точек соединения;
greeting(): название данного среза;
: execution: точка соединения это выполнение метода, *: с любым типом результата, Main.printName(..): метода printName класса Main с любым списком аргументов.
Далее идет конкретный совет before(), который выполняется до вызова целевого метода, : greeting() это срез, на который данный совет реагирует, ну а ниже мы видим само тело метода, которое написано на понятном нам языке Java.
При запуске main с наличием данного аспекта мы получим вывод в консоль:
Привет Толя
Привет Вова
Привет Саша
Мы видим, что каждый вызов метода printName был модифицирован при помощи аспекта.
А теперь давайте взглянем, как будет выглядеть аспект, но уже как класс Java с аннотациями:
@Aspect
public class GreetingAspect{
@Pointcut("execution(* Main.printName(String))")
public void greeting() {
}
@Before("greeting()")
public void beforeAdvice() {
System.out.print("Привет ");
}
}
После файла аспекта .aj тут все более очевидно:- @Aspect обозначает, что данный класс является аспектом;
- @Pointcut("execution(* Main.printName(String))"): точка среза, которая срабатывает на все вызовы Main.printName с входящим аргументом типа String;
- @Before("greeting()"): совет, который применяется до вызова кода, описанного в точке среза greeting().
Привет Толя
Привет Вова
Привет Саша
Пример №2
Допустим, у нас есть некоторый метод, который осуществляет некоторые операции для клиентов и вызов этого метода из main:
public class Main {
public static void main(String[] args) {
makeSomeOperation("Толя");
}
public static void makeSomeOperation(String clientName) {
System.out.println("Выполнение некоторых операций для клиента - " + clientName);
}
}
С помощью аннотации @Around сделаем что-то типа “псевдотранзакции”:
@Aspect
public class TransactionAspect{
@Pointcut("execution(* Main.makeSomeOperation(String))")
public void executeOperation() {
}
@Around(value = "executeOperation()")
public void beforeAdvice(ProceedingJoinPoint joinPoint) {
System.out.println("Открытие транзакции...");
try {
joinPoint.proceed();
System.out.println("Закрытие транзакции....");
}
catch (Throwable throwable) {
System.out.println("Операция не удалась, откат транзакции...");
}
}
}
С помощью метода proceed объекта ProceedingJoinPoint мы вызываем оборачиваемый метод, чтобы определить его место в совете и, соответственно, код в методе, который выше joinPoint.proceed();, работает как Before, а который ниже, как After.
Если мы запустим main, в консоли мы получим:
Открытие транзакции...
Выполнение некоторых операций для клиента - Толя
Закрытие транзакции....
Если же мы добавим бросок исключения в наш метод (вдруг выполнение операции дало сбой):
public static void makeSomeOperation(String clientName)throws Exception {
System.out.println("Выполнение некоторых операций для клиента - " + clientName);
throw new Exception();
}
То мы получим вывод в консоли:
Открытие транзакции...
Выполнение некоторых операций для клиента - Толя
Операция не удалась, откат транзакции...
Получилась такая себе псевдообработка неудачи.Пример №3
В качестве следующего примера сделаем что-то типа логирования в консоли. Для начала посмотрим в Main, где у нас происходит псевдо бизнес-логика:
public class Main {
private String value;
public static void main(String[] args) throws Exception {
Main main = new Main();
main.setValue("<некоторое значение>");
String valueForCheck = main.getValue();
main.checkValue(valueForCheck);
}
public void setValue(String value) {
this.value = value;
}
public String getValue() {
return this.value;
}
public void checkValue(String value) throws Exception {
if (value.length() > 10) {
throw new Exception();
}
}
}
В main с помощью setValue мы зададим значение внутренней переменной value, далее с помощью getValue возьмем это значение и в checkValue проверим, длиннее ли это значение 10 символов. Если да, будет брошено исключение.
Теперь посмотрим на аспект, с помощью которого мы будем логировать работу методов:
@Aspect
public class LogAspect {
@Pointcut("execution(* *(..))")
public void methodExecuting() {
}
@AfterReturning(value = "methodExecuting()", returning = "returningValue")
public void recordSuccessfulExecution(JoinPoint joinPoint, Object returningValue) {
if (returningValue != null) {
System.out.printf("Успешно выполнен метод - %s, класса- %s, с результатом выполнения - %s\n",
joinPoint.getSignature().getName(),
joinPoint.getSourceLocation().getWithinType().getName(),
returningValue);
}
else {
System.out.printf("Успешно выполнен метод - %s, класса- %s\n",
joinPoint.getSignature().getName(),
joinPoint.getSourceLocation().getWithinType().getName());
}
}
@AfterThrowing(value = "methodExecuting()", throwing = "exception")
public void recordFailedExecution(JoinPoint joinPoint, Exception exception) {
System.out.printf("Метод - %s, класса- %s, был аварийно завершен с исключением - %s\n",
joinPoint.getSignature().getName(),
joinPoint.getSourceLocation().getWithinType().getName(),
exception);
}
}
Что тут происходит?
@Pointcut("execution(* *(..))"): будет срабатывать на выполнение всех методов;
@AfterReturning(value = "methodExecuting()", returning = "returningValue"): совет, который будет выполнен после успешного выполнения целевого метода.
У нас тут есть два случая:- Когда у метода есть возвращаемое значение if (returningValue != null) {
- Когда возвращаемого значения нет else {
Успешно выполнен метод - setValue, класса- Main
Успешно выполнен метод - getValue, класса- Main, с результатом выполнения - <некоторое значение>
Метод - checkValue, класса- Main, был аварийно завершен с исключением - java.lang.Exception
Метод - main, класса- Main, был аварийно завершен с исключением - java.lang.Exception
Ну и так как мы не обработали исключения, еще получим его стектрейс:
Почитать об исключениях и их обработке можно в этих статьях: Исключения в Java и Исключения и их обработка. На этом у меня сегодня все.
Сегодня мы познакомились с АОП, и вы смогли увидеть, что сей зверь не так страшен, как его рисуют.
Goodbye everyone!
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ