Зв'язування під час завантаження (Load-time weaving/LTW) належить до процесу зв'язування аспектів AspectJ із файлами класів програми під час їх завантаження до віртуальної машини Java (Java virtual machin/ JVM). У цьому розділі основну увагу приділено налаштуванню та використанню механізму LTW у конкретному контексті Spring Framework. Цей розділ не є загальним вступом до механізму зв'язування під час завантаження. Для отримання повної інформації про специфіку механізму зв'язування під час завантаження та його налаштування з використанням тільки AspectJ (при цьому Spring не задіяний взагалі) див.розділ, присвячений зв'язуванню під час завантаження в Посібнику з середовища розробки AspectJ.

Цінність Spring Framework для механізму зв'язування під час завантаження з AspectJ полягає в тому, що дозволяє набагато більш тонко контролювати процес зв'язування. Ванільний механізм зв'язування під час завантаження AspectJ здійснюється за допомогою агента Java (5+), який включається завданням аргументу віртуальної машини при запуску JVM. Таким чином, це параметр для всієї JVM, який може бути хороший в деяких ситуаціях, але часто занадто грубий. Механізм LTW з підтримкою Spring дозволяє включати його на основі кожного ClassLoader, що є більш тонким параметром і може бути куди доцільнішим у середовищі з однією JVM, але безліччю додатків (як, наприклад, у типовому середовищі сервера додатків).

Більш того, у певних середовищах ця підтримка дозволяє виконувати зв'язування під час завантаження без внесення змін до сценарію запуску сервера додатків, необхідних для додавання -javaagent:path/to/aspectjweaver.jar або (як описано далі в цьому розділі) -javaagent:path/to/spring-instrument.jar. Розробники конфігурують контекст програми для включення зв'язування під час завантаження замість покладатися на адміністраторів, які зазвичай відповідають за конфігурацію розгортання, наприклад, скрипти запуску.

Тепер, коли ми закінчили нахвалювати, давайте розглянемо швидкий приклад LTW AspectJ з використанням Spring, а потім докладно зупинимося на елементах, наведених у прикладі.

Перший приклад

Припустимо, що ви є розробником додатків, якому доручено діагностувати причину деяких проблем з продуктивністю системи. Замість використання інструменту профілю ми включимо простий аспект профілю, що дозволяє швидко отримати деякі показники продуктивності. Відразу після цього можна застосувати більш тонкий інструмент профілювання до цієї конкретної області.

У цьому прикладі використовується XML-конфігурація. Ти також можеш налаштувати та використовувати @AspectJ за допомогою Java-конфігурації. Зокрема, можна використовувати анотацію @EnableLoadTimeWeaving як альтернативу <context:load-time-weaver/>.

У наступному прикладі показаний не надто принадний аспект профілювання. Це профілювальник, який контролюється за часом, який використовує @AspectJ-стиль оголошення аспектів:

Java

package foo;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.util.StopWatch;
import org.springframework.core.annotation.Order;
@Aspect
public class ProfilingAspect {
    @Around("methodsToBeProfiled()")
    public Object profile(ProceedingJoinPoint pjp) throws Throwable {
        StopWatch sw = new StopWatch(getClass().getSimpleName());
        try {
            sw.start(pjp.getSignature().getName());
            return pjp.proceed();
        } finally {
            sw.stop();
            System.out.println(sw.prettyPrint());
        }
    }
    @Pointcut("execution(public * foo..*.*(..))")
    public void methodsToBeProfiled(){}
}
Kotlin

package foo
import org.aspectj.lang.ProceedingJoinPoint
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.Around
import org.aspectj.lang.annotation.Pointcut
import org.springframework.util.StopWatch
import org.springframework.core.annotation.Order
@Aspect
class ProfilingAspect {
    @Around("methodsToBeProfiled()")
    fun profile(pjp: ProceedingJoinPoint): Any {
        val sw = StopWatch(javaClass.simpleName)
        try {
            sw.start(pjp.getSignature().getName())
            return pjp.proceed()
        } finally {
            sw.stop()
            println(sw.prettyPrint())
        }
    }
    @Pointcut("execution(public * foo..*.*(..))")
    fun methodsToBeProfiled() {
    }
}

