Связывание во время загрузки (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, а затем подробно остановимся на элементах, представленных в примере.
Первый пример
Предположим, что вы являетесь разработчиком приложений, которому поручено диагностировать причину некоторых проблем с производительностью системы. Вместо использования инструмента профилирования, мы включим простой аспект профилирования, позволяющий быстро получить некоторые показатели производительности. Сразу после этого можно применить более тонкий инструмент профилирования к этой конкретной области.
@EnableLoadTimeWeaving
в качестве альтернативы <context:load-time-weaver/>
.
В следующем примере показан не слишком вычурный аспект профилирования. Это профилировщик, контролируемый по времени, который использует @AspectJ-стиль объявления аспектов:
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(){}
}
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 в действии:
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();
}
}
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
дает тот же результат:
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();
}
}
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.lang.instrument
, прежде чем продолжить. Хотя эта документация не является исчерпывающей, по крайней мере, вы сможете видеть основные интерфейсы и классы (для справки при чтении этого раздела).Конфигурирование LoadTimeWeaver
для конкретного ApplicationContext
может сводиться к добавлению одной строки. (Обратите внимание, что вам почти наверняка нужно будет использовать ApplicationContext
в качестве контейнера Spring - обычно BeanFactory
недостаточно, поскольку поддержка механизма LTW использует BeanFactoryPostProcessors
).
Чтобы активировать поддержку механизма LTW в Spring Framework, необходимо сконфигурировать LoadTimeWeaver
, что обычно делается с помощью аннотации @EnableLoadTimeWeaving
, как показано ниже:
@Configuration
@EnableLoadTimeWeaving
public class AppConfig {
}
@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
:
Среда выполнения | Реализация LoadTimeWeaver |
---|---|
Выполнение в Apache Tomcat |
|
Выполнение в GlassFish (ограничено развертыванием EAR) |
|
|
|
Выполнение в WebSphere от IBM |
|
Выполнение в WebLogic от Oracle |
|
JVM запущена с |
|
Возврат, ожидающий, что базовым ClassLoader будут соблюдены общие соглашения (а именно |
|
Обратите внимание, что в таблице перечислены только те LoadTimeWeaver
, которые автоматически определяются при использовании DefaultContextLoadTimeWeaver
. Вы можете точно задать, какую реализацию LoadTimeWeaver
использовать.
Чтобы задать конкретный LoadTimeWeaver
с помощью Java-конфигурации, реализуйте интерфейс LoadTimeWeavingConfigurer
и переопределите метод getLoadTimeWeaver()
. В следующем примере задан ReflectiveLoadTimeWeaver
:
@Configuration
@EnableLoadTimeWeaving
public class AppConfig implements LoadTimeWeavingConfigurer {
@Override
public LoadTimeWeaver getLoadTimeWeaver() {
return new ReflectiveLoadTimeWeaver();
}
}
@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
, если атрибут отсутствует. В следующей таблице приведены три возможных значения:
Значение аннотации | Значение XML | Пояснение |
---|---|---|
|
|
Связывание AspectJ включено, а связывание аспектов происходит во время загрузки по мере необходимости. |
|
|
Связывание во время загрузки отключено. Связывание аспектов во время загрузки не происходит. |
|
|
Если инфраструктуре LTW в Spring удастся найти хотя бы один файл |
Конфигурирование для конкретной среды
Данный последний раздел содержит все дополнительные параметры и конфигурации, необходимые при использовании поддержки 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, но многие общие темы АОП тоже исследованы (достаточно глубоко).
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