Если вы используете IoC-контейнер Spring (ApplicationContext или BeanFactory) для своих бизнес-объектов (а вам следует это делать!), ваша задача - использовать одну из реализаций FactoryBean из Spring AOP. (Помните, что бин-фабрика вводит уровень косвенной адресации, что позволяет создавать объекты другого типа).

Вспомогательные средства Spring AOP также используют сокрытые фабричные бины.

Основной способ создания прокси АОП в Spring – это использование org.springframework.aop.framework.ProxyFactoryBean. Он дает полный контроль над срезами, любыми советами, которые применяются, и их упорядочиванием. Однако есть и более простые варианты, которые являются более предпочтительными, если вам не требуется такой контроль.

Основы

ProxyFactoryBean, как и другие реализации FactoryBean из Spring, вводит уровень косвенности. Если вы определяете ProxyFactoryBean с именем foo, объекты, ссылающиеся на foo, видят не сам экземпляр ProxyFactoryBean, а объект, созданный реализацией метода getObject() в ProxyFactoryBean. Данный метод создает прокси АОП, который обертывает целевой объект.

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

Свойства JavaBean

Как и большинство реализаций FactoryBean, предоставляемых Spring, класс ProxyFactoryBean сам является классом JavaBean. Его свойства используются для:

  • Задания цели, которую нужно проксировать.

  • Задания необходимости использовать CGLIB (описано далее, см. также раздел "прокси на основе JDK и CGLIB").

Некоторые ключевые свойства наследуются от org.springframework.aop.framework.ProxyConfig (суперкласс для всех фабрик прокси АОП в Spring). Эти ключевые свойства состоят из:

  • proxyTargetClass: является true, если будет проксирован целевой класс, а не интерфейсы целевого класса. Если значение этого свойства установлено в true, то создаются CGLIB-прокси.

  • optimize: Управляет тем, применяются ли агрессивные способы оптимизации к прокси, созданным с помощью CGLIB. Не стоит легкомысленно использовать этот параметр, если нет понимания, как соответствующий прокси АОП управляет оптимизацией. В настоящее время используется только для CGLIB-прокси. Параметр не влияет на динамические JDK-прокси.

  • frozen: Если конфигурация прокси имеет свойство frozen, то изменения в ней больше не допускаются. Это можно использовать как для легкой оптимизации, так и в тех случаях, если не требуется, чтобы вызывающие подпрограммы имели возможность манипулировать прокси (через интерфейс Advised) после его создания. По умолчанию значение этого свойства равно false, поэтому изменения (например, добавление дополнительных советов) разрешены.

  • exposeProxy: Определяет, должен ли текущий прокси быть открыт в ThreadLocal, чтобы к нему могла получить доступ цель. Если цели необходимо получить прокси, а свойство exposeProxy установлено в true, цель может использовать метод AopContext.currentProxy().

Другими свойствами, специфичными для ProxyFactoryBean, являются:

  • proxyInterfaces: Массив String имен интерфейсов. Если данное свойство не указано, то используется CGLIB-прокси для целевого класса.

  • interceptorNames: String массив имен Advisor, перехватчиков или других советов для применения. Упорядочивание играет важную роль, все происходит в порядке очерёдности. Таким образом, первый перехватчик в списке первым сможет перехватить вызов.

    Имена – это имена бинов в текущей фабрике, включая имена бинов из родительских фабрик. В данном случае нельзя упоминать ссылки на бины, поскольку это приведет к тому, что ProxyFactoryBean будет игнорировать параметр объекта-одиночки в совете.

    Можно дополнить имя перехватчика звездочкой (*). Это приведет к тому, что будут применены все бины-советники с именами, начинающимися с части перед звездочкой.

  • singleton: Должна ли фабрика возвращать одиночный объект, независимо от того, как часто вызывается метод getObject(). Несколько реализаций FactoryBean обеспечивают такой метод. Значение по умолчанию - true. Если нужно использовать совет, сохраняющий состояние, – например, для примесей, сохраняющих состояние, - используйте советы-прототипы вместе со значением свойства singleton, равным false.

Прокси на основе JDK и CGLIB

Данный раздел служит в качестве предписывающей документации касательно того, каким образом ProxyFactoryBean выбирает между созданием прокси на основе JDK и на основе CGLIB для определенного целевого объекта (который должен быть проксирован).