Нам також потрібно створити файл META-INF/aop.xml, щоб повідомити інструмент зв'язування AspectJ, що ми хочемо зв'язати наш ProfilingAspect з нашими класами. Дана угода про файли, а саме наявність файлу (або файлів) на шляху класів Java під назвою META-INF/aop.xml, є стандартом AspectJ. Наступні приклади виконають aop.xml file:


<!DOCTYPE aspectj PUBLIC "-//AspectJ//DTD//EN" "https://www.eclipse.org/aspectj/dtd/aspectj.dtd">
<aspectj>
    <weaver>
        <!-- we only bundle classes in our application-specific packages -->
        <include within="foo.*"/>
    </weaver>
    <aspects>
        <!-- we connect only this aspect... -->
        <aspect name="foo.ProfilingAspect"/>
    </aspects>
</aspectj>

Тепер можна перейти до частини конфігурації, специфічної для Spring. Нам потрібно налаштувати LoadTimeWeaver (пояснення буде пізніше). Цей інструмент зв'язування під час завантаження є основним компонентом, який відповідає за зв'язування конфігурації аспектів в одному або кількох файлах META-INF/aop.xml з класами твоєї програми. На щастя, він не вимагає складного налаштування (є ще кілька опцій, які можна вказати, але вони будуть докладно описані пізніше), як видно з наступного прикладу:


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http:// www.springframework.org/schema/context"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">
    <!-- service object; we will profile its methods -->
    <bean id="entitlementCalculationService"
            class="foo.StubEntitlementCalculationService"/>
    <!-- this enables binding at boot time -->
    <context:load-time-weaver/>
</beans>

Тепер, коли всі необхідні артефакти (аспект, файл META-INF/aop.xml та конфігурація Spring) на місці, ми можемо створити наступний клас драйвера з методом main(..), щоб продемонструвати механізм LTW у дії:

Java

package foo;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public final class Main {
    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml", Main.class);
        EntitlementCalculationService entitlementCalculationService =
                (EntitlementCalculationService) ctx.getBean("entitlementCalculationService");
        // профільований аспект "обплітає" виконання цього методу
        entitlementCalculationService.calculateEntitlement();
    }
}
Kotlin

package foo
import org.springframework.context.support.ClassPathXmlApplicationContext
fun main() {
    val ctx = ClassPathXmlApplicationContext("beans.xml")
    val entitlementCalculationService = ctx.getBean("entitlementCalculationService") as EntitlementCalculationService
    // профільований аспект "оплітає" виконання цього методу
    entitlementCalculationService.calculateEntitlement()
            } 

Нам залишилося зробити лише одне. У вступі до цього розділу говорилося, що за допомогою Spring можна включати механізм LTW вибірково на основі кожного класу завантажувача, і це дійсно так. Однак у цьому прикладі ми використовуємо Java-агент (наданий разом зі Spring) для включення механізму LTW. Ми використовуємо наступну команду для запуску класу Main, показаного раніше:

java-javaagent:C:/projects/foo/lib/global/spring-instrument.jar foo.Main

Прапор -javaagent — це прапор для завдання та активації агентів для інструментування програм, що виконуються на JVM. У складі Spring Framework є такий агент, InstrumentationSavingAgent, упакований у spring-instrument.jar, який був наданий як значення аргументу -javaagent у попередньому прикладі.

