5.10。在Spring应用程序中使用AspectJ
到目前为止,我们在本章中讨论的所有内容都是纯粹的Spring AOP。在本节中,我们将介绍如果您的需求超出了Spring AOP单独提供的功能,那么如何使用AspectJ编译器或weaver来代替Spring AOP,或者在Spring AOP之外使用它。
Spring附带了一个小的AspectJ切面库,它在您的发行版中作为Spring-aspect .jar独立提供。您需要将其添加到类路径中,以便在其中使用切面。使用AspectJ向Spring和AspectJ的其他Spring切面依赖注入域对象将讨论这个库的内容以及如何使用它。通过使用Spring IoC配置AspectJ切面讨论了如何依赖注入使用AspectJ编译器编织的AspectJ切面。最后,在Spring框架中使用AspectJ进行加载时编织提供了对load-time 的介绍.
5.10.1。使用AspectJ用Spring依赖注入域对象
Spring容器实例化和配置在应用程序上下文中定义的bean。如果给定包含要应用的配置的bean定义的名称,也可以要求bean工厂配置已存在的对象。jar包含一个注释驱动的切面,它利用这个功能来允许任何对象的依赖项注入。该支持用于在任何容器控制之外创建的对象。领域对象通常属于这一类,因为它们通常是使用new操作符或由ORM工具作为数据库查询的结果以编程方式创建的。
@Configure注释将一个类标记为符合spring驱动配置的条件。在最简单的情况下,您可以纯粹地使用它作为标记注释,如下面的示例所示:
package com.xyz.myapp.domain;import org.springframework.beans.factory.annotation.Configurable;@Configurable
public class Account {// ...
}
当以这种方式作为标记接口使用时,Spring通过使用与完全限定类型名(com.xyz.myapp.domain.Account)同名的bean定义(通常是原型作用域)来配置注释类型(在本例中为Account)的新实例。因为bean的默认名称是其类型的全限定名,所以声明原型定义的一种方便的方法是省略id属性,如下面的示例所示:
<bean class="com.xyz.myapp.domain.Account" scope="prototype"><property name="fundsTransferService" ref="fundsTransferService"/>
</bean>
如果您想显式地指定要使用的原型bean定义的名称,您可以在注释中直接这样做,如下面的例子所示:
package com.xyz.myapp.domain;import org.springframework.beans.factory.annotation.Configurable;@Configurable("account")
public class Account {// ...
}
Spring现在寻找一个名为account的bean定义,并使用它作为定义来配置新的帐户实例。
您还可以使用自动装配来避免指定一个专门的bean定义。要让Spring应用自动装配,请使用@Configurable 注释的自动装配属性。您可以指定@Configurable(autowire=Autowire.BY_TYPE)或@Configurable(autowire=Autowire.BY_NAME)分别用于按类型或按名称自动装配。作为一种替代方法,最好在字段或方法级别通过@Autowired或@Inject为@Configurable bean指定显式的注解驱动的依赖注入(请参阅基于注解的容器配置以了解更多细节)。
最后,通过使用dependencyCheck属性(例如,@Configurable(autowire=Autowire.BY_NAME,dependencyCheck=true)),您可以为新创建和配置的对象中的对象引用启用Spring依赖项检查。如果该属性设置为true,则Spring在配置后验证所有属性(不是原语或集合)是否已经设置。
注意,单独使用注释不会产生任何效果。spring-aspects.jar 中的AnnotationBeanConfigurerAspect在注释出现时起作用。从本质上说,切面是这样说的:“在初始化一个带有@Configurable注释的类型的新对象之后,使用Spring根据注释的属性配置新创建的对象”。在这个上下文中,“初始化”指的是新实例化的对象(例如,用new操作符实例化的对象)以及正在进行反序列化的可序列化对象(对于exa)。
上面段落中的一个关键短语是“本质上”。对于大多数情况,“从初始化一个新对象返回后”的确切语义是可以接受的。在这个上下文中,“初始化之后”意味着依赖项在对象构造完成之后被注入。这意味着依赖项不能在类的构造函数体中使用。如果你想在构造函数体执行之前注入依赖,从而在构造函数体中可用,你需要在@Configurable 声明中定义,如下:
@Configurable(preConstruction = true)
在AspectJ编程指南的这个附录中,您可以在AspectJ中找到关于各种切入点类型的语言语义的更多信息。
要使其工作,注释类型必须与AspectJ编织器一起编织。您可以使用构建时Ant或Maven任务来完成这项工作(例如,请参阅AspectJ开发环境指南)或加载时编织(请参阅Spring框架中使用AspectJ进行的加载时编织)。AnnotationBeanConfigurerAspect本身需要由Spring进行配置(以便获得将用于配置新对象的bean工厂的引用)。如果您使用基于java的配置,您可以添加@EnableSpringConfigured到任何@Configuration类,如下所示:
@Configuration
@EnableSpringConfigured
public class AppConfig {
}
如果您更喜欢基于XML的配置,Spring上下文名称空间定义了一个方便的上下文:Spring配置元素,您可以如下使用:
<context:spring-configured/>
在配置方面之前创建的@Configurable 对象的实例会导致向调试日志发出一条消息,并且不会进行对象的配置。例如,Spring配置中的bean在被Spring初始化时创建域对象。在这种情况下,可以使用depends-on bean属性手动指定bean依赖于配置方面。下面的例子展示了如何使用依赖属性:
<bean id="myService"class="com.xzy.myapp.service.MyService"depends-on="org.springframework.beans.factory.aspectj.AnnotationBeanConfigurerAspect"><!-- ... --></bean>
不要通过bean配置器方面激活@Configurable处理,除非您真的想在运行时依赖于它的语义。特别地,确保不要在容器中注册为常规Spring bean的bean类上使用@Configurable。这样做会导致两次初始化,一次通过容器,一次通过切面。
单元测试@Configurable对象
@Configurable 支持的目标之一是启用域对象的独立单元测试,而不存在与硬编码查找相关的困难。如果AspectJ没有编织@ Configurable 类型,那么在单元测试期间注释不会产生影响。您可以在测试对象中设置模拟或存根属性引用,然后正常进行。如果AspectJ编织了@Configurable 类型,您仍然可以在容器之外正常地进行单元测试,但是每次构造@Configurable 对象时,您都会看到一条警告消息,指示它没有被Spring配置。
使用多个应用程序上下文
用于实现@Configurable 支持的AnnotationBeanConfigurerAspect是一个AspectJ单例切面。单例切面的作用域与静态成员的作用域相同:每个类加载器都有一个切面实例来定义类型。这意味着,如果在同一个类加载器层次结构中定义多个应用程序上下文,则需要考虑在何处定义@EnableSpringConfigured bean,以及在何处将spring-aspect .jar放在类路径上。
考虑一个典型的Spring web应用程序配置,它有一个共享的父应用程序上下文,该上下文定义了公共业务服务、支持这些服务所需的一切,以及每个servlet的一个子应用程序上下文(其中包含特定于该servlet的定义)。所有这些上下文都共存于同一个类加载器层次结构中,因此AnnotationBeanConfigurerAspect只能保存对其中一个上下文的引用。在这种情况下,我们建议在共享(父)应用程序上下文中定义@EnableSpringConfigured bean。这定义了您可能想要注入到域对象中的服务。结果是,您不能使用@可配置机制(这可能不是您想做的事情)来配置域对象,这些域对象引用在子(特定于servlet的)上下文中定义的bean。
当在同一个容器中部署多个web应用程序时,确保每个web应用程序都使用自己的类加载器来加载spring-aspect.jar中的类型(例如,通过在web - inf /lib中放置spring-aspect.jar)。如果只将spring-aspect .jar添加到容器范围的类路径(因此由共享的父类加载器加载),那么所有web应用程序都共享相同的aspect实例(这可能不是您想要的)。
5.10.2。AspectJ的其他Spring方面
除了@Configurable方面之外,Spring - AspectJ .jar还包含一个AspectJ切面,您可以使用它来驱动Spring的事务管理,以处理带有@Transactional注释的类型和方法。这主要是为希望在Spring容器之外使用Spring框架事务支持的用户准备的。
解释@Transactional注释的切面是AnnotationTransactionAspect。当您使用这个切面时,您必须注释实现类(或该类中的方法或两者),而不是类实现的接口(如果有的话)。AspectJ遵循Java的规则,接口上的注释不被继承。
类上的@Transactional注释指定了执行类中任何公共操作的默认事务语义。
类中方法上的@Transactional注释覆盖类注释(如果存在)给出的默认事务语义。任何可见性的方法都可以被注释,包括私有方法。直接注释非公共方法是获得此类方法执行的事务界定的唯一方法。
自Spring Framework 4.2以来,Spring -aspects提供了一个类似的方面,它为标准javax.transaction提供了完全相同的特性。事务注释。查看JtaAnnotationTransactionAspect以获得更多细节。
对于想要使用Spring配置和事务管理支持但不想(或不能)使用注释的AspectJ程序员,Spring -aspect .jar还包含可以扩展以提供自己的切入点定义的抽象方面。有关更多信息,请参阅AbstractBeanConfigurerAspect和AbstractTransactionAspect方面的源代码。作为一个例子,下面的摘录展示了如何编写一个方面来配置域模型中定义的对象的所有实例,方法是使用原型bean定义来匹配完全限定的类名:
public aspect DomainObjectConfiguration extends AbstractBeanConfigurerAspect {public DomainObjectConfiguration() {setBeanWiringInfoResolver(new ClassNameBeanWiringInfoResolver());}// the creation of a new bean (any object in the domain model)protected pointcut beanCreation(Object beanInstance) :initialization(new(..)) &&SystemArchitecture.inDomainModel() &&this(beanInstance);
}
5.10.3。通过使用Spring IoC配置AspectJ方面
当在Spring应用程序中使用AspectJ切面时,很自然会希望并期望能够用Spring配置这样的方面。AspectJ运行时本身负责方面的创建,通过Spring配置AspectJ创建的方面的方法取决于方面使用的AspectJ实例化模型(per-xxx子句)。
大多数AspectJ方面都是单例方面。这些切面的配置很容易。您可以创建一个作为常规引用方面类型的bean定义,并包含factory-method="aspectOf"
bean属性。这确保了Spring通过向AspectJ询问方面实例而不是尝试自己创建实例来获得方面实例。下面的例子展示了如何使用factory-method="aspectOf"
属性:
<bean id="profiler" class="com.xyz.profiler.Profiler"factory-method="aspectOf"> <property name="profilingStrategy" ref="jamonProfilingStrategy"/>
</bean>
注意factory-method="aspectOf"
属性
非单例方面更难配置。但是,可以通过创建原型bean定义并在方面实例由AspectJ运行时创建bean之后使用spring-aspect.jar中的@Configurable支持来实现这一点。
如果你有@ AspectJ切面你想编织与AspectJ域模型(例如,使用装入时编织类型)和其他您想要使用Spring AOP @ AspectJ切面,而这些切面都是配置在spring,你需要告诉Spring AOP @ AspectJ auto-proxying @ AspectJ方面的支持,它精确子集定义的配置应该用于auto-proxying。你可以在<aop:aspectj-autoproxy/>
声明中使用一个或多个<include/>
元素。每个<include/>
元素指定一个名称模式,只有名称与至少一个模式匹配的bean才用于Spring AOP自动代理配置。下面的例子展示了如何使用<include/>
元素:
<aop:aspectj-autoproxy><aop:include name="thisBean"/><aop:include name="thatBean"/>
</aop:aspectj-autoproxy>
不要被<aop:aspectj-autoproxy/>
元素的名称所误导。使用它会导致Spring AOP代理的创建。这里使用了@AspectJ风格的方面声明,但是没有涉及AspectJ运行时。
5.10.4。在Spring框架中使用AspectJ进行加载时编织
加载时编织(LTW)是指在将AspectJ方面装入Java虚拟机(JVM)时将它们编织到应用程序的类文件中的过程。本节的重点是在Spring框架的特定上下文中配置和使用LTW。本节不是LTW的一般介绍。有关LTW的详细信息以及仅使用AspectJ(完全不涉及Spring)配置LTW的详细信息,请参阅AspectJ开发环境指南的LTW部分。
Spring框架给AspectJ LTW带来的价值在于支持对编织过程进行更细粒度的控制。‘香草’AspectJ LTW是通过使用Java(5+)代理实现的,它是通过在启动JVM时指定VM参数来启用的。因此,它是一个jvm范围的设置,在某些情况下可能很好,但通常有点太粗糙了。启用spring的LTW允许您在每个类加载器的基础上打开LTW,这是更细粒度的,在“单jvm -多应用程序”环境中更有意义(比如在典型的应用程序服务器环境中).
而且,在某些环境中,这种支持支持加载时编织,而不需要对应用服务器的启动脚本进行任何修改,这些脚本需要添加-javaagent:path/to/ aspectjwever .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编织器,我们希望将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><!-- only weave classes in our application-specific packages --><include within="foo.*"/></weaver><aspects><!-- weave in just this aspect --><aspect name="foo.ProfilingAspect"/></aspects></aspectj>
现在我们可以继续讨论配置中特定于spring的部分。我们需要配置LoadTimeWeaver(稍后解释)。这个加载时编织器是负责将一个或多个元- 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/beanshttps://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/contexthttps://www.springframework.org/schema/context/spring-context.xsd"><!-- a service object; we will be profiling its methods --><bean id="entitlementCalculationService"class="foo.StubEntitlementCalculationService"/><!-- this switches on the load-time weaving --><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");// the profiling aspect is 'woven' around this method executionentitlementCalculationService.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 entitlementStopWatch 'ProfilingAspect': running time (millis) = 1234
------ ----- ----------------------------
ms % Task name
------ ----- ----------------------------
01234 100% calculateEntitlement
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();// the profiling aspect will be 'woven' around this method executionentitlementCalculationService.calculateEntitlement();}
}
注意,在前面的程序中,我们引导Spring容器,然后在完全脱离Spring上下文的情况下创建StubEntitlementCalculationService的新实例。分析建议仍然会被纳入其中。
诚然,这个例子过于简单。但是,在前面的示例中已经介绍了Spring中LTW支持的基础知识,本节的其余部分将详细解释每个配置和使用背后的“原因”。
本例中使用的ProfilingAspect可能是基本的,但它非常有用。它是开发时间方面的一个很好的例子,开发人员可以在开发期间使用它,然后很容易地从部署到UAT或生产中的应用程序的构建中排除它。
切面 Aspects
在LTW中使用的方面必须是AspectJ方面。您可以用AspectJ语言本身编写它们,也可以用@AspectJ风格编写方面。这样,您的方面就是有效的AspectJ和Spring AOP方面。此外,编译后的方面类需要在类路径中可用。
META-INF/aop.xml
AspectJ LTW基础设施是通过使用一个或多个元- inf /aop.xml文件来配置的,这些文件位于Java类路径上(直接或更典型地位于jar文件中)。
这个文件的结构和内容在AspectJ参考文档的LTW部分中有详细说明。因为aop.xml文件是100% AspectJ的,所以这里不再进一步描述它。
所需的库(jar)
至少,您需要以下库来使用Spring框架对AspectJ LTW的支持:
spring-aop.jar
aspectjweaver.jar
If you use the Spring-provided agent to enable instrumentation, you also need:
spring-instrument.jar
spring 配置
Spring的LTW支持中的关键组件是LoadTimeWeaver接口(在org.springframework.instrument.classloading 包中),以及Spring发行版附带的众多类加载包实现。LoadTimeWeaver负责添加一个或多个java.lang.instrument.classfiletransformer在运行时转换为ClassLoader,这为各种有趣的应用程序打开了大门,其中之一就是aspect的LTW。
如果您不熟悉运行时类文件转换的概念,请参阅java.lang的javadoc API文档。继续之前的java.lang.instrument包。虽然文档并不全面,但至少您可以看到关键的接口和类(在阅读本节时可以参考)。
为特定的ApplicationContext配置LoadTimeWeaver非常简单,只需添加一行代码即可。(注意,您几乎肯定需要使用ApplicationContext作为Spring容器—通常,一个BeanFactory是不够的,因为LTW支持使用beanfactorypostprocessor。)
要启用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/beanshttps://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/contexthttps://www.springframework.org/schema/context/spring-context.xsd"><context:load-time-weaver/></beans>
前面的配置为您自动定义和注册了许多特定于lpar的基础设施bean,例如LoadTimeWeaver和AspectJWeavingEnabler。默认的LoadTimeWeaver是DefaultContextLoadTimeWeaver类,它试图修饰一个自动检测到的LoadTimeWeaver。被“自动检测”的LoadTimeWeaver的确切类型取决于您的运行时环境。下表总结了各种LoadTimeWeaver实现: