Spring AOP использует либо динамические прокси JDK, либо CGLIB для создания прокси для заданного целевого объекта. Динамические прокси JDK встроены в JDK, в то время как CGLIB – это обычная библиотека определения классов с открытым исходным кодом (перепакованная в spring-core).

Если проксируемый целевой объект реализует хотя бы один интерфейс, используется динамический прокси JDK. Все интерфейсы, реализуемые целевым типом, проксируются. Если целевой объект не реализует никаких интерфейсов, создается CGLIB-прокси.

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

  • В случае CGLIB final методы нельзя снабдить советами, поскольку они не могут быть переопределены в подклассах, создаваемых во время выполнения.

  • Начиная со Spring 4.0, конструктор проксируемого объекта больше НЕ вызывается дважды, поскольку экземпляр CGLIB-прокси создается через Objenesis. Если ваша JVM не позволяет обходить конструктор, то можно заметить двойные вызовы и соответствующие записи в журнале отладки от средств поддержки АОП в Spring.

Чтобы принудительно использовать CGLIB-прокси, установите значение атрибута proxy-target-class элемента <aop:config> в true, как показано ниже:

<aop:config proxy-target-class="true">
    <!-- другие бины, определенные здесь... -->
</aop:config>

Чтобы в принудительном порядке осуществить CGLIB-проксирование при использовании поддержки авто-прокси в @AspectJ, установите атрибут proxy-target-class элемента <aop:aspectj-autoproxy> в true, как показано ниже:

<aop:aspectj-autoproxy proxy-target-class="true"/>

Несколько разделов <aop:config/> сворачиваются в одно единое средство создания (creator) авто-прокси во время выполнения программы, который применяет самые строгие настройки прокси, указанные в любом из разделов <aop:config/> (обычно из разных файлов определения бинов XML). Это также относится к элементам <tx:annotation-driven/> и <aop:aspectj-autoproxy/>.

Для большего понимания, использование proxy-target-class="true" в элементах <tx:annotation-driven/>, <aop:aspectj-autoproxy/> или <aop:config/> принудительно используется CGLIB-прокси для всех трех элементов.

Основные сведения об АОП-прокси

Spring AOP основан на прокси. Крайне важно, чтобы вы поняли семантику того, что означает последняя инструкция, прежде чем писать свои собственные аспекты или использовать какие-либо аспекты на основе Spring AOP, поставляемые со Spring Framework.

Рассмотрим сначала сценарий, в котором у вас есть обыкновенная, типовая, непроксированная, ничем особенным не выделяющаяся, прямая ссылка на объект, как показано в следующем фрагменте кода:

Java
public class SimplePojo implements Pojo {
    public void foo() {
        // следующий вызов метода является прямым вызовом ссылки "this"
        this.bar();
    }
    public void bar() {
        // немного логики...
    }
}
Kotlin
class SimplePojo : Pojo {
    fun foo() {
        // следующий вызов метода является прямым вызовом ссылки "this"
        this.bar()
    }
    fun bar() {
        // немного логики...
    }
}

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

Java
public class Main {
    public static void main(String[] args) {
        Pojo pojo = new SimplePojo();
        // это прямой вызов метода по ссылке "pojo"
        pojo.foo();
    }
}
Kotlin
fun main() {
    val pojo = SimplePojo()
    // это прямой вызов метода по ссылке "pojo"
    pojo.foo()
}

Ситуация немного меняется, если ссылка, которая содержится в клиентском коде, является прокси. Рассмотрим следующую схему и фрагмент кода:

Java
public class Main {
    public static void main(String[] args) {
        ProxyFactory factory = new ProxyFactory(new SimplePojo());
        factory.addInterface(Pojo.class);
        factory.addAdvice(new RetryAdvice());
        Pojo pojo = (Pojo) factory.getProxy();
        // это вызов метода для прокси!
        pojo.foo();
    }
}
Kotlin
fun main() {
    val factory = ProxyFactory(SimplePojo())
    factory.addInterface(Pojo::class.java)
    factory.addAdvice(RetryAdvice())
    val pojo = factory.proxy as Pojo
    // это вызов метода для прокси!
    pojo.foo()
}