Логика работы ProxyFactoryBean в отношении создания прокси на основе JDK или CGLIB изменилась между версиями 1.2.x и 2.0 Spring. ProxyFactoryBean теперь имеет ту же семантику в отношении автоматического определения интерфейсов, что и класс TransactionProxyFactoryBean.

Если класс целевого объекта, который должен быть проксирован (далее просто называется целевым классом), не реализует никаких интерфейсов, создается прокси на основе CGLIB. Это самый простой сценарий, поскольку JDK-прокси основаны на интерфейсах, а отсутствие интерфейсов означает, что JDK-проксирование вообще невозможно. Можно подключить целевой бин и указать список перехватчиков, установив свойство interceptorNames. Обратите внимание, что прокси на основе CGLIB создается, даже если свойство proxyTargetClass в ProxyFactoryBean было установлено в false. (Делать это бессмысленно и лучше убрать это свойство из определения бина, потому что оно, в лучшем случае, избыточно, а в худшем - сбивает с толку).

Если целевой класс реализует один (или несколько) интерфейсов, то тип создаваемого прокси зависит от конфигурации ProxyFactoryBean.

Если свойство proxyTargetClass в ProxyFactoryBean было установлено в true, то создается прокси на основе CGLIB. Это логично и соответствует принципу наименьшего удивления. Даже если свойство proxyInterfaces в ProxyFactoryBean было установлено для одного или нескольких полностью уточненных имен интерфейсов, тот факт, что свойство proxyTargetClass установлено в true, приводит к осуществлению проксирования на основе CGLIB.

Если свойство proxyInterfaces в ProxyFactoryBean было установлено для одного или нескольких полностью уточненных имен интерфейсов, создается прокси на основе JDK. Созданный прокси реализует все интерфейсы, которые были заданы в свойстве proxyInterfaces. Если целевой класс реализует гораздо больше интерфейсов, чем задано в свойстве proxyInterfaces, это замечательно, но эти дополнительные интерфейсы не будут реализованы возвращаемым прокси.

Если свойство proxyInterfaces в ProxyFactoryBean не было задано, но целевой класс реализует один (или более) интерфейсов, ProxyFactoryBean автоматически определяет тот факт, что целевой класс действительно реализует по крайней мере один интерфейс, и создается прокси на основе JDK. Интерфейсы, которые фактически проксируются, – это все интерфейсы, которые реализует целевой класс. По сути, это то же самое, что указать список всех и каждого интерфейса, который реализует целевой класс, для свойства proxyInterfaces. Тем не менее, так значительно меньше трудозатрат и меньше вероятность опечаток.

Проксирование интерфейсов

Рассмотрим простой пример ProxyFactoryBean в действии. Этот пример включает в себя:

  • Целевой проксируемый бин. Это определение бина personTarget в примере.

  • Advisor и Interceptor, используемые для снабжения советом.

  • Определение прокси-бина АОП для указания целевого объекта (бин personTarget), интерфейсов для прокси и советов, которые будут применены.

В следующем листинге показан пример:

<bean id="personTarget" class="com.mycompany.PersonImpl">
    <property name="name" value="Tony"/>
    <property name="age" value="51"/>
</bean>
<bean id="myAdvisor" class="com.mycompany.MyAdvisor">
    <property name="someProperty" value="Custom string property value"/>
</bean>
<bean id="debugInterceptor" class="org.springframework.aop.interceptor.DebugInterceptor">
</bean>
<bean id="person"
    class="org.springframework.aop.framework.ProxyFactoryBean">
    <property name="proxyInterfaces" value="com.mycompany.Person"/>
    <property name="target" ref="personTarget"/>
    <property name="interceptorNames">
        <list>
            <value>myAdvisor</value>
            <value>debugInterceptor</value>
        </list>
    </property>
</bean>

Обратите внимание, что свойство interceptorNames принимает список String, который содержит имена бинов перехватчиков или советников в текущей фабрике. Можно использовать советники, перехватчики, объекты с советами "перед", "после возврата" и "генерация исключения". Упорядочивание советников имеет большое значение.

Вы можете удивиться, почему в списке нет ссылок на бины. Причина заключается в том, что если свойство объекта-одиночки в ProxyFactoryBean установлено в false, он должен иметь возможность возвращать независимые экземпляры прокси. Если какой-либо из советников сам является прототипом, то потребуется возвращать независимый экземпляр, поэтому необходимо иметь возможность получить экземпляр прототипа из фабрики. Одной ссылки недостаточно.

