Связывание во время загрузки (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. The following example shows the aop.xml file:

<!DOCTYPE aspectj PUBLIC "-//AspectJ//DTD//EN" "https://www.eclipse.org/aspectj/dtd/aspectj.dtd">
<aspectj>
    <weaver>
        <!-- связываем только классы в наших пакетах, специфичных для конкретного приложения -->
        <include within="foo.*"/>
    </weaver>
    <aspects>
        <!-- связываем только этот аспект... -->
        <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">
    <!-- объект-служба; мы будем профилировать его методы -->
    <bean id="entitlementCalculationService"
            class="foo.StubEntitlementCalculationService"/>
    <!-- это включает связывание во время загрузки -->
    <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 во время выполнения, что открывает двери для всевозможных интересных режимов применения, одним из которых является связывание аспектов во время загрузки.

Если вы не знакомы с идеей преобразования файлов классов во время выполнения, обратитесь к документации javadoc по 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, но многие общие темы АОП тоже исследованы (достаточно глубоко).