5.10.4. Spring框架中带AspectJ的加载时织入
加载时织入(LTW)指的是在将AspectJ切面加载到Java虚拟机(JVM)中的过程中,将AspectJ切面编织成应用程序的类文件。本节的重点是在Spring框架的特定上下文中配置和使用LTW。本节不是LTW的一般介绍。有关LTW的详细信息和仅使用AspectJ配置LTW(根本不涉及Spring),请参阅AspectJ开发环境指南的LTW部分。
Spring框架为AspectJ LTW带来的价值在于能够对织入过程进行更细粒度的控制。”AspectJ LTW是通过使用Java(5 +)代理来实现的,它在启动JVM时通过指定VM参数来打开。因此,它是一个JVM范围的设置,在某些情况下可能很好,但通常有点过于粗糙。启用了Spring的LTW允许您在每个类加载器的基础上打开LTW,它更细粒度,并且在“单个JVM多应用程序”环境中(例如在典型的应用程序服务器环境中)更有意义。
此外,在某些环境中,此支持启用加载时编织,而不需要对应用程序服务器的启动脚本进行任何修改,这些脚本需要添加 -javaagent:path/to/aspectjweaver.jar
或(如本节后面所述)·-javaagent:path/to/spring-instrument.jar·。开发人员配置应用程序上下文以启用加载时织入,而不是依赖通常负责部署配置(如启动脚本)的管理员。
让我们先浏览一下使用Spring的AspectJ LTW的一个快速示例,然后详细介绍示例中介绍的元素。有关完整的示例,请参阅Petclinic 示例应用程序。
第一个例子
假设您是一个应用程序开发人员,负责诊断系统中某些性能问题的原因。我们将打开一个简单的分析切面,让我们快速获得一些性能指标,而不是开发一个分析工具。我们可以在之后对特定区域应用更细粒度的分析工具。
这里的示例使用XML配置。还可以使用Java配置来配置和使用@ AspectJ。具体来说,您可以使用@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(){}
}
我们还需要创建一个META-INF/aop.xml文件,通知AspectJ weaver我们想要将ProfilingAspect织入到我们的类中。这个文件约定,即在Java类路径上的文件的存在,称为META-INF/aop.xml是标准AspectJ文件。下面的示例显示aop.xml文件:
<!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 (稍后解释)。这个加载时的weaver是负责将一个或多个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();
}
}
我们还有最后一件事要做。前面说过,可以使用Spring在每个类加载器的基础上选择性地打开LTW,这是正确的。但是,对于这个例子,我们使用Java代理(用Spring提供)来切换LTW。我们使用以下命令来运行前面显示的主类:
java -javaagent:C:/projects/foo/lib/global/spring-instrument.jar foo.Main
-javaagent是一个标志,用于指定和使代理能够检测在JVM上运行的程序。Spring框架附带这样一个代理,即InstrumentationSavingAgent,它打包在Spring-instrument.jar中,在前面的示例中作为-javaagent参数的值提供。
主程序执行时的输出类似于下一个示例。(我在calculateEntitlement()实现中引入了一个Thread.sleep(..)语句,以便探查器实际捕获0毫秒以外的内容(01234毫秒不是AOP引入的开销)。下面的列表显示了运行分析器时得到的输出:
Calculating entitlement
StopWatch 'ProfilingAspect': running time (millis) = 1234
------ ----- ----------------------------
ms % Task name
------ ----- ----------------------------
01234 100% calculateEntitlement
由于此LTW是通过使用AspectJ来实现的,因此我们不仅限于通知Spring Beans。主程序的以下细微变化产生相同的结果:
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();
}
}
注意,在前面的程序中,我们引导Spring容器,然后完全在Spring上下文之外创建StubEntitlementCalculationService的新实例。分析通知仍然被编织在一起。
诚然,这个例子是简单的。但是,在前面的示例中已经介绍了在Spring中LTW支持的基础知识,本节的其余部分详细解释了每一配置和使用背后的“为什么”。
本例中使用的配置方面可能是基本的,但非常有用。这是一个很好的开发时间切面的例子,开发人员可以在开发期间使用它,然后很容易地将其从AT或生产中中排除。
切面
您在LTW中使用的切面必须是AspectJ切面。您可以用AspectJ语言本身编写它们,也可以用@AspectJ样式编写切面。您的切面是有效的AspectJ和Spring AOP切面。此外,编译的切面类需要在类路径上可用。
'META-INF/aop.xml'
AspectJ LTW基础结构通过使用Java类路径上的一个或多个META-INF/aop.xml
文件来配置(直接或更典型地,在JAR文件中)。
此文件的结构和内容在AspectJ参考文档的LTW部分中有详细说明。因为aop.xml文件是100%的AspectJ,所以我们在这里不再详细描述它。
所需库(JAR)
至少,您需要以下库来使用Spring框架对AspectJ LTW的支持:
- spring-aop.jar
- aspectjweaver.jar
如果使用Spring提供的代理来启用检测,还需要:
spring-instrument.jar
Spring配置
Spring LTW支持中的关键组件是loadTimeweaver接口(在org.springframework.instrument.classloading包中),以及与Spring发行版一起提供的许多实现。LoadTimeWeaver负责在运行时向类加载器添加一个或多个java.lang.instrument.ClassFileTransformers,这为各种有趣的应用程序打开了大门,其中一个应用程序恰好是方面的LTW。
如果您不熟悉运行时类文件转换的概念,请参阅java.lang.instrument包的javadoc API文档,然后继续。虽然该文档并不全面,但至少您可以看到关键的接口和类(供您阅读本节时参考)。
为特定的应用程序上下文配置LoadTimeWeaver与添加一行一样简单。(请注意,您几乎肯定需要使用ApplicationContext作为Spring容器-通常,BeanFactory是不够的,因为LTW支持使用BeanFactoryPostProcessors。)
要启用Spring框架的LTW支持,您需要配置一个loadTimeweaver,通常通过使用@EnableLoadTimeWeaving注释完成,如下所示:
@Configuration
@EnableLoadTimeWeaving
public class AppConfig {
}
或者,如果您更喜欢基于XML的配置,请使用<context:load-time-weaver/>
元素。请注意,元素是在上下文命名空间中定义的。下面的示例演示如何使用<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特定的基础结构bean,例如LoadTimeWeaver和AspectJWeavingEnabler。默认的LoadTimeWeaver是LoadTimeWeaver类,它尝试修饰自动检测到的LoadTimeWeaver。“自动检测”的LoadTimeWeaver的确切类型取决于运行时环境。下表总结了各种LoadTimeWeaver实现:
表13. DefaultContextLoadTimeWeaver LoadTimeWeavers
运行环境 | LoadTimeWeaver实例 |
---|---|
Apache Tomcat | TomcatLoadTimeWeaver |
GlassFish(仅限于EAR部署) | GlassFishLoadTimeWeaver |
JBoss AS或WildFly | JBossLoadTimeWeaver |
WebSphere | WebSphereLoadTimeWeaver |
WebLogic | WebLogicLoadTimeWeaver |
使用Spring的InstrumentationSavingAgent(java -javaagent:path/to/spring-instrument.jar) | InstrumentationLoadTimeWeaver |
回退,期望底层类加载器遵循公共约定(即addTransformer和可选的getThrowawayClassLoader加载器方法) | ReflectiveLoadTimeWeaver |
注意,该表仅列出使用DefaultContextLoadTimeWeaver时自动检测的LoadTimeWeaver。您可以准确地指定要使用的LoadTimeWeaver实现。
若要指定具有Java配置的特定LoadTimeWeaver,请实现 LoadTimeWeavingConfigurer 接口并重写getLoadTimeWeaver() 方法。以下示例指定了ReflectiveLoadTimeWeaver:
@Configuration
@EnableLoadTimeWeaving
public class AppConfig implements LoadTimeWeavingConfigurer {
@Override
public LoadTimeWeaver getLoadTimeWeaver() {
return new ReflectiveLoadTimeWeaver();
}
}
如果使用基于XML的配置,则可以将完全限定的类名指定为<context:load-time-weaver/>
元素上weaver-class 属性的值。同样,下面的示例指定了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稍后可以使用名称loadTimeWeaver从Spring容器中检索。记住, LoadTimeWeaver仅作为Spring的LTW基础结构添加一个或多个ClassFileTransformers的机制存在。执行LTW的实际ClassFileTransformer是ClassPreprocessorAgentAdapter(来自org.aspectj.weaver.loadtime包)类。有关进一步的详细信息,请参见ClassPreprocessorAgentAdapter类的类级JavaDoc,因为细节超出了本文档的范围。
还有一个最后要讨论的配置属性:aspectjWeaving 属性(如果使用XML,则为aspectj-weaving
属性)。此属性控制是否启用LTW。它接受三个可能值中的一个,如果属性不存在,则默认值为自动检测。下表总结了三个可能的值:
表14.AspectJ的weaving属性值
注解值 | XML值 | 说明 |
---|---|---|
ENABLED | on | Aspectj编织已启用,并且各个切面在加载时视情况进行编织。 |
DISABLED | of | LTW关闭。任何切面在加载时不织入 |
AUTODETECT | autodetect | 如果SpringLTW基础结构可以找到至少一个META-INF/aop.xml文件,那么 AspectJ weaving就打开了。否则,它将关闭。这是默认值。 |
环境特定配置
最后一部分包含在应用程序服务器和Web容器等环境中使用Spring的LTW支持时所需的任何其他设置和配置。
Tomcat, JBoss, WebSphere, WebLogic
Tomcat、JBoss/WildFly、IBM WebSphere和Oracle WebLogic 都提供了一个通用的应用程序类加载器,能够进行本地检测。Spring的本地LTW可以利用这些类加载器实现来提供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/>
设置自动检测。
要使用它,必须通过提供以下JVM选项来使用Spring代理启动虚拟机:
-javaagent:/path/to/spring-instrument.jar
请注意,这需要修改JVM启动脚本,这可能会阻止您在应用程序服务器环境中使用该脚本(取决于您的服务器和操作策略)。也就是说,对于每个JVM部署一个应用程序(如独立的Spring引导应用程序),您通常在任何情况下都控制整个JVM设置。
5.11.进一步的资源
关于AspectJ的更多信息可以在AspectJ网站上找到。
Adrian Colyer等人的Eclipse Aspectj(Addison-Wesley,2005)为AspectJ语言提供了全面的介绍和参考。
Aspectj in Action,第二版由Ramnivas Laddad(Manning,2009)强烈推荐。这本书的重点是Aspectj,但很多一般的AOP主题都被探讨过。