@AspectJ предполагает стиль объявления аспектов как обычных классов Java, аннотированных аннотациями. Стиль @AspectJ был введен проектом AspectJ в качестве части релиза AspectJ 5. Spring интерпретирует те же аннотации, что и AspectJ 5, используя библиотеку, поставляемую AspectJ для синтаксического разбора и сопоставления срезов. Однако среда выполнения АОП по-прежнему является чистой Spring AOP, а какая-либо зависимость от компилятора AspectJ или инструмента связывания отсутствует.

Компилятор AspectJ и инструмент связывания, которые позволяют использовать язык AspectJ полнофункционально.

Включение поддержки @AspectJ

Чтобы использовать аспекты @AspectJ в конфигурации Spring, необходимо включить поддержку Spring для конфигурирования Spring AOP на основе аспектов @AspectJ и автопроксирования бинов в зависимости от того, снабдили ли их советами по этим аспектам или нет. Под автопроксированием мы подразумеваем, что если Spring определяет, что бин снабжен советом по одному или нескольким аспектам, он автоматически создает прокси для этого бина, чтобы перехватывать вызовы методов и обеспечить выполнение совета по мере необходимости.

Поддержка @AspectJ может быть активирована с помощью конфигурации в стиле XML или Java. В любом случае, необходимо убедиться, что библиотека aspectjweaver.jar из AspectJ находится в пути классов вашего приложения (версия 1.8 или более поздняя). Эта библиотека доступна в каталоге lib дистрибутива AspectJ или из репозитория Maven Central.

Включение поддержки @AspectJ с помощью конфигурации Java

Чтобы активировать поддержку @AspectJ с помощью @Configurationиз Java, добавьте аннотацию @EnableAspectJAutoProxy, как показано в следующем примере:

Java
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
}
Kotlin
@Configuration
@EnableAspectJAutoProxy
class AppConfig

Включение поддержки @AspectJ с помощью XML-конфигурации

Чтобы активировать поддержку @AspectJ с помощью конфигурации на основе XML, используйте элемент aop:aspectj-autoproxy, как показано в следующем примере:

<aop:aspectj-autoproxy/>

Объявление аспекта

При активированной поддержке @AspectJ любой бин, определенный в вашем контексте приложения с помощью класса, который является аспектом @AspectJ (имеет аннотацию @Aspect), автоматически определяется Spring и используется для конфигурирования Spring AOP. Следующие два примера демонстрируют простое определение, необходимое для не слишком полезного аспекта.

На первом из двух примеров показано обычное определение бина в контексте приложения, которое указывает на класс бина, имеющий аннотацию @Aspect:

<bean id="myAspect" class="org.xyz.NotVeryUsefulAspect">
    <!-- настраиваем свойства аспекта здесь -->
</bean>

На втором из двух примеров показано определение класса NotVeryUsefulAspect, который аннотирован с помощью аннотации org.aspectj.lang.annotation.Aspect;

Java
package org.xyz;
import org.aspectj.lang.annotation.Aspect;
@Aspect
public class NotVeryUsefulAspect {
}
Kotlin
package org.xyz
import org.aspectj.lang.annotation.Aspect;
@Aspect
class NotVeryUsefulAspect

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

Автоматическое обнаружение аспектов посредством сканирования компонентов
Вы можете зарегистрировать классы аспектов как обычные бины в конфигурации Spring XML с помощью методов, аннотированных @Bean, в классах, помеченных аннотацией @Configuration, или сделать так, чтоб Spring автоматически обнаруживал их посредством сканирования пути классов - так же, как и любой другой управляемый Spring бин. Однако обратите внимание, что аннотации @Aspect недостаточно для автоматического обнаружения в пути классов. Для этого необходимо добавить отдельную аннотацию @Component (или, как вариант, специальную стереотипную аннотацию, которая соответствует правилам сканера компонентов Spring).
Снабжение советом аспектов с помощью других аспектов?
В Spring AOP сами аспекты не могут быть целями снабжения советом от других аспектов. Аннотация @Aspect для класса помечает его как аспект и, следовательно, исключает его из автопроксирования.

Объявление среза

Срезы определяют точки соединения и, таким образом, позволяют контролировать время выполнения советов. Spring AOP поддерживает только точки соединения выполнения методов для бинов Spring, поэтому можно считать, что срез совпадает с выполнением методов для бинов Spring. Объявление среза состоит из двух частей: сигнатуры, включающей имя и любые параметры, и выражения среза, которое определяет, какие именно выполнения метода нас интересуют. В стиле АОП с аннотацией @AspectJ сигнатура среза указывается определением обычного метода, а выражение среза указывается с помощью аннотации @Pointcut (метод, служащий сигнатурой среза, должен иметь тип возврата void).

Пример может помочь прояснить различие между сигнатурой и выражением среза. Следующий пример определяет срез с именем anyOldTransfer, который соответствует выполнению любого метода с именем transfer:

Java
@Pointcut("execution(* transfer(..))") // выражение среза
private void anyOldTransfer() {} // сигнатура среза
Kotlin
@Pointcut("execution(* transfer(..))") // выражение среза
private fun anyOldTransfer() {} // сигнатура среза

Выражение среза, которое формирует значение аннотации @Pointcut, является обычным выражением среза на AspectJ. Всестороннее рассмотрение языка срезов AspectJ см. в Руководстве по программированию на AspectJ (а для расширений - Памятку разработчика по AspectJ 5) или одну из книг по AspectJ (например, Eclipse AspectJ за авторством Колье и др. или AspectJ в действии за авторством Рамниваса Ладдада).

Поддерживаемые указатели срезов

Spring AOP поддерживает следующие указатели (designators) срезов AspectJ (pointcut designators/PCD) для использования в выражениях среза:

  • execution: Для согласования точек соединения при выполнении метода. Это основной указатель срезов, который следует использовать при работе с Spring AOP.

  • within: Ограничивает согласование точками соединения внутри определенных типов (выполнение метода, объявленного внутри сопоставляемого типа при использовании Spring AOP).

  • this: Ограничивает согласование точками соединения (выполнение методов при использовании Spring AOP), где ссылка на бин (прокси Spring AOP) является экземпляром заданного типа.

  • target: Ограничивает согласование точками соединения (выполнение методов при использовании Spring AOP), где целевой объект (проксируемый объект приложения) является экземпляром заданного типа.

  • args: Ограничивает согласование точками соединения (выполнение методов при использовании Spring AOP), где аргументами являются экземпляры заданных типов.

  • @target: Ограничивает согласование точками соединения (выполнение методов при использовании Spring AOP), где класс выполняющего объекта имеет аннотацию данного типа.

  • @args: Ограничивает согласование точками соединения (выполнение методов при использовании Spring AOP), где тип времени выполнения переданных фактических аргументов имеет аннотации заданных типов.

  • @within: Ограничивает согласование точками соединения внутри типов, имеющих данную аннотацию (выполнение методов, объявленных в типах с помощью данной аннотации, при использовании Spring AOP).

  • @annotation: Ограничивает согласование точками соединения, где субъект точки соединения (метод, выполняемый в Spring AOP) имеет заданную аннотацию.