Здесь самое важно понять, что клиентский код внутри метода main(..) класса Main содержит ссылку на прокси. Это означает, что вызовы методов по этой ссылке на объект являются вызовами прокси. В результате прокси может делегировать все перехватчики (советы), которые имеют отношение к данному конкретному вызову метода. Однако, как только вызов достигнет целевого объекта (в данном случае ссылки SimplePojo), все вызовы методов, которые он может осуществить для себя, такие как this.bar() или this.foo(), будут вызваны для this ссылки, а не для прокси. Это имеет важные последствия. Это означает, что самовызов не приведет к тому, что совет, связанный с вызовом метода, сможет выполняться.

Ладно, но что же с этим делать? Лучший подход (термин "лучший" здесь используется условно) – это рефакторинг вашего кода таким образом, чтобы самовызов не происходил. Это требует некоторых усилий с вашей стороны, но это лучший, наименее агрессивный подход. Следующий подход абсолютно ужасен, и мы не уверены, что стоит заострять на нем внимание, именно потому, что он столь ужасен. Вы можете (как бы это болезненно для нас ни выглядело) полностью привязать логику внутри вашего класса к Spring AOP, как показано в следующем примере:

Java
public class SimplePojo implements Pojo {
    public void foo() {
        // это работает, но... фу!
        ((Pojo) AopContext.currentProxy()).bar();
    }
    public void bar() {
        // немного логики...
    }
}
Kotlin
class SimplePojo : Pojo {
    fun foo() {
        // это работает, но... фу!
        (AopContext.currentProxy() as Pojo).bar()
    }
    fun bar() {
        // немного логики...
    }
}

Этот подход полностью привяжет ваш код к Spring AOP, а сам класс будет знать о том, что он используется в контексте АОП, что идет вразрез с АОП. Также он потребует некоторой дополнительной настройки при создании прокси, как показано в следующем примере:

Java
public class Main {
    public static void main(String[] args) {
        ProxyFactory factory = new ProxyFactory(new SimplePojo());
        factory.addInterface(Pojo.class);
        factory.addAdvice(new RetryAdvice());
        factory.setExposeProxy(true);
        Pojo pojo = (Pojo) factory.getProxy();
        // это вызов метода для прокси!
        pojo.foo();
    }
}
Kotlin
fun main() {
    val factory = ProxyFactory(SimplePojo())
    factory.addInterface(Pojo::class.java)
    factory.addAdvice(RetryAdvice())
    factory.isExposeProxy = true
    val pojo = factory.proxy as Pojo
    // это вызов метода для прокси!
    pojo.foo()
}

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

Программное создание прокси @AspectJ

В дополнение к объявлению аспектов в конфигурации с помощью <aop:config> или <aop:aspectj-autoproxy>, можно также программно создавать прокси, которые снабжают советами целевые объекты. В этой части мы хотим сосредоточиться на возможности автоматического создания прокси с помощью аспектов @AspectJ.

Вы можете использовать класс org.springframework.aop.aspectj.annotation.AspectJProxyFactory для создания прокси для целевого объекта, который был снабжен советом одним или несколькими аспектами @AspectJ. Базовое использование этого класса выглядит крайне просто, как показано в следующем примере:

Java
// создаем фабрику, которая может генерировать прокси для заданного целевого объекта
AspectJProxyFactory factory = new AspectJProxyFactory(targetObject);
// добавляем аспект, класс должен быть аспектом @AspectJ
// можно вызывать его столько раз, сколько нужно, с различными аспектами
factory.addAspect(SecurityManager.class);
// также можно добавить существующие экземпляры аспектов, тип предоставленного объекта должен быть аспектом @AspectJ
factory.addAspect(usageTracker);
// теперь получаем объект прокси...
MyInterfaceType proxy = factory.getProxy();
Kotlin
// создаем фабрику, которая может генерировать прокси для заданного целевого объекта
val factory = AspectJProxyFactory(targetObject)
// добавляем аспект, класс должен быть аспектом @AspectJ
// можно вызывать его столько раз, сколько нужно, с различными аспектами
factory.addAspect(SecurityManager::class.java)
// также можно добавить существующие экземпляры аспектов, тип предоставленного объекта должен быть аспектом @AspectJ
factory.addAspect(usageTracker)
// теперь получаем объект прокси...
val proxy = factory.getProxy<Any>()