Определение бина person, показанное ранее, может быть использовано вместо реализации Person следующим образом:

Java
Person person = (Person) factory.getBean("person");
Kotlin
val person = factory.getBean("person") as Person;

Другие бины в том же IoC-контексте могут выражать сильно типизированную зависимость от него, как в случае с обычным объектом Java. В следующем примере показано, как это сделать:

<bean id="personUser" class="com.mycompany.PersonUser">
    <property name="person"><ref bean="person"/></property>
</bean>

Класс PersonUser в этом примере открывает свойство типа Person. Что касается прокси АОП, то его можно использовать прозрачным образом вместо реализации "настоящего" типа person. Однако его класс будет динамическим. Можно было бы привести его к интерфейсу Advised (о нем речь пойдет позже).

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

<bean id="myAdvisor" class="com.mycompany.MyAdvisor">
    <property name="someProperty" value="Custom string property value"/>
</bean>
<bean id="debugInterceptor" class="org.springframework.aop.interceptor.DebugInterceptor"/>
<bean id="person" class="org.springframework.aop.framework.ProxyFactoryBean">
    <property name="proxyInterfaces" value="com.mycompany.Person"/>
    <!-- Используйте внутренний бин, а не локальную ссылку на цель -->
    <property name="target">
        <bean class="com.mycompany.PersonImpl">
            <property name="name" value="Tony"/>
            <property name="age" value="51"/>
        </bean>
    </property>
    <property name="interceptorNames">
        <list>
            <value>myAdvisor</value>
            <value>debugInterceptor</value>
        </list>
    </property>
</bean>

Преимуществом использования анонимного внутреннего бина является то, что существовать будет только один объект типа Person. Это целесообразно, если мы хотим предотвратить получение пользователями контекста приложения ссылки на объект, который не снабжен советом, или если нужно избежать двусмысленности при автоматическом обнаружении и связывании при IoC в Spring. Также, вероятно, преимущество заключается еще в том, что определение ProxyFactoryBean является самодостаточным. Тем не менее, бывают случаи, когда возможность получить из фабрики цель, которая не была снабжена советом, может оказаться преимуществом (например, в определенных сценариях тестирования).

Проксирование классов

Что если нужно проксировать класс, а не один или несколько интерфейсов?

Представьте, что в нашем предыдущем примере не было интерфейса Person. Нам нужно было снабдить советом класс под названием Person, который не реализовывал никакого бизнес-интерфейса. В этом случае можно настроить Spring на использование CGLIB-проксирования, а не динамических прокси. Для этого установите свойство proxyTargetClass для ProxyFactoryBean, продемонстрированного ранее, в true. Хотя лучше всего программировать на интерфейсах, а не на классах, возможность снабжать советом классы, не реализующие интерфейсы, может быть полезна при работе с устаревшим кодом. (В целом, Spring не является прескриптивным. Хотя это и облегчает применение передового опыта, так можно избежать принуждения к определенному подходу).

Если хотите, то можете принудительно использовать CGLIB в любом случае, даже если у вас есть интерфейсы.

CGLIB-проксирование работает путем генерации подкласса целевого класса во время выполнения программы. Spring конфигурирует этот сгенерированный подкласс таким образом, чтобы он делегировал вызовов методов исходной цели. Подкласс используется для реализации шаблона "Декоратор (Decorator)", связывая с ним советы.

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

  • Final методы нельзя снабдить советом, так как их нельзя переопределить.

  • Нет необходимости добавлять CGLIB в ваш путь классов. Начиная с версии Spring 3.2, CGLIB перепакован и включен в JAR модуля ядра spring-core. Другими словами, АОП на основе CGLIB работает "из коробки", как и динамические JDK-прокси.

Разница в производительности между CGLIB-проксированием и динамическим проксированием незначительна. Производительность не должна быть решающим фактором в этом случае.

Использование "глобальных" советников

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

<bean id="proxy" class="org.springframework.aop.framework.ProxyFactoryBean">
    <property name="target" ref="service"/>
    <property name="interceptorNames">
        <list>
            <value>global*</value>
        </list>
    </property>
</bean>
<bean id="global_debug" class="org.springframework.aop.interceptor.DebugInterceptor"/>
<bean id="global_performance" class="org.springframework.aop.interceptor.PerformanceMonitorInterceptor"/>