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主题都被探讨过。