Якщо ти використовуєш 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"/>
     <!-- Use an internal bean rather than a local reference to the target -->
    <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"/>