Результат виконання програми Main виглядає приблизно так, як показано в наступному прикладі. (Я ввів інструкцію Thread.sleep(..) до реалізації calculateEntitlement(), щоб профілювальник фактично зафіксував якесь значення, крім 0 мілісекунд (01234 мілісекунди не є затримкою, що вводиться АОП). У наступному лістингу показаний результат, який ми отримали під час запуску нашого профілювача:

Calculating entitlement
StopWatch 'ProfilingAspect': running time (millis) = 1234
------ ----- ----------------------------
ms     %     Task name
------ ----- ----------------------------
01234  100%  calculateEntitlement

Оскільки цей механізм LTW здійснюється за допомогою повнофункціонального AspectJ, ми не обмежені лише постачанням порадами бінів Spring. Наступна невелика варіація на тему програми Main дає той самий результат:

Java

package foo;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public final class Main {
    public static void main(String[] args) {
        new ClassPathXmlApplicationContext("beans.xml", Main.class);
        EntitlementCalculationService entitlementCalculationService =
                new StubEntitlementCalculationService();
        // профільований аспект "оплетатиме" виконання цього методу
        entitlementCalculationService.calculateEntitlement();
    }
}
Kotlin

package foo
import org.springframework.context.support.ClassPathXmlApplicationContext
fun main(args: Array<String>) {
    ClassPathXmlApplicationContext("beans.xml")
    val entitlementCalculationService = StubEntitlementCalculationService()
    // профільований аспект "оплетатиме" виконання цього методу
    entitlementCalculationService.calculateEntitlement()
}

Зверни увагу, що в попередній програмі ми завантажуємо контейнер Spring, а потім створюємо новий екземпляр StubEntitlementCalculationService зовсім поза контекстом Spring. Поради, що профільуються, все одно зв'язуються.

Звісно, приклад є спрощеним. Однак, основи підтримки механізму LTW в Spring були представлені в попередньому прикладі, і в іншій частині цього розділу докладно описаний сенс кожного біта конфігурації та застосування.

Аспект ProfilingAspect, який використовується в цьому прикладі, може бути базовим, але він дуже корисний. Це хороший приклад аспекту на час розробки, який розробники можуть використовувати під час розробки, а потім легко виключити зі збірок програми, що розгортається в середовищі приймального користувацького тестування (UAT) або в експлуатаційному середовищі.

Аспекти

Аспекти, які ти використовуєш в LTW, повинні бути аспектами AspectJ. Ти можеш писати їх на мові AspectJ, або писати свої аспекти в стилі @AspectJ. Тоді твої аспекти будуть одночасно допустимими аспектами і AspectJ, і Spring AOP. Крім того, скомпільовані класи аспектів повинні бути доступні в шляху класів.

'META-INF/aop.xml'

Інфраструктура AspectJ LTW конфігурується за допомогою одного або декількох файлів META-INF/aop.xml, які знаходяться в шляху класів Java (або безпосередньо, або, що більш типово, в jar-файлах).

Структура та зміст цього файлу докладно описані в частині довідкової документації AspectJ, присвяченої LTW. Оскільки файл aop.xml на 100% написаний на AspectJ, ми не описуватимемо його тут.

Необхідні бібліотеки (JARS)

Для використання підтримки механізму LTW з AspectJ у Spring Framework тобі знадобляться, як мінімум, наступні бібліотеки:

  • spring-aop.jar

  • aspectjweaver.jar

Якщо ти використовуєш агент для активації інструментування, що надається Spring, тобі також знадобиться:

  • spring-instrument.jar

Конфігурування Spring

Ключовим компонентом підтримки механізму LTW у Spring є інтерфейс LoadTimeWeaver (у пакеті org.springframework.instrument.classloading) та численні його реалізації, що поставляються з дистрибутивом Spring. LoadTimeWeaver відповідає за додавання одного або кількох java.lang.instrument.ClassFileTransformers до ClassLoader під час виконання, що відчиняє двері для всіляких цікавих режимів застосування, одним з яких є зв'язування аспектів під час завантаження.

Якщо ти не знайомий з ідеєю перетворення файлів класів під час виконання, звернися до документації Java API інтерфейс для пакета java.lang.instrument, перш ніж продовжити. Хоча ця документація не є вичерпною, принаймні ти зможеш бачити основні інтерфейси та класи (для довідки при читанні цього розділу).

Конфігурування LoadTimeWeaver для конкретного ApplicationContext може зводитися до додавання одного рядка. (Зверни увагу, що тобі майже напевно потрібно буде використовувати ApplicationContext як контейнер Spring — зазвичай BeanFactory недостатньо, оскільки підтримка механізму LTW використовує BeanFactoryPostProcessors).

Щоб активувати підтримку механізму LTW у Spring Framework, необхідно налаштувати LoadTimeWeaver, що зазвичай робиться за допомогою анотації @EnableLoadTimeWeaving, як показано нижче:

Java

@Configuration
@EnableLoadTimeWeaving
public class AppConfig {
}
Kotlin
@Configuration
@EnableLoadTimeWeaving
class AppConfig {
}

Як варіант, якщо ти надаєш перевагу конфігурації на основі XML, використовуй елемент <context:load-time-weaver/>. Зверни увагу, що елемент визначено у просторі імен context. У цьому прикладі показано, як використовувати <context:load-time-weaver/>:


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">
    <context:load-time-weaver/>
</beans>

Попередня конфігурація автоматично визначає та реєструє для тебе низку інфраструктурних бінів, специфічних для LTW, таких як LoadTimeWeaver та AspectJWeavingEnabler. За замовчуванням LoadTimeWeaver — це клас DefaultContextLoadTimeWeaver, який намагається доповнити автоматично виявлений LoadTimeWeaver. Точний тип LoadTimeWeaver, який буде "визначений автоматично", залежить від твого середовища виконання. У наступній таблиці наведено різні реалізації LoadTimeWeaver:

Таблиця 13. DefaultContextLoadTimeWeaver LoadTimeWeaver
Середовище виконання РеалізаціяLoadTimeWeaver

Виконання Apache Tomcat

TomcatLoadTimeWeaver

Виконання GlassFish (обмежене розгортанням EAR)

GlassFishLoadTimeWeaver

Виконання JBoss AS від Red Hat або WildFly

JBossLoadTimeWeaver

Виконання WebSphere від IBM

WebSphereLoadTimeWeaver

Виконання WebLogic від Oracle

WebLogicLoadTimeWeaver

JVM запущена з InstrumentationSavingAgent(java -javaagent:path/to/spring-instrument.jar) зі Spring

InstrumentationLoadTimeWeaver

Повернення, яке очікує, що базовим ClassLoader буде дотримано загальні угоди (а саме addTransformer і, опціонально, метод getThrowawayClassLoader).

ReflectiveLoadTimeWeaver

Зверни увагу, що в таблиці перераховані лише ті LoadTimeWeaver, які автоматично визначаються під час використання DefaultContextLoadTimeWeaver. Ти можеш точно встановити, яку реалізацію LoadTimeWeaver використовувати.

Щоб вказати конкретний LoadTimeWeaver за допомогою Java-конфігурації, реалізуй інтерфейс LoadTimeWeavingConfigurer та перевизнач метод getLoadTimeWeaver(). У наступному прикладі вказано ReflectiveLoadTimeWeaver:

Java

@Configuration
@EnableLoadTimeWeaving
public class AppConfig implements LoadTimeWeavingConfigurer {
    @Override
    public LoadTimeWeaver getLoadTimeWeaver() {
        return new ReflectiveLoadTimeWeaver();
    }
}
Kotlin

@Configuration
@EnableLoadTimeWeaving
class AppConfig : LoadTimeWeavingConfigurer {
    override fun getLoadTimeWeaver(): LoadTimeWeaver {
        return ReflectiveLoadTimeWeaver()
    }
}

Якщо ти використовуєш конфігурацію на основі XML, то можеш вказати повне ім'я класу як значення атрибуту weaver-class в елементі <context:load-time-weaver/>. Знову ж таки, у наступному прикладі вказано ReflectiveLoadTimeWeaver:


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">
    <context:load-time-weaver
            weaver-class="org.springframework.instrument.classloading.ReflectiveLoadTimeWeaver"/>
</beans>

LoadTimeWeaver, який визначено та зареєстровано конфігурацією, може бути пізніше отриманий з контейнера Spring за допомогою загальновідомого імені loadTimeWeaver. Пам'ятай, що LoadTimeWeaver існує лише як механізм для інфраструктури LTW у Spring, який дозволяє додати один або кілька ClassFileTransformers. Фактичний ClassFileTransformer, який виконує LTW, — це клас ClassPreProcessorAgentAdapter (з пакета org.aspectj.weaver.loadtime). Більш детальну інформацію можна знайти в javadoc класу ClassPreProcessorAgentAdapter, оскільки специфіка того, як відбувається зв'язування, виходить за межі цього документа.

Залишилося розглянути останній атрибут конфігурації: атрибут aspectjWeaving (або aspectj-weaving, якщо ти використовуєш XML). Цей атрибут контролює, чи активовано механізм LTW чи ні. Він приймає одне з трьох можливих значень, при цьому значенням за замовчуванням є autodetect, якщо атрибут відсутній. У наступній таблиці наведено три можливі значення:

Таблиця 14. Значення атрибута зв'язування AspectJ
Значення анотації Значення XML Пояснення

ENABLED

увімкнено

Зв'язування AspectJ увімкнено, а зв'язування аспектів відбувається під час завантаження в міру необхідності.

DISABLED

відключено

Зв'язування під час завантаження вимкнено. Зв'язування аспектів під час завантаження не відбувається.

AUTODETECT

автоматичне виявлення

Якщо інфраструктурі LTW у Spring вдасться знайти хоча б один файл META-INF/aop.xml, то зв'язування AspectJ буде запущено. В іншому випадку зв'язування буде вимкнено. Це значення за замовчуванням.

Конфігурування для конкретного середовища

Цей останній розділ містить усі додаткові параметри та конфігурації, необхідні під час використання підтримки LTW з Spring у таких середовищах, як сервери програм та вебконтейнери.

Tomcat, JBoss, WebSphere, WebLogic

Tomcat, JBoss/WildFly, IBM WebSphere Application Server та Oracle WebLogic Server — всі вони надають загальний ClassLoader для програм, здатних здійснювати локальне інструментування. Рідний LTW із Spring може використовувати ці реалізації ClassLoader для забезпечення зв'язування з AspectJ. Ти можеш просто активувати зв'язування під час завантаження. Зокрема, не потрібно змінювати сценарій запуску JVM, щоб додати -javaagent:path/to/spring-instrument.jar.

Зверни увагу, що у випадку з JBoss може бути потрібно відключити сканування сервера програм, щоб він не завантажував класи до того, як програма фактично запуститься. Швидким обхідним рішенням є додавання до твого артефакту файлу з ім'ям WEB-INF/jboss-scanning.xml з таким вмістом:

<scanning xmlns="urn:jboss:scanning:1.0"/>

Універсальне використання Java

Якщо потрібно провести інструментування класів у середовищах, які не підтримуються конкретними реалізаціями LoadTimeWeaver, загальним рішенням є агент JVM. Для таких випадків у Spring є InstrumentationLoadTimeWeaver, який вимагає наявності специфічного для Spring (але вкрай універсального) агента JVM, spring-instrument.jar, що автоматично визначається загальними налаштуваннями @ EnableLoadTimeWeaving та <context:load-time-weaver/>.

Щоб використовувати його, тобі потрібно запустити віртуальну машину з агентом зі Spring, вказавши наступні параметри для JVM:

-javaagent:/path/to/spring-instrument .jar

Зверни увагу, що для цього потрібно змінити скрипт запуску JVM, що може завадити використовувати його в середовищах сервера програм (залежно від твого сервера та політик експлуатації). Тим не менш, у сценаріях розгортання додатків, таких як автономні програми Spring Boot, за схемою "одна програма на одну JVM" тобі зазвичай доводиться контролювати налаштування JVM цілком у будь-якому випадку.

Додаткові ресурси

Детальнішу інформацію про AspectJ можна знайти на вебсайті AspectJ.

Eclipse AspectJ за авторством Адріана Кольє та ін. (Addison-Wesley, 2005) містить вичерпний вступ та посібник з мови AspectJ.

Рекомендуємо друге видання AspectJ в дії за авторством Рамніваса Ладдада (Manning, 2009). Основну увагу в книзі приділено AspectJ, але багато загальних тем АОП теж досліджено досить глибоко.