Другие типы указателей

Полнофункциональный язык срезов AspectJ поддерживает дополнительные указатели срезов, которые не поддерживаются в Spring:call, get, set, preinitialization, staticinitialization, initialization, handler, adviceexecution, withincode, cflow, cflowbelow, if, @this и @withincode. Использование этих указателей срезов в выражениях среза, интерпретируемых Spring AOP, приводит к возникновению IllegalArgumentException.

Набор указателей срезов, поддерживаемых Spring AOP, может быть расширен в будущих выпусках для обеспечения поддержки большего количества указателей срезов из AspectJ.

Поскольку Spring AOP ограничивает согласование только точками соединения выполнения метода, в предыдущем описании указателей срезов было дано более узкое определение, чем то, которое вы можете найти в руководстве по программированию на AspectJ. Кроме того, сам AspectJ имеет семантику, основанную на типах, и в точке соединения выполнения и this, и target ссылаются на один и тот же объект: объект, выполняющий метод. Spring AOP – это система на основе прокси, в которой различается сам прокси-объект (который привязан к this) и целевой объект за прокси (который привязан к target).

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

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

Если помимо прочего требуется перехватывать вызовы методов или даже конструкторы внутри целевого класса, изучите возможность использования нативного связывания AspectJ, управляемого Spring, вместо основанного на прокси фреймворка Spring AOP. Нативное связывание представляет собой иной способ использования АОП с другими характеристиками, поэтому обязательно ознакомьтесь со связыванием, прежде чем принимать решение.

Spring AOP также поддерживает дополнительный PCD под названием бин. Этот PCD позволяет ограничить согласование точек соединения определенными именованными бинами Spring или набором именованных бинов Spring (при использовании подстановочных знаков). Bean PCD имеет следующую форму:

Java
bean(idOrNameOfBean)
Kotlin
bean(idOrNameOfBean)

Маркер idOrNameOfBean может быть именем любого бина Spring. Предусмотрена ограниченная возможность использования подстановочных знаков с помощью символа *, поэтому, если установить некоторые соглашения об именовании ваших бинов Spring, то можно написать выражение PCD bean для осуществления их выборки. Как и в случае с другими указателями срезов, PCD для bean можно использовать с операторами && (and), || (or) и ! (negation).

PCD для Bean поддерживается только в Spring AOP, но не при нативном связывании на AspectJ. Это специфическое для Spring расширение стандартных PCD, которые определяет AspectJ, и поэтому оно недоступно для аспектов, объявленных в модели, аннотированной @Aspect.

PCD для Bean работает на уровне экземпляра (основываясь на концепции имен бинов Spring), а не только на уровне типов (чем ограничивается АОП на основе связывания). Указатели срезов на основе экземпляров – это функциональная возможность АОП-фреймворка Spring, основывающегося на прокси, с учетом его тесной интеграции с фабрикой бинов Spring, с помощью которой естественным и доступным способом можно определять конкретные бины по имени.

Комбинирование выражений среза

Вы можете комбинировать выражения среза, используя &&, || и ! Вы также можете ссылаться на выражения среза по имени. В следующем примере показаны три выражения среза:

Java
@Pointcut("execution(public * *(..))")
private void anyPublicOperation() {} 
@Pointcut("within(com.xyz.myapp.trading..*)")
private void inTrading() {} 
@Pointcut("anyPublicOperation() && inTrading()")
private void tradingOperation() {} 
  1. anyPublicOperation согласовывается, если точка соединения выполнения метода представляет собой выполнение любого публичного метода.
  2. inTrading согласовывается, если выполнение метода происходит в торговом модуле.
  3. tradingOperation согласовывается, если выполнение метода представляет любой публичный метод в торговом модуле.
Kotlin
@Pointcut("execution(public * *(..))")
private fun anyPublicOperation() {} 
@Pointcut("within(com.xyz.myapp.trading..*)")
private fun inTrading() {} 
@Pointcut("anyPublicOperation() && inTrading()")
private fun tradingOperation() {} 
  1. anyPublicOperation согласовывается, если точка соединения выполнения метода представляет собой выполнение любого публичного метода.
  2. inTrading согласовывается, если выполнение метода происходит в торговом модуле.
  3. tradingOperation согласовывается, если выполнение метода представляет любой публичный метод в торговом модуле.

Лучшим способом является построение более сложных выражений среза из более мелких именованных компонентов, как было показано ранее. При обращении к срезам по имени применяются обычные правила видимости Java (вы можете видеть приватные срезы в том же типе, защищенные срезы в иерархии, публичные срезы в любом месте и так далее). Видимость не влияет на согласование срезов.

Совместное использование общих определений среза

При работе с корпоративными приложениями разработчикам часто необходимо ссылаться на модули приложения и определенные наборы операций из нескольких аспектов. Для этой цели рекомендуется определить аспект CommonPointcuts, который фиксирует общие выражения среза. Такой аспект обычно напоминает следующий пример:

Java
package com.xyz.myapp;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class CommonPointcuts {
    /**
     * Точка соединения находится на уровне взаимодействия с интернетом, если определен метод
     * в типе в пакете com.xyz.myapp.web или любом подпакете
     * под этим. 
     */
    @Pointcut("within(com.xyz.myapp.web..*)")
    public void inWebLayer() {}
    /**
     * Точка соединения находится на уровне служб, если определен метод
     * в типе в пакете com.xyz.myapp.service или любом подпакете
     * под этим. 
     */
    @Pointcut("within(com.xyz.myapp.service..*)")
    public void inServiceLayer() {}
    /**
     * Точка соединения находится на уровне доступа к данным, если определен метод
     * в типе из пакета com.xyz.myapp.dao или любого подпакета
     * под этим. 
     */
    @Pointcut("within(com.xyz.myapp.dao..*)")
    public void inDataAccessLayer() {}
    /**
     * Бизнес-служба – это выполнение любого метода, определенного в службе.
     * интерфейс. Данное определение предполагает, что интерфейсы размещены в
     * пакете "service", и что типы реализации находятся в подпакетах.
     *
     * Если вы группируете интерфейсы служб по функциональным областям (например,
     * в пакетах com.xyz.myapp.abc.service и com.xyz.myapp.def.service) тогда
     * выражение среза "execution(* com.xyz.myapp..service.*.*(...))".
     * можно использовать вместо этого.
     *
     * Как вариант, можно записать выражение, используя "бин-ориентированный"
     * PCD, например, "bean(*Service)". (Предполагается, что вы
     * поименовали свои служебные бины Spring последовательным образом).
     */
    @Pointcut("execution(* com.xyz.myapp..service.*.*(..))")
    public void businessService() {}
    /**
     * Операция доступа к данным – это выполнение любого метода, определенного для
     * интерфейса dao. Данное определение предполагает, что интерфейсы размещены в
     * пакете "dao", и что типы реализации находятся в подпакетах.
     */
    @Pointcut("execution(* com.xyz.myapp.dao.*.*(..))")
    public void dataAccessOperation() {}
}
Kotlin
package com.xyz.myapp
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.Pointcut
@Aspect
class CommonPointcuts {
    /**
    * Точка соединения находится на уровне взаимодействия с интернетом, если определен метод
    * в типе в пакете com.xyz.myapp.web или любом подпакете
    * под этим. 
    */
    @Pointcut("within(com.xyz.myapp.web..*)")
    fun inWebLayer() {
    }
    /**
    * Точка соединения находится на уровне служб, если определен метод
    * в типе в пакете com.xyz.myapp.service или любом подпакете
    * под этим. 
    */
    @Pointcut("within(com.xyz.myapp.service..*)")
    fun inServiceLayer() {
    }
    /**
    * Точка соединения находится на уровне доступа к данным, если определен метод
    * в типе из пакета com.xyz.myapp.dao или любого подпакета
    * под этим. 
    */
    @Pointcut("within(com.xyz.myapp.dao..*)")
    fun inDataAccessLayer() {
    }
    /**
    * Бизнес-служба – это выполнение любого метода, определенного в службе.
    * интерфейс. Данное определение предполагает, что интерфейсы размещены в
    * пакете "service", и что типы реализации находятся в подпакетах.
    *
    * Если вы группируете интерфейсы служб по функциональным областям (например,
    * в пакетах com.xyz.myapp.abc.service и com.xyz.myapp.def.service) тогда
    * выражение среза "execution(* com.xyz.myapp..service.*.*(...))".
    * можно использовать вместо этого.
    *
    * Как вариант, можно записать выражение, используя "бин-ориентированный"
    * PCD, например, "bean(*Service)". (Предполагается, что вы
    * поименовали свои служебные бины Spring последовательным образом).
    */
    @Pointcut("execution(* com.xyz.myapp..service.*.*(..))")
    fun businessService() {
    }
    /**
    * Операция доступа к данным – это выполнение любого метода, определенного для
    * интерфейса dao. Данное определение предполагает, что интерфейсы размещены в
    * пакете "dao", и что типы реализации находятся в подпакетах.
    */
    @Pointcut("execution(* com.xyz.myapp.dao.*.*(..))")
    fun dataAccessOperation() {
    }
}

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

<aop:config>
    <aop:advisor
        pointcut="com.xyz.myapp.CommonPointcuts.businessService()"
        advice-ref="tx-advice"/>
</aop:config>
<tx:advice id="tx-advice">
    <tx:attributes>
        <tx:method name="*" propagation="REQUIRED"/>
    </tx:attributes>
</tx:advice>

Примеры

Пользователи Spring AOP чаще всего используют указатель срезов execution. Формат выражения c указателем execution следующий:

    execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern)
                throws-pattern?)

Все части, кроме шаблона возвращаемого типа(ret-type-pattern в предыдущем фрагменте), шаблона имени и шаблона параметров, являются необязательными. Шаблон возвращаемого типа определяет, каким должен быть возвращаемый тип метода, чтобы точка соединения совпадала. В качестве шаблона возвращаемого типа чаще всего используется знак *. Он соответствует любому типу возврата. Полное имя типа совпадает только в том случае, если метод возвращает данный тип. Шаблон имени совпадает с именем метода. Подстановочный знак * можно использовать в шаблоне имени как полностью, так и частично. Если вы указываете шаблон объявляющего типа, включите завершающий знак ., чтобы присоединить его к компоненту шаблона имени. Шаблон параметров немного сложнее: () соответствует методу, который не принимает никаких параметров, в то время как (..) соответствует любому количеству (нулю или более) параметров. Шаблон (*) соответствует методу, который принимает один параметр любого типа. (*,String) соответствует методу, который принимает два параметра. Первый может быть любого типа, а второй должен быть String.

В следующих примерах показаны некоторые распространенные выражения среза:

  • Выполнение любого публичного метода:

        execution(public * *(..))
  • Выполнение любого метода, имя которого начинается с set:

        execution(* set*(..))
  • Выполнение любого метода, определенного интерфейсом AccountService:

        execution(* com.xyz.service.AccountService.*(..))
  • Выполнение любого метода, определенного в пакете service:

        execution(* com.xyz.service.*.*(..))
  • Выполнение любого метода, определенного в пакете служб или одном из его подпакетов:

        execution(* com.xyz.service..*.*(..))
  • Любая точка соединения (выполнение метода только в Spring AOP) в рамках пакета служб:

        within(com.xyz.service.*)
  • Любая точка соединения (выполнение метода только в Spring AOP) в рамках пакета служб или одного из его подпакетов:

        within(com.xyz.service..*)
  • Любая точка соединения (выполнение метода только в Spring AOP), где прокси реализует интерфейс AccountService:

        this(com.xyz.service.AccountService)
    this чаще всего используется в привязываемой форме.
  • Любая точка соединения (выполнение метода только в Spring AOP), где целевой объект реализует интерфейс AccountService:

        target(com.xyz.service.AccountService)
    target чаще всего используется в привязываемой форме.
  • Любая точка соединения (выполнение метода только в Spring AOP), которая принимает один параметр и где аргумент передается во время выполнения, является Serializable:

        args(java.io.Serializable)
    args чаще всего используется в привязываемой форме.

    Обратите внимание, что приведенный в этом примере срез отличается от execution(* * *(java.io.Serializable)). Вариант "args" согласовывается, если аргумент, передаваемый во время выполнения, является Serializable, а вариант "execution" согласовывается, если в сигнатуре метода объявлен единственный параметр типа Serializable.

  • Любая точка соединения (выполнение метода только в Spring AOP), где целевой объект имеет аннотацию @Transactional:

        @target(org.springframework.transaction.annotation.Transactional)
    Также можно использовать @target в привязываемой форме.
  • Любая точка соединения (выполнение метода только в Spring AOP), где объявленный тип целевого объекта имеет аннотацию @Transactional:

        @within(org.springframework.transaction.annotation.Transactional)
    Также можно использовать @within в привязываемой форме.
  • Любая точка соединения (выполнение метода только в Spring AOP), где выполняющий метод имеет аннотацию @Transactional:

        @annotation(org.springframework.transaction.annotation.Transactional)
    Также можно использовать @annotation в привязываемой форме.
  • Любая точка соединения (выполнение метода только в Spring AOP), которая принимает один параметр, и где тип передаваемого аргумента имеет аннотацию @Classified:

        @args(com.xyz.security.Classified)
    Также можно использовать @args в привязываемой форме.
  • Любая точка соединения (выполнение метода только в Spring AOP) для бинов Spring с именем tradeService:

        bean(tradeService)
  • Любая точка соединения (выполнение метода только в Spring AOP) для бинов Spring, имена которых соответствуют выражению с подстановочными знаками *Service:

        bean(*Service)

Написание хороших срезов

Во время компиляции AspectJ обрабатывает срезы, чтобы оптимизировать эффективность согласования. Изучение кода и определение соответствия каждой точки соединения (статически или динамически) заданному срезу – это ресурсоемкий процесс. (Динамическое сопоставление означает, что соответствие нельзя полностью определить посредством статического анализа и что в код помещается тест для определения фактического соответствия при выполнении кода). При первом обнаружении объявления среза AspectJ переписывает его в форме, оптимальной для процедуры сопоставления. Что это означает? По сути, срезы переписываются в ДНФ (дизъюнктивная нормальная форма), а компоненты среза сортируются таким образом, что сначала проверяются те компоненты, которые можно вычислить с меньшими затратами ресурсов. Это означает, что не нужно беспокоиться об описании работы различных указателей срезов и можно использовать их в любом порядке в объявлении срезов.

Однако AspectJ может выполнить только то, что ему будет задано. Для оптимальной эффективности сопоставления следует подумать о том конечной цели и максимально сузить пространство поиска соответствий в определении. Существующие указатели, естественно, относятся к одной из трех групп: родовые, определительные и контекстуальные:

  • Родовые указатели отбирают конкретный вид точки соединения: execution, get, set, call или handler.

  • Определительные указатели отбирают выбирают нужную группу точек соединения (возможно, из многих родов): within и withincode

  • Контекстуальные указатели сопоставляются (и по желанию привязываются) на основе контекста: this, target и @annotation

Хорошо написанный срез должен включать, по крайней мере, первые два типа указателей (родовые и определительные). Вы можете включать контекстуальные указатели для сопоставления на основе контекста точки соединения или привязывать этот контекст для использования в совете. Указание только родового или только контекстуального указателя сработает, но может повлиять на сложность выполнения связывания (время и используемый объем памяти) из-за дополнительной обработки и анализа. Определительные указатели крайне быстро сопоставляются, а их использование означает, что AspectJ может очень быстро отсеять группы точек соединения, которые не следует обрабатывать дальше. Хороший срез всегда должен включать в себя один такой, если это возможно.

Объявление советов

Совет связан с выражением среза и выполняется перед, после или вместо выполнения метода, совпадающего со срезом. Выражение среза может быть либо простой ссылкой на именованный срез, либо непосредственным выражением срезов.

Совет Before

Вы можете объявить совет "перед" в аспекте с помощью аннотации @Before:

Java
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class BeforeExample {
    @Before("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
    public void doAccessCheck() {
        // ...
    }
}
Kotlin
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.Before
@Aspect
class BeforeExample {
    @Before("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
    fun doAccessCheck() {
        // ...
    }
}

Если используется непосредственное выражение среза, то можно переписать предыдущий пример следующим образом:

Java
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class BeforeExample {
    @Before("execution(* com.xyz.myapp.dao.*.*(..))")
    public void doAccessCheck() {
        // ...
    }
}
Kotlin
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.Before
@Aspect
class BeforeExample {
    @Before("execution(* com.xyz.myapp.dao.*.*(..))")
    fun doAccessCheck() {
        // ...
    }
}

Совет After Returning

Совет "после возврата (after returning)" выполняется, когда выполнение совпавшего метода возвращается в нормальном порядке. Объявить его можно с помощью аннотации @AfterReturning:

Java
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;
@Aspect
public class AfterReturningExample {
    @AfterReturning("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
    public void doAccessCheck() {
        // ...
    }
}
Kotlin
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.AfterReturning
@Aspect
class AfterReturningExample {
    @AfterReturning("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
    fun doAccessCheck() {
        // ...
    }
}
Можно иметь несколько объявлений советов (и других членов), все внутри одного аспекта. В этих примерах показано только одно объявление совета, чтобы обратить внимание на влияние каждого из них.

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

Java
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;
@Aspect
public class AfterReturningExample {
    @AfterReturning(
        pointcut="com.xyz.myapp.CommonPointcuts.dataAccessOperation()",
        returning="retVal")
    public void doAccessCheck(Object retVal) {
        // ...
    }
}
Kotlin
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.AfterReturning
@Aspect
class AfterReturningExample {
    @AfterReturning(
        pointcut = "com.xyz.myapp.CommonPointcuts.dataAccessOperation()",
        returning = "retVal")
    fun doAccessCheck(retVal: Any) {
        // ...
    }
}

Имя, используемое в returning атрибуте, должно соответствовать имени параметра в методе совета. Если выполнение метода возвращается, возвращаемое значение передается методу совета в качестве соответствующего значения аргумента. Выражение returning также ограничивает сопоставление только теми выполнениями метода, которые возвращают значение указанного типа (в данном случае Object, что соответствует любому возвращаемому значению).

Обратите внимание, что при использовании совета "после возврата" вернуть совершенно другую ссылку невозможно.

Совет After Throwing

Совет "после генерации исключения (after throwing)" выполняется, когда выполнение соответствующего метода завершается генерацией исключения. Его можно объявить с помощью аннотации @AfterThrowing, как показано в следующем примере:

Java
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;
@Aspect
public class AfterThrowingExample {
    @AfterThrowing("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
    public void doRecoveryActions() {
        // ...
    }
}
Kotlin
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.AfterThrowing
@Aspect
class AfterThrowingExample {
    @AfterThrowing("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
    fun doRecoveryActions() {
        // ...
    }
}

Зачастую требуется, чтобы совет запускался только при генерации исключений определенного типа, но также зачастую требуется и доступ к исключению в теле совета. Атрибут throw можно использовать как для ограничения сопоставительного поиска (при желании - в противном случае используйте Throwable в качестве типа исключения), так и для привязки сгенерированного исключения к параметру совета. В следующем примере показано, как это сделать:

Java
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;
@Aspect
public class AfterThrowingExample {
    @AfterThrowing(
        pointcut="com.xyz.myapp.CommonPointcuts.dataAccessOperation()",
        throwing="ex")
    public void doRecoveryActions(DataAccessException ex) {
        // ...
    }
}
Kotlin
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.AfterThrowing
@Aspect
class AfterThrowingExample {
    @AfterThrowing(
        pointcut = "com.xyz.myapp.CommonPointcuts.dataAccessOperation()",
        throwing = "ex")
    fun doRecoveryActions(ex: DataAccessException) {
        // ...
    }
}

Имя, используемое в атрибуте throw, должно соответствовать имени параметра в методе совета. Если выполнение метода завершается генерацией исключения, исключение передается методу совета в качестве соответствующего значения аргумента. Выражение throw также ограничивает сопоставительный поиск только теми выполнениями метода, которые генерируют исключение указанного типа(в данном случае DataAccessException).

Обратите внимание, что @AfterThrowing не указывает на общий обратный вызов обработки исключений. В частности, метод совета, аннотированный @AfterThrowing, должен получать исключения только от самой точки соединения (объявленного пользователем целевого метода), но не от сопутствующего метода @After/@AfterReturning.

Совет After (Finally)

Совет "после (окончательно) (After (finally))" выполняется, когда происходит завершение выполнения соответствующего метода. Он объявляется с помощью аннотации @After. Совет "после" необходимо подготовить к работе как с обычными, так и с исключительными условиями возврата. Обычно он используется для высвобождения ресурсов и схожих целей. В следующем примере показано, как использовать совет "после (завершения)":

Java
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.After;
@Aspect
public class AfterFinallyExample {
    @After("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
    public void doReleaseLock() {
        // ...
    }
}
Kotlin
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.After
@Aspect
class AfterFinallyExample {
    @After("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
    fun doReleaseLock() {
        // ...
    }
}

Обратите внимание, что совет, помеченный аннотацией @After, в AspectJ определяется как "совет после (завершения)", аналогично блоку "finally" в инструкции обработки исключения (try-catch statement). Он будет вызван для любого результата, нормального возврата или исключения, сгенерированного из точки соединения (объявленного пользователем целевого метода), в отличие от аннотации @AfterReturning, которая применяется только к успешным нормальным возвратам.

Совет Around

Последний вид советов – это совет "вместо". Совет "вместо" заменяет выполнение сопоставленного метода. Оно может выполняться как до, так и после запуска метода и определять, когда, как, и даже будет ли метод вообще запущен. Совет "вместо" часто используется, если нужно разделить состояние "перед" и "после" выполнения метода потокобезопасным образом – например, как для запуска и остановки таймера.

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

Например, не используйте совет "вместо", если для ваших нужд достаточно совета"перед".

Совет "вместо" объявляется путем аннотирования метода с помощью аннотации @Around. Метод должен объявить Object в качестве возвращаемого типа, а первый параметр метода должен быть типа ProceedingJoinPoint. В теле метода совета нужно вызвать proceed() для ProceedingJoinPoint, чтобы запустился основной метод. Вызов proceed() без аргументов приведет к тому, что исходные аргументы вызывающего кода будут переданы базовому методу при его вызове. Для более продвинутых случаев использования существует перегруженный вариант метода proceed(), который принимает массив аргументов(Object[]). Значения в массиве будут использованы в качестве аргументов базового метода при его вызове.

Логика работы proceed при вызове с помощью Object[] немного отличается от логики работы proceed для совета "вместо", скомпилированного компилятором на AspectJ. Для совета "вместо", написанного с использованием традиционного языка AspectJ, количество аргументов, передаваемых для proceed, должно соответствовать количеству аргументов, переданных совету "вместо" (а не количеству аргументов, принимаемых базовой точкой соединения), а значение, переданное для продолжения работы в данной позиции аргумента, вытесняет исходное значение в точке соединения для сущности, к которой это значение было привязано (не переживайте, если сейчас это кажется бессмыслицей).

Подход, используемый Spring, более прост и лучше соответствует его основанной на прокси и предназначенной только для выполнения семантике. Вам нужно понимать эту разницу, только если вы компилируете аспекты, аннотированные @AspectJ, написанные для Spring, и используете метод proceed, снабженный аргументами, с компилятором и инструментом связывания AspectJ. Существует способ написания таких аспектов, который на 100% совместим как со Spring AOP, так и с AspectJ.

Значение, возвращаемое советом "вместо", – это возвращаемое значение, которое видит вызывающий код метода. Например, простой аспект кэширования может возвращать значение из кэша, если оно в нем есть, или вызывать proceed() (и возвращать это значение), если его нет. Обратите внимание, что proceed можно вызвать единожды, множество раз или вообще не вызывать в теле совета "вместо". Все это не запрещено.

Если вы объявите тип возврата метода совета "вместо" как void, то вызывающему коду всегда будет возвращаться null, фактически игнорируя результат любого вызова proceed(). Поэтому рекомендуется, чтобы метод совета "вместо" объявлял возвращаемый тип Object. Метод совета обычно должен возвращать значение, возвращаемое при вызове proceed(), даже если базовый метод имеет тип возврата void. Однако опционально совет может возвращать кэшированное значение, обернутое значение или какое-либо другое значение в зависимости от случая использования.

В следующем примере показано, как использовать совет "вместо":

Java
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.ProceedingJoinPoint;
@Aspect
public class AroundExample {
    @Around("com.xyz.myapp.CommonPointcuts.businessService()")
    public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
        // запускаем секундомер
        Object retVal = pjp.proceed();
        // stop stopwatch
        return retVal;
    }
}
Kotlin
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.Around
import org.aspectj.lang.ProceedingJoinPoint
@Aspect
class AroundExample {
    @Around("com.xyz.myapp.CommonPointcuts.businessService()")
    fun doBasicProfiling(pjp: ProceedingJoinPoint): Any {
        // запускаем секундомер
        val retVal = pjp.proceed()
        // останавливаем секундомер
        return retVal
    }
}

Параметры советов

Spring предлагает полностью типизированные советы, что означает, что вы объявляете необходимые параметры в сигнатуре совета (как можно было увидеть ранее в примерах с возвратом и генерированием исключения), а не постоянно работаете с массивами Object[]. Далее в этом разделе мы рассмотрим, как сделать аргументы и другие контекстные значения доступными для тела совета. Сначала мы рассмотрим, как написать обобщенный совет, способный распознавать метод, который данный момент снабжается советом.

Доступ к текущей JoinPoint

Для любого метода совета можно объявить в качестве его первого параметра параметр типа org.aspectj.lang.JoinPoint. Обратите внимание, что для совета "вместо" требуется объявить первый параметр типа ProceedingJoinPoint, который является подклассом JoinPoint.

Интерфейс JoinPoint предоставляет ряд полезных методов:

  • getArgs(): Возвращает аргументы метода.

  • getThis(): Возвращает прокси-объект.

  • getTarget(): Возвращает целевой объект.

  • getSignature(): Возвращает описание метода, снабжаемого советом.

  • toString(): Выводит практически используемое описание метода, снабжаемого советом.

Передача параметров в совет

Мы уже видели, как привязывать возвращаемое значение или значение исключения (используя советы "после возвращения" и "после генерации исключения"). Чтобы сделать значения аргументов доступными для тела совета, можно использовать форму привязывания args. Если вы используете имя параметра вместо имени типа в выражении args, значение соответствующего аргумента передается в качестве значения параметра при вызове совета. На примере все будет понятнее. Предположим, вам нужно снабдить советом выполнение операций DAO, которые принимают объект Account в качестве первого параметра, и вам нужен доступ к учетной записи в теле рекомендации. Вы можете написать следующее:

Java
@Before("com.xyz.myapp.CommonPointcuts.dataAccessOperation() && args(account,..)")
public void validateAccount(Account account) {
    // ...
}
Kotlin
@Before("com.xyz.myapp.CommonPointcuts.dataAccessOperation() && args(account,..)")
fun validateAccount(account: Account) {
    // ...
}

Часть выражения среза args(account,..) служит двум целям. Во-первых, она ограничивает сопоставительный поиск только теми выполнениями метода, при которых метод принимает хотя бы один параметр, а аргумент, переданный в качестве параметра, является экземпляром Account. Во-вторых, она открывает доступ совету к фактическому объекту Account через параметр account.

Другой способ написания - объявить срез, который "предоставляет" значение объекта Account, если оно соответствует точке соединения, а затем обратиться к названному срезу из совета. Это будет выглядеть следующим образом:

Java
@Pointcut("com.xyz.myapp.CommonPointcuts.dataAccessOperation() && args(account,..)")
private void accountDataAccessOperation(Account account) {}
@Before("accountDataAccessOperation(account)")
public void validateAccount(Account account) {
    // ...
}
Kotlin
@Pointcut("com.xyz.myapp.CommonPointcuts.dataAccessOperation() && args(account,..)")
private fun accountDataAccessOperation(account: Account) {
}
@Before("accountDataAccessOperation(account)")
fun validateAccount(account: Account) {
    // ...
}

Более подробную информацию см. в руководстве по программированию на AspectJ.

Прокси-объект (this), целевой объект (target) и аннотации(@within, @target, @annotation и @args) могут быть привязаны аналогичным образом. Следующие два примера показывают, как сопоставить выполнение методов, аннотированных аннотацией @Auditable, и извлечь код аудита:

В первом из двух примеров показано определение аннотации @Auditable:

Java
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Auditable {
    AuditCode value();
}
Kotlin
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
annotation class Auditable(val value: AuditCode)

Во втором из двух примеров показан совет, совпадающий с выполнением методов, помеченных аннотацией @Auditable:

Java
@Before("com.xyz.lib.Pointcuts.anyPublicMethod() && @annotation(auditable)")
public void audit(Auditable auditable) {
    AuditCode code = auditable.value();
    // ...
}
Kotlin
@Before("com.xyz.lib.Pointcuts.anyPublicMethod() && @annotation(auditable)")
fun audit(auditable: Auditable) {
    val code = auditable.value()
    // ...
}

Параметры советов и обобщения

Spring AOP может обрабатывать обобщения, используемые в объявлениях классов и параметрах методов. Предположим, у вас есть общий тип, подобный следующему:

Java
public interface Sample<T> {
    void sampleGenericMethod(T param);
    void sampleGenericCollectionMethod(Collection<T> param);
}
Kotlin
interface Sample<T> {
    fun sampleGenericMethod(param: T)
    fun sampleGenericCollectionMethod(param: Collection<T>)
}

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

Java
@Before("execution(* ..Sample+.sampleGenericMethod(*)) && args(param)")
public void beforeSampleMethod(MyType param) {
    // Реализация совета
}
Kotlin
@Before("execution(* ..Sample+.sampleGenericMethod(*)) && args(param)")
fun beforeSampleMethod(param: MyType) {
    // Реализация совета
}

Данный подход не работает для обобщенных коллекций. Таким образом, не выйдет определить срез следующим образом:

Java
@Before("execution(* ..Sample+.sampleGenericCollectionMethod(*)) && args(param)")
public void beforeSampleMethod(Collection<MyType> param) {
    // Реализация совета
}
Kotlin
@Before("execution(* ..Sample+.sampleGenericCollectionMethod(*)) && args(param)")
fun beforeSampleMethod(param: Collection<MyType>) {
    // Реализация совета
}

Чтобы это сработало, пришлось бы проверять каждый элемент коллекции, что нецелесообразно, поскольку мы также не можем решить, как обращаться с null значениям в целом. Чтобы добиться чего-то подобного, необходимо привести параметр к виду Collection<?> и вручную проверять тип элементов.

Определение имен аргументов

Привязка параметров в вызовах советов основана на сопоставлении имен, используемых в выражениях среза, с объявленными именами параметров в советах и сигнатурах методов срезов. Имена параметров недоступны через рефлексию Java, поэтому Spring AOP использует следующую стратегию для определения имен параметров:

  • Если имена параметров были явно заданы пользователем, то используются заданные имена параметров. Аннотации советов и срезов имеют необязательный атрибут argNames, который можно использовать для задания имен аргументов аннотируемого метода. Эти имена аргументов доступны во время выполнения. В следующем примере показано, как использовать атрибут argNames:

Java
@Before(value="com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)",
        argNames="bean,auditable")
public void audit(Object bean, Auditable auditable) {
    AuditCode code = auditable.value();
    //... используем код, бин и точку соединения
}
Kotlin
@Before(value = "com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)", argNames = "bean,auditable")
fun audit(bean: Any, auditable: Auditable) {
    val code = auditable.value()
    //... используем код, бин и точку соединения
}

Если первый параметр имеет тип JoinPoint, ProceedingJoinPoint или JoinPoint.StaticPart, то можно не задавать имя параметра в значении атрибута argNames. Например, если вы измените предыдущий совет, чтобы получить объект точки соединения, атрибут argNames не должен содержать его:

Java
@Before(value="com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)",
        argNames="bean,auditable")
public void audit(JoinPoint jp, Object bean, Auditable auditable) {
    AuditCode code = auditable.value();
    // ... используем код, бин и точку соединения
}
Kotlin
@Before(value = "com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)", argNames = "bean,auditable")
fun audit(jp: JoinPoint, bean: Any, auditable: Auditable) {
    val code = auditable.value()
    // ... используем код, бин и точку соединения
}

Особое обработка первого параметра типов JoinPoint, ProceedingJoinPoint и JoinPoint.StaticPart особенно удобна для экземпляров советов, которые не осуществляют сбор никакого другого контекста точки соединения. В таких ситуациях атрибут argNames можно опустить. Например, в следующем совете не нужно объявлять атрибут argNames:

Java
@Before("com.xyz.lib.Pointcuts.anyPublicMethod()")
public void audit(JoinPoint jp) {
    // ... используем точку соединения
}
Kotlin
@Before("com.xyz.lib.Pointcuts.anyPublicMethod()")
fun audit(jp: JoinPoint) {
    // ... используем точку соединения
}
  • Использовать атрибут argNames несколько неудобно, поэтому если атрибут argNames не был задан, Spring AOP просматривает отладочную информацию для класса и пытается определять имена параметров из таблицы локальных переменных. Эта информация будет присутствовать до тех пор, пока классы не будут скомпилированы с использованием отладочной информации (как минимум-g:vars). Последствия компиляции с этим флагом следующие: (1) код становится немного легче понять (реверс-инжиниринг); (2) размер файлов классов становится немного больше (обычно несущественно); (3) оптимизация для удаления неиспользуемых локальных переменных не применяется вашим компилятором. Иными словами, вам не грозят трудности, если вы будете осущесвлять построение с этим флагом.

    Если аспект @AspectJ был скомпилирован компилятором AspectJ(ajc) даже без отладочной информации, нет необходимости добавлять атрибут argNames, поскольку компилятор сохранит необходимую информацию.
  • Если код был скомпилирован без необходимой отладочной информации, Spring AOP будет пытаться проследить пары привязываемых переменных до параметров (например, если в выражении среза только одна переменная имеет привязку, а метод совета принимает только один параметр, то парная связь очевидна). Если привязка переменных является неоднозначной с учетом имеющейся информации, будет сгенерировано исключение AmbiguousBindingException.

  • Если все вышеперечисленные стратегии не сработают, будет сгенерировано исключение IllegalArgumentException.

Продолжение выполнения метода с использованием аргументов

Ранее мы отмечали, что расскажем, как написать вызов метода proceed с использованием аргументов, который будет работать последовательно в Spring AOP и AspectJ. Решение заключается в том, что сигнатура совета должна связывать все параметры метода по порядку. В следующем примере показано, как это сделать:

Java
@Around("execution(List<Account> find*(..)) && " +
        "com.xyz.myapp.CommonPointcuts.inDataAccessLayer() && " +
        "args(accountHolderNamePattern)")
public Object preProcessQueryPattern(ProceedingJoinPoint pjp,
        String accountHolderNamePattern) throws Throwable {
    String newPattern = preProcess(accountHolderNamePattern);
    return pjp.proceed(new Object[] {newPattern});
}
Kotlin
@Around("execution(List<Account> find*(..)) && " +
        "com.xyz.myapp.CommonPointcuts.inDataAccessLayer() && " +
        "args(accountHolderNamePattern)")
fun preProcessQueryPattern(pjp: ProceedingJoinPoint,
                        accountHolderNamePattern: String): Any {
    val newPattern = preProcess(accountHolderNamePattern)
    return pjp.proceed(arrayOf<Any>(newPattern))
}

Во многих случаях вам все равно приходится осуществлять эту привязку (как в предыдущем примере).

Упорядочивание советов

Что происходит, если несколько советов будут выполняться в одной и той же точке соединения? Spring AOP следует тем же правилам старшинства, что и AspectJ для определения порядка выполнения советов. Совет с наивысшим старшинством (приоритетом) выполняется первым "на входе" (таким образом, если даны два совета "перед", то первым выполняется тот, который имеет самый высокий уровень старшинства). "На выходе" из точки соединения совет с самым высоким уровнем старшинства выполняется последним (поэтому, если даны два совета "после", совет с наивысшим уровнем старшинства будет выполняться вторым).

Если два совета, определенные в разных аспектах, должны выполняться в одной и той же точке соединения, если не указано иное, порядок выполнения не определен. Вы можете контролировать порядок выполнения, задав старшинство. Это делается обычным для Spring способом: либо путем реализации интерфейса org.springframework.core.Ordered в классе аспекта, либо путем аннотирования совета аннотацией @Order. При наличии двух аспектов, аспект, возвращающий меньшее значение из Ordered.getOrder() (или значение аннотации), имеет высший ровень старшинства.

Каждый из отдельных типов советов конкретного аспекта концептуально предназначен для непосредственного применения к точке соединения. Как следствие не предполагается, что метод совета, помеченного аннотацией @AfterThrowing, будет получать исключение от сопутствующего метода, аннотированного @After/@AfterReturning.

Начиная с версии Spring Framework 5.2.7, методы совета, определенные в одном классе, помеченном аннотацией @Aspect, которые должны выполняться в одной и той же точке соединения, назначаются в соответствии уровнем старшинства на основе их типа совета в следующем порядке, начиная от самого высокого к самому низкому уровню старшинства: @Around, @Before, @After, @AfterReturning, @AfterThrowing. Обратите внимание, однако, что метод совета, аннотированный @After, фактически будет вызван после любых методов совета, помеченных аннотациями @AfterReturning или @AfterThrowing, в том же аспекте, следуя семантике "совета после (завершения)" из AspectJ для аннотации @After.

Если две части одного типа совета (например, два метода совета с аннотацией @After), определенные в одном классе, аннотированном @Aspect, должны выполняться в одной и той же точке соединения, их порядок не будет определен (поскольку javac-компилированны классы не могут получать порядок объявления в исходном коде через рефлексию). Рассмотрите возможность объединения таких методов совета в один метод совета для каждой точки соединения в каждом классе, помеченном аннотацией @Aspect, или произведите рефакторинг советов в отдельные классы, аннотированные @Aspect, которые можно упорядочить на уровне аспекта с помощью Ordered или @Order.

Введения

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

Вы можете осуществить введение, используя аннотацию @DeclareParents. Данная аннотация используется для объявления того, что у совпадающих типов есть новый родительский тип (отсюда и название). Например, если взять интерфейс UsageTracked и реализацию этого интерфейса DefaultUsageTracked, то следующий аспект объявит, что все реализаторы служебных интерфейсов также реализуют интерфейс UsageTracked (например, для сбора статистики через JMX):

Java
@Aspect
public class UsageTracking {
    @DeclareParents(value="com.xzy.myapp.service.*+", defaultImpl=DefaultUsageTracked.class)
    public static UsageTracked mixin;
    @Before("com.xyz.myapp.CommonPointcuts.businessService() && this(usageTracked)")
    public void recordUsage(UsageTracked usageTracked) {
        usageTracked.incrementUseCount();
    }
}
Kotlin
@Aspect
class UsageTracking {
    companion object {
        @DeclareParents(value = "com.xzy.myapp.service.*+", defaultImpl = DefaultUsageTracked::class)
        lateinit var mixin: UsageTracked
    }
    @Before("com.xyz.myapp.CommonPointcuts.businessService() && this(usageTracked)")
    fun recordUsage(usageTracked: UsageTracked) {
        usageTracked.incrementUseCount()
    }
}

Интерфейс, который будет реализован, определяется типом аннотированного поля. Атрибут value аннотации @DeclareParents является шаблоном типа AspectJ. Любой бин соответствующего типа реализует интерфейс UsageTracked. Обратите внимание, что в совете "перед" в предыдущем пример служебные бины могут быть непосредственно использованы в качестве реализаций интерфейса UsageTracked. При программном доступе к бину вам нужно написать следующее:

Java
UsageTracked usageTracked = (UsageTracked) context.getBean("myService");
Kotlin
val usageTracked = context.getBean("myService") as UsageTracked

Модели создания экземпляра аспекта

Это сложная тема. Если вы только начинаете изучать АОП, то можете смело пропустить этот раздел.

По умолчанию в контексте приложения существует единственный экземпляр каждого аспекта. AspectJ называет это моделью создания экземпляров-одиночек. Можно определить аспекты с альтернативными жизненными циклами. Spring поддерживает модели создания экземпляров perthis и pertarget из AspectJ; percflow, percflowbelow и pertypewithin в настоящее время не поддерживаются.

Вы можете объявить аспект perthis, задав выражение perthis в аннотации @Aspect. Рассмотрим следующий пример:

Java
@Aspect("perthis(com.xyz.myapp.CommonPointcuts.businessService())")
public class MyAspect {
    private int someState;
    @Before("com.xyz.myapp.CommonPointcuts.businessService()")
    public void recordServiceUsage() {
        // ...
    }
}
Kotlin
@Aspect("perthis(com.xyz.myapp.CommonPointcuts.businessService())")
class MyAspect {
    private val someState: Int = 0
    @Before("com.xyz.myapp.CommonPointcuts.businessService()")
    fun recordServiceUsage() {
        // ...
    }
}

В предыдущем примере действие выражения perthis заключается в том, что один экземпляр аспекта создается для каждого уникального объекта-службы, выполняющего бизнес-службу (каждый уникальный объект, связанный с this в точках соединения, соответствующих выражению среза). Экземпляр аспекта создается при первом вызове метода для объекта-службы. Аспект выходит из области видимости, если объект-служба тоже выходит из области видимости. До создания экземпляра аспекта ни один из советов в нем не выполняется. Как только экземпляр аспекта будет создан, объявленные в нем советы начнут выполняться в сопоставленных точках соединения, но только если объект-служба является тем, с которым связан данный аспект. Более подробную информацию о выражениях per см. в руководстве по программированию на AspectJ.

Модель создания экземпляров pertarget работает точно так же, как и perthis, но она создает один экземпляр аспекта для каждого уникального целевого объекта в совпадающих точках соединения.

Пример АОП

Теперь, когда вы увидели, как работают все составные части, мы можем собрать их вместе, чтобы сделать что-то практически применимое.

Иногда выполнение бизнес-служб может завершиться ошибкой из-за проблем с параллелизмом (например, из-за взаимоблокировки). Если операцию повторить, то, скорее всего, она будет успешной при следующей попытке. В случае с бизнес-службами, с которыми уместно повторять операцию в таких условиях (идемпотентные операции, при которых не нужно возвращаться к пользователю для разрешения конфликта), мы хотим доступным способом повторить такую операцию, чтобы клиентская часть не увидела PessimisticLockingFailureException. Это требование, которое очевидным образом охватывает несколько служб на уровне служб и, следовательно, идеально подходит для реализации через аспект.

Поскольку нам нужно повторить операцию, то необходимо использовать совет "вместо", чтобы можно было вызывать метод proceed несколько раз. В следующем листинге показана базовая реализация аспекта:

Java
@Aspect
public class ConcurrentOperationExecutor implements Ordered {
    private static final int DEFAULT_MAX_RETRIES = 2;
    private int maxRetries = DEFAULT_MAX_RETRIES;
    private int order = 1;
    public void setMaxRetries(int maxRetries) {
        this.maxRetries = maxRetries;
    }
    public int getOrder() {
        return this.order;
    }
    public void setOrder(int order) {
        this.order = order;
    }
    @Around("com.xyz.myapp.CommonPointcuts.businessService()")
    public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
        int numAttempts = 0;
        PessimisticLockingFailureException lockFailureException;
        do {
            numAttempts++;
            try {
                return pjp.proceed();
            }
            catch(PessimisticLockingFailureException ex) {
                lockFailureException = ex;
            }
        } while(numAttempts <= this.maxRetries);
        throw lockFailureException;
    }
}
Kotlin
@Aspect
class ConcurrentOperationExecutor : Ordered {
    private val DEFAULT_MAX_RETRIES = 2
    private var maxRetries = DEFAULT_MAX_RETRIES
    private var order = 1
    fun setMaxRetries(maxRetries: Int) {
        this.maxRetries = maxRetries
    }
    override fun getOrder(): Int {
        return this.order
    }
    fun setOrder(order: Int) {
        this.order = order
    }
    @Around("com.xyz.myapp.CommonPointcuts.businessService()")
    fun doConcurrentOperation(pjp: ProceedingJoinPoint): Any {
        var numAttempts = 0
        var lockFailureException: PessimisticLockingFailureException
        do {
            numAttempts++
            try {
                return pjp.proceed()
            } catch (ex: PessimisticLockingFailureException) {
                lockFailureException = ex
            }
        } while (numAttempts <= this.maxRetries)
        throw lockFailureException
    }
}

Обратите внимание, что аспект реализует интерфейс Ordered, чтобы можно было задать уровень старшинства аспекта выше, чем совета транзакции (нам нужно, чтобы при каждой повторной попытке транзакция была новой). Свойства maxRetries и order настраиваются Spring. Основное действие происходит в совете "вместо" doConcurrentOperation. Обратите внимание, что на данный момент мы применяем логику повторных попыток к каждому businessService(). Пытаемся продолжить выполнение, а в случае ошибки PessimisticLockingFailureException пытаемся снова, если не были исчерпаны все попытки повторения.

Далее следует соответствующая конфигурация Spring:

<aop:aspectj-autoproxy/>
<bean id="concurrentOperationExecutor" class="com.xyz.myapp.service.impl.ConcurrentOperationExecutor">
    <property name="maxRetries" value="3"/>
    <property name="order" value="100"/>
</bean>

Для уточнения аспекта, чтобы он повторял только идемпотентные операции, мы можем определить следующую аннотацию Idempotent:

Java
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
    // marker annotation
}
Kotlin
@Retention(AnnotationRetention.RUNTIME)
annotation class Idempotent// marker annotation

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

Java
@Around("com.xyz.myapp.CommonPointcuts.businessService() && " +
        "@annotation(com.xyz.myapp.service.Idempotent)")
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
    // ...
}
Kotlin
@Around("com.xyz.myapp.CommonPointcuts.businessService() && " +
        "@annotation(com.xyz.myapp.service.Idempotent)")
fun doConcurrentOperation(pjp: ProceedingJoinPoint): Any {
    // ...
}