Appearance
Spring 核心:AOP - Part2
Tip:基于 Spring Core 5.3.30 版本。
1. 基于 Schema 的 AOP 支持
如果你更喜欢基于 XML 的格式,Spring 也提供了使用 aop 命名空间标签来定义切面的支持。当使用 @AspectJ 风格时,支持完全相同的切点表达式和增强类型。因此,在本节中,我们将重点讨论该语法,并参考前一节(@AspectJ 支持)中的讨论,以理解编写切点表达式和绑定增强参数。
要使用本节中描述的 aop 命名空间标签,你需要导入 spring-aop Schema,如基于 XML Schema 的配置中所述。请参阅 AOP Schema,了解如何导入 aop 命名空间中的标签。
在你的 Spring 配置中,所有的切面和增强器元素必须放在 <aop:config> 元素内(在一个应用程序上下文配置中可以有多个 <aop:config> 元素)。一个 <aop:config> 元素可以包含切点、增强器和切面元素(注意,这些必须按该顺序声明)。
Warning:
<aop:config>配置风格大量使用了 Spring 的自动代理机制。如果你已经通过使用BeanNameAutoProxyCreator或类似的东西明确地使用了自动代理,这可能会导致一些问题(如增强没有被织入)。推荐的使用模式是只使用<aop:config>风格或者只使用AutoProxyCreator风格,而且永远不要混合使用它们。
1.1. 声明切面
当你使用 Schema 支持时,切面是一个在你的 Spring 应用程序上下文中定义为 Bean 的常规 Java 对象。状态和行为被捕获在对象的字段和方法中,而切点和增强信息则被捕获在 XML 中。
你可以使用 <aop:aspect> 元素来声明一个切面,并使用 ref 属性来引用支持 Bean,如下面的示例所示:
XML
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
...
</aop:aspect>
</aop:config>
<bean id="aBean" class="...">
...
</bean>1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
支持切面的 Bean(在这种情况下是 aBean)当然可以像任何其他 Spring Bean 一样被配置和依赖注入。
1.2. 声明切点
你可以在 <aop:config> 元素内声明一个命名的切点,让切点定义在多个切面和增强器之间共享。
可以如下定义代表在服务层执行任何业务服务的切点:
XML
<aop:config>
<aop:pointcut id="businessService"
expression="execution(* com.xyz.myapp.service.*.*(..))"/>
</aop:config>1
2
3
4
5
6
2
3
4
5
6
请注意,切点表达式本身使用的是与 @AspectJ 支持中描述的相同的 AspectJ 切点表达式语言。如果你使用基于 Schema 的声明风格,你可以在切点表达式中引用在类型(@Aspects)中定义的命名切点。定义上述切点的另一种方式如下:
XML
<aop:config>
<aop:pointcut id="businessService"
expression="com.xyz.myapp.CommonPointcuts.businessService()"/>
</aop:config>1
2
3
4
5
6
2
3
4
5
6
假设你有一个如在共享常见的切点定义中描述的 CommonPointcuts 切面。
然后,在切面内声明一个切点与声明一个顶级切点非常相似,如下面的示例所示:
XML
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
<aop:pointcut id="businessService"
expression="execution(* com.xyz.myapp.service.*.*(..))"/>
...
</aop:aspect>
</aop:config>1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
与 @AspectJ 切面非常相似,使用基于 Schema 的定义风格声明的切点可以收集连接点上下文。例如,以下切点收集 this 对象作为连接点上下文,并将其传递给增强:
XML
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
<aop:pointcut id="businessService"
expression="execution(* com.xyz.myapp.service.*.*(..)) && this(service)"/>
<aop:before pointcut-ref="businessService" method="monitor"/>
...
</aop:aspect>
</aop:config>1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
必须通过包括匹配名称的参数来声明该增强,以接收收集的连接点上下文,如下所示:
Java
public void monitor(Object service) {
// ...
}1
2
3
2
3
在组合切点子表达式时,&& 在 XML 文档中使用起来很别扭,所以你可以使用 and、or 和 not 关键字来分别替代 &&、|| 和 !。例如,前面的切点可以更好地写成如下形式:
XML
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
<aop:pointcut id="businessService"
expression="execution(* com.xyz.myapp.service.*.*(..)) and this(service)"/>
<aop:before pointcut-ref="businessService" method="monitor"/>
...
</aop:aspect>
</aop:config>1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
请注意,以这种方式定义的切点是通过它们的 XML id 来引用的,不能用作命名切点来形成复合切点。因此,基于 Schema 的定义样式中的命名切点支持比 @AspectJ 样式提供的更有限。
1.3. 声明增强
基于 Schema 的 AOP 支持使用与 @AspectJ 风格相同的五种增强,它们的语义完全相同。
1.3.1. 前置增强
前置增强在匹配的方法执行之前运行。它是在 <aop:aspect> 内部通过使用 <aop:before> 元素声明的,如下面的示例所示:
XML
<aop:aspect id="beforeExample" ref="aBean">
<aop:before
pointcut-ref="dataAccessOperation"
method="doAccessCheck"/>
...
</aop:aspect>1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
在这里,dataAccessOperation 是在顶层(<aop:config>)定义的切点的 id。如果要内联定义切点,可以将 pointcut-ref 属性替换为 pointcut 属性,如下所示:
XML
<aop:aspect id="beforeExample" ref="aBean">
<aop:before
pointcut="execution(* com.xyz.myapp.dao.*.*(..))"
method="doAccessCheck"/>
...
</aop:aspect>1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
正如我们在讨论 @AspectJ 风格时所提到的,使用命名切点可以显著提高你的代码的可读性。
method 属性标识了一个方法(doAccessCheck),该方法提供了增强的主体。这个方法必须为包含增强的切面元素引用的 Bean 定义。在执行数据访问操作之前(由切点表达式匹配的方法执行连接点),切面 Bean 上的 doAccessCheck 方法会被调用。
1.3.2. 返回后增强
返回后增强在匹配的方法执行正常完成时运行。它是在 <aop:aspect> 内部声明的,与前置增强的方式相同。下面的示例展示了如何声明它:
XML
<aop:aspect id="afterReturningExample" ref="aBean">
<aop:after-returning
pointcut-ref="dataAccessOperation"
method="doAccessCheck"/>
...
</aop:aspect>1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
与 @AspectJ 风格一样,你可以在增强主体内获取返回值。为了做到这一点,可以使用 returning 属性来指定应将返回值传递给哪个参数,如下面的示例所示:
XML
<aop:aspect id="afterReturningExample" ref="aBean">
<aop:after-returning
pointcut-ref="dataAccessOperation"
returning="retVal"
method="doAccessCheck"/>
...
</aop:aspect>1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
doAccessCheck 方法必须声明一个名为 retVal 的参数。这个参数的类型以与 @AfterReturning 描述的相同的方式约束匹配。例如,你可以按照以下方式声明方法签名:
Java
public void doAccessCheck(Object retVal) { ... }1.3.3. 抛出异常后增强
抛出异常后增强在匹配的方法执行通过抛出异常退出时运行。它是在 <aop:aspect> 内部通过使用 after-throwing 元素声明的,如下面的示例所示:
XML
<aop:aspect id="afterThrowingExample" ref="aBean">
<aop:after-throwing
pointcut-ref="dataAccessOperation"
method="doRecoveryActions"/>
...
</aop:aspect>1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
与 @AspectJ 风格一样,你可以在增强主体内获取抛出的异常。为了做到这一点,可以使用 throwing 属性来指定应将异常传递给哪个参数,如下面的示例所示:
XML
<aop:aspect id="afterThrowingExample" ref="aBean">
<aop:after-throwing
pointcut-ref="dataAccessOperation"
throwing="dataAccessEx"
method="doRecoveryActions"/>
...
</aop:aspect>1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
doRecoveryActions 方法必须声明一个名为 dataAccessEx 的参数。这个参数的类型以与 @AfterThrowing 描述的相同的方式约束匹配。例如,你可以按照以下方式声明方法签名:
Java
public void doRecoveryActions(DataAccessException dataAccessEx) { ... }1.3.4. 最终增强
无论匹配的方法执行如何退出,最终增强都会运行。你可以使用 after 元素来声明它,如下面的示例所示:
XML
<aop:aspect id="afterFinallyExample" ref="aBean">
<aop:after
pointcut-ref="dataAccessOperation"
method="doReleaseLock"/>
...
</aop:aspect>1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
1.3.5. 环绕增强
最后一种增强是环绕增强。环绕增强 “环绕” 着匹配的方法的执行运行。它有机会在方法运行之前和之后进行工作,并决定方法何时、如何,甚至是否真的运行。如果你需要以线程安全的方式在方法执行之前和之后共享状态,那么通常会使用环绕增强 —— 例如,启动和停止计时器。
Note
始终使用满足你需求的最弱的增强形式。
例如,如果前置增强足以满足你的需求,那么就不要使用环绕增强。
你可以使用 aop:around 元素来声明环绕增强。增强方法应声明 Object 为其返回类型,方法的第一个参数必须是 ProceedingJoinPoint 类型。在增强方法的主体内,你必须在 ProceedingJoinPoint 上调用 proceed(),以便底层方法运行。不带参数地调用 proceed() 将导致在调用底层方法时提供调用者的原始参数。对于高级用例,proceed() 方法有一个接受参数数组(Object[])的重载变体。数组中的值将被用作调用底层方法时的参数。请参阅环绕增强,了解使用 Object[] 调用 proceed() 的注意事项。
以下示例展示了如何在 XML 中声明环绕增强:
XML
<aop:aspect id="aroundExample" ref="aBean">
<aop:around
pointcut-ref="businessService"
method="doBasicProfiling"/>
...
</aop:aspect>1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
doBasicProfiling 增强的实现可以与 @AspectJ 示例中的完全相同(当然,减去了注解),如下面的示例所示:
Java
public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
// start stopwatch
Object retVal = pjp.proceed();
// stop stopwatch
return retVal;
}1
2
3
4
5
6
2
3
4
5
6
1.3.6. 增强参数
基于 Schema 的声明样式支持完全类型化的增强,与 @AspectJ 支持的方式相同 —— 通过将切点参数的名称与增强方法参数进行匹配。详见增强参数部分。如果你希望为增强方法显式指定参数名称(不依赖之前描述的检测策略),你可以通过使用增强元素的 arg-names 属性来实现,这与增强注解中的 argNames 属性的处理方式相同(如在确定参数名称中所述)。以下示例展示了如何在 XML 中指定参数名称:
XML
<aop:before
pointcut="com.xyz.lib.Pointcuts.anyPublicMethod() and @annotation(auditable)"
method="audit"
arg-names="auditable"/>1
2
3
4
2
3
4
arg-names 属性接受一个由逗号分隔的参数名称列表。
以下稍微复杂一些的基于 XSD 的方法示例展示了一些与多个强类型参数一起使用的环绕增强:
Java
package x.y.service;
public interface PersonService {
Person getPerson(String personName, int age);
}
public class DefaultPersonService implements PersonService {
public Person getPerson(String name, int age) {
return new Person(name, age);
}
}1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
接下来是切面。注意到 profile(..) 方法接受了一些强类型的参数,其中第一个恰好是用于继续方法调用的连接点。这个参数的存在表明 profile(..) 将被用作环绕增强,如下面的示例所示:
Java
package x.y;
import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.util.StopWatch;
public class SimpleProfiler {
public Object profile(ProceedingJoinPoint call, String name, int age) throws Throwable {
StopWatch clock = new StopWatch("Profiling for '" + name + "' and '" + age + "'");
try {
clock.start(call.toShortString());
return call.proceed();
} finally {
clock.stop();
System.out.println(clock.prettyPrint());
}
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
最后,以下示例 XML 配置对特定连接点的前述增强的执行产生了影响:
XML
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- this is the object that will be proxied by Spring's AOP infrastructure -->
<bean id="personService" class="x.y.service.DefaultPersonService"/>
<!-- this is the actual advice itself -->
<bean id="profiler" class="x.y.SimpleProfiler"/>
<aop:config>
<aop:aspect ref="profiler">
<aop:pointcut id="theExecutionOfSomePersonServiceMethod"
expression="execution(* x.y.service.PersonService.getPerson(String,int))
and args(name, age)"/>
<aop:around pointcut-ref="theExecutionOfSomePersonServiceMethod"
method="profile"/>
</aop:aspect>
</aop:config>
</beans>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
请考虑以下驱动脚本:
Java
import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import x.y.service.PersonService;
public final class Boot {
public static void main(final String[] args) throws Exception {
BeanFactory ctx = new ClassPathXmlApplicationContext("x/y/plain.xml");
PersonService person = (PersonService) ctx.getBean("personService");
person.getPerson("Pengo", 12);
}
}1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
如果有这样一个 Boot 类,我们在标准输出上会得到类似于以下的输出:
Text
StopWatch 'Profiling for 'Pengo' and '12': running time (millis) = 0
-----------------------------------------
ms % Task name
-----------------------------------------
00000 ? execution(getFoo)1
2
3
4
5
2
3
4
5
1.3.7. 增强排序
当多个增强需要在同一连接点(执行方法)运行时,排序规则如增强排序中所述。切面之间的优先级是通过 <aop:aspect> 元素中的 order 属性确定的,或者通过向支持切面的 Bean 添加 @Order 注解,或者让 Bean 实现 Ordered 接口来确定的。
Tip
与在同一个
@Aspect类中定义的增强方法的优先级规则相比,当在同一个<aop:aspect>元素中定义的两个增强都需要在同一个连接点运行时,优先级由增强元素在封闭的<aop:aspect>元素中声明的顺序决定,从最高优先级到最低优先级。例如,给定一个环绕增强和一个在同一个
<aop:aspect>元素中定义的适用于同一个连接点的前置增强,为了确保环绕增强比前置增强具有更高的优先级,必须在<aop:before>元素之前声明<aop:around>元素。作为一个经验法则,如果你发现在同一个
<aop:aspect>元素中定义了多个适用于同一个连接点的增强,考虑将这些增强方法合并为每个<aop:aspect>元素中每个连接点的一个增强方法,或者将增强重构为可以在切面级别排序的单独的<aop:aspect>元素。
1.4. 引入(Introduction)
引入(Introduction,在 AspectJ 中被称为 inter-type 声明)允许一个切面声明被增强的对象实现了给定的接口,并代表这些对象提供该接口的实现。
你可以通过在 aop:aspect 内部使用 aop:declare-parents 元素来进行引入。你可以使用 aop:declare-parents 元素来声明匹配的类型有一个新的父类(因此得名)。例如,给定一个名为 UsageTracked 的接口和该接口的一个名为 DefaultUsageTracked 的实现,以下切面声明所有服务接口的实现者也实现了 UsageTracked 接口。(例如,为了通过 JMX 公开统计信息。)
XML
<aop:aspect id="usageTrackerAspect" ref="usageTracking">
<aop:declare-parents
types-matching="com.xzy.myapp.service.*+"
implement-interface="com.xyz.myapp.service.tracking.UsageTracked"
default-impl="com.xyz.myapp.service.tracking.DefaultUsageTracked"/>
<aop:before
pointcut="com.xyz.myapp.CommonPointcuts.businessService()
and this(usageTracked)"
method="recordUsage"/>
</aop:aspect>1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
支持 usageTracking Bean 的类将包含以下方法:
Java
public void recordUsage(UsageTracked usageTracked) {
usageTracked.incrementUseCount();
}1
2
3
2
3
要实现的接口由 implement-interface 属性确定。types-matching 属性的值是一个 AspectJ 类型模式。任何类型匹配的 Bean 都会实现 UsageTracked 接口。注意,在前述示例的前置增强中,服务 Bean 可以直接作为 UsageTracked 接口的实现。要以编程方式访问 Bean,你可以编写以下内容:
Java
UsageTracked usageTracked = (UsageTracked) context.getBean("myService");1.5. 切面实例化模型
对于 Schema 定义的切面,唯一支持的实例化模型是单例模型。其他实例化模型可能会在未来的版本中得到支持。
1.6. 增强器
“增强器(Advisor)” 这个概念源自 Spring 中定义的 AOP 支持,在 AspectJ 中并没有直接对应的概念。增强器就像一个包含单个增强的小型自包含切面。增强本身由一个 Bean 表示,并且必须实现 Spring 中的增强类型中描述的一个接口。增强器可以利用 AspectJ 的切点表达式。
Spring 使用 <aop:advisor> 元素来支持增强器概念。你最常见的用法是与事务增强一起使用,这在 Spring 中也有自己的命名空间支持。以下示例展示了一个增强器:
XML
<aop:config>
<aop:pointcut id="businessService"
expression="execution(* com.xyz.myapp.service.*.*(..))"/>
<aop:advisor
pointcut-ref="businessService"
advice-ref="tx-advice"/>
</aop:config>
<tx:advice id="tx-advice">
<tx:attributes>
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
除了前面的示例中使用的 pointcut-ref 属性,你还可以使用 pointcut 属性来内联定义一个切点表达式。
为了定义一个增强器的优先级,以便增强可以参与排序,使用 order 属性来定义增强器的 Ordered 值。
1.7. AOP Schema 示例
这一部分展示了如何使用 Schema 支持重写 AOP 示例中的并发锁定失败重试示例。
业务服务的执行有时会因为并发问题(例如,死锁失败者)而失败。如果重试操作,那么下一次尝试很可能会成功。对于在这种情况下适合重试的业务服务(幂等操作,不需要回到用户进行冲突解决),我们希望透明地重试操作,以避免客户端看到 PessimisticLockingFailureException。这是一个明显横跨服务层中多个服务的需求,因此,通过切面来实现是理想的。
因为我们想要重试操作,所以我们需要使用环绕增强,这样我们就可以多次调用 proceed。以下代码片段显示了基本的切面实现(这是一个使用 Schema 支持的常规 Java 类):
Java
public class ConcurrentOperationExecutor implements Ordered {
private static final int DEFAULT_MAX_RETRIES = 2;
private int maxRetries = DEFAULT_MAX_RETRIES;
private int order = 1;
public void setMaxRetries(int maxRetries) {
this.maxRetries = maxRetries;
}
public int getOrder() {
return this.order;
}
public void setOrder(int order) {
this.order = order;
}
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
int numAttempts = 0;
PessimisticLockingFailureException lockFailureException;
do {
numAttempts++;
try {
return pjp.proceed();
}
catch(PessimisticLockingFailureException ex) {
lockFailureException = ex;
}
} while(numAttempts <= this.maxRetries);
throw lockFailureException;
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
请注意,切面实现了 Ordered 接口,这样我们就可以将切面的优先级设置得比事务增强更高(我们希望每次重试时都有一个新的事务)。maxRetries 和 order 属性都由 Spring 配置。主要的操作发生在 doConcurrentOperation 环绕增强方法中。我们尝试继续执行,如果我们因 PessimisticLockingFailureException 失败,我们会再试一次,除非我们已经用尽了所有的重试次数。
Note:这个类与
@AspectJ示例中使用的类完全相同,但是移除了注解。
对应的 Spring 配置如下:
XML
<aop:config>
<aop:aspect id="concurrentOperationRetry" ref="concurrentOperationExecutor">
<aop:pointcut id="idempotentOperation"
expression="execution(* com.xyz.myapp.service.*.*(..))"/>
<aop:around
pointcut-ref="idempotentOperation"
method="doConcurrentOperation"/>
</aop:aspect>
</aop:config>
<bean id="concurrentOperationExecutor"
class="com.xyz.myapp.service.impl.ConcurrentOperationExecutor">
<property name="maxRetries" value="3"/>
<property name="order" value="100"/>
</bean>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
请注意,目前我们假设所有的业务服务都是幂等的。如果不是这样,我们可以通过引入一个 Idempotent 注解,并使用该注解来注解服务操作的实现,从而精细化切面,使其只重试真正的幂等操作,如下面的示例所示:
Java
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
// marker annotation
}1
2
3
4
2
3
4
只重试幂等操作的切面更改涉及到精细化切点表达式,使得只有 @Idempotent 操作匹配,如下所示:
XML
<aop:pointcut id="idempotentOperation"
expression="execution(* com.xyz.myapp.service.*.*(..)) and
@annotation(com.xyz.myapp.service.Idempotent)"/>1
2
3
2
3
2. 选择使用哪种 AOP 声明风格
一旦你决定了切面是实现给定需求的最佳方法,你如何在使用 Spring AOP 和 AspectJ 之间做出选择,以及在 Aspect 语言(代码)风格、@AspectJ 注解风格或 Spring XML 风格之间做出选择呢?这些决策受到许多因素的影响,包括应用程序需求、开发工具和团队对 AOP 的熟悉程度。
2.1. Spring AOP 还是完整的 AspectJ?
使用最简单的可以工作的东西。Spring AOP 比使用完整的 AspectJ 更简单,因为没有必要在你的开发和构建过程中引入 AspectJ 编译器/织入器。如果你只需要对 Spring Bean 的操作进行增强,Spring AOP 是正确的选择。如果你需要对不由 Spring 容器管理的对象(如典型的领域对象)进行增强,你需要使用 AspectJ。如果你希望增强除简单方法执行之外的连接点(例如,字段获取或设置连接点等),你也需要使用 AspectJ。
当你使用 AspectJ 时,你可以选择 AspectJ 语言语法(也称为 “代码风格”)或 @AspectJ 注解风格。显然,如果你不使用 Java 5+,选择已经为你做出了:使用代码风格。如果切面在你的设计中起着重要的作用,并且你能够使用 AspectJ 开发工具(AJDT)Eclipse 插件,那么 AspectJ 语言语法是首选选项。它更清晰、更简单,因为这种语言是专门为编写切面而设计的。如果你不使用 Eclipse 或者只有少数几个切面在你的应用程序中并未起到主要作用,你可能会考虑使用 @AspectJ 风格,坚持在你的 IDE 中进行常规的 Java 编译,并在你的构建脚本中添加一个切面织入阶段。
2.2. Spring AOP 中使用 @AspectJ 还是 XML?
如果你选择使用 Spring AOP,你可以选择 @AspectJ 或 XML 风格。这里有各种需要考虑的权衡。
XML 风格可能对现有的 Spring 用户最为熟悉,而且它由真正的 POJO 支持。当使用 AOP 作为配置企业服务的工具时,XML 可以是一个好的选择(一个好的测试是你是否认为切点表达式是你可能想要独立改变的配置的一部分)。使用 XML 风格,从你的配置中可以更清楚地看出系统中存在哪些切面。
XML 风格有两个缺点。首先,它并没有完全封装在一个地方实现需求。DRY 原则指出,系统中的任何知识片段都应该有一个单一、明确、权威的表示。当使用 XML 风格时,如何实现需求的知识被分割在支持 Bean 类的声明和配置文件中的 XML 之间。当你使用 @AspectJ 风格时,这些信息被封装在一个模块中:切面。其次,XML 风格在表达能力上稍微比 @AspectJ 风格有限:只支持 “单例” 切面实例化模型,而且不可能组合在 XML 中声明的命名切点。例如,在 @AspectJ 风格中,你可以写如下的内容:
Java
@Pointcut("execution(* get*())")
public void propertyAccess() {}
@Pointcut("execution(org.xyz.Account+ *(..))")
public void operationReturningAnAccount() {}
@Pointcut("propertyAccess() && operationReturningAnAccount()")
public void accountPropertyAccess() {}1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
在 XML 格式中,你可以声明前两个切点:
XML
<aop:pointcut id="propertyAccess"
expression="execution(* get*())"/>
<aop:pointcut id="operationReturningAnAccount"
expression="execution(org.xyz.Account+ *(..))"/>1
2
3
4
5
2
3
4
5
XML 方法的缺点是你不能通过组合这些定义来定义 accountPropertyAccess 切点。
@AspectJ 风格支持额外的实例化模型和更丰富的切点组合。它的优点在于保持切面作为一个模块单元。它还有一个优点,那就是 @AspectJ 切面可以被 Spring AOP 和 AspectJ 理解(因此可以被它们使用)。所以,如果你后来决定你需要 AspectJ 的功能来实现额外的需求,你可以轻松地迁移到一个经典的 AspectJ 设置。总的来说,对于简单企业服务配置的自定义切面,Spring 团队更倾向于使用 @AspectJ 风格。
3. 混合切面类型
通过使用自动代理支持、Schema 定义的 <aop:aspect> 切面、<aop:advisor> 声明的增强器、甚至其他风格的代理和拦截器,完全可以在同一配置中混合 @AspectJ 风格的切面。所有这些都是通过使用相同的底层支持机制实现的,可以毫无困难地共存。
4. 代理机制
Spring AOP 使用 JDK 动态代理或 CGLIB 来为给定的目标对象创建代理。JDK 动态代理内置于 JDK 中,而 CGLIB 是一个常见的开源类定义库(重新打包到 spring-core 中)。
如果要被代理的目标对象实现了至少一个接口,那么将使用 JDK 动态代理。目标类型实现的所有接口都将被代理。如果目标对象没有实现任何接口,那么将创建一个 CGLIB 代理。
如果你想强制使用 CGLIB 代理(例如,代理目标对象定义的每个方法,而不仅仅是它实现的接口的方法),你可以这样做。然而,你应该考虑以下问题:
对于 CGLIB,无法增强
final方法,因为它们不能在运行时生成的子类中被覆盖。从 Spring 4.0 开始,你的被代理对象的构造函数不再被调用两次,因为 CGLIB 代理实例是通过 Objenesis 创建的。只有当你的 JVM 不允许构造函数绕过时,你可能会看到双重调用和来自 Spring 的 AOP 支持的相应调试日志条目。
要强制使用 CGLIB 代理,将 <aop:config> 元素的 proxy-target-class 属性值设置为 true,如下所示:
XML
<aop:config proxy-target-class="true">
<!-- other beans defined here... -->
</aop:config>1
2
3
2
3
当你使用 @AspectJ 自动代理支持时,要强制使用 CGLIB 代理,将 <aop:aspectj-autoproxy> 元素的 proxy-target-class 属性值设置为 true,如下所示:
XML
<aop:aspectj-autoproxy proxy-target-class="true"/>Tip
在运行时,多个
<aop:config/>部分会被折叠成一个统一的自动代理创建器,它应用任何<aop:config/>部分(通常来自不同的 XML Bean 定义文件)指定的最强代理设置。这也适用于<tx:annotation-driven/>和<aop:aspectj-autoproxy/>元素。明确地说,对
<tx:annotation-driven/>、<aop:aspectj-autoproxy/>或<aop:config/>元素使用proxy-target-class="true"将强制所有三者使用 CGLIB 代理。
4.1. 理解 AOP 代理
Spring AOP 是基于代理的。在你编写自己的切面或使用 Spring 框架提供的基于 Spring AOP 的切面之前,理解上述声明的实际含义至关重要。
首先,考虑你有一个普通的、未代理的、没有任何特殊之处的、直接的对象引用的场景,如下面的代码片段所示:
Java
public class SimplePojo implements Pojo {
public void foo() {
// this next method invocation is a direct call on the 'this' reference
this.bar();
}
public void bar() {
// some logic...
}
}1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
如果你在一个对象引用上调用一个方法,该方法将直接在该对象引用上被调用,如下图和代码片段所示:

Java
public class Main {
public static void main(String[] args) {
Pojo pojo = new SimplePojo();
// this is a direct method call on the 'pojo' reference
pojo.foo();
}
}1
2
3
4
5
6
7
2
3
4
5
6
7
当客户端代码拥有的引用是一个代理时,情况会稍微改变一些。请参考下图和代码片段:

Java
public class Main {
public static void main(String[] args) {
ProxyFactory factory = new ProxyFactory(new SimplePojo());
factory.addInterface(Pojo.class);
factory.addAdvice(new RetryAdvice());
Pojo pojo = (Pojo) factory.getProxy();
// this is a method call on the proxy!
pojo.foo();
}
}1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
这里需要理解的关键点是,Main 类的 main(..) 方法中的客户端代码拥有一个代理的引用。这意味着在该对象引用上的方法调用是对代理的调用。因此,代理可以委托给与该特定方法调用相关的所有拦截器(增强)。然而,一旦调用最终到达目标对象(在这种情况下是 SimplePojo 引用),它可能对自身的任何方法进行调用,如 this.bar() 或 this.foo(),都将对 this 引用进行调用,而不是代理。这有重要的含义。这意味着自我调用不会导致与方法调用相关的增强有机会运行。
那么,对此应该怎么办呢?最好的方法(这里的 “最好” 用得很宽松)是重构你的代码,使得自我调用不会发生。这确实需要你做一些工作,但这是最好的、最不具侵入性的方法。下一个方法绝对糟糕,我们犹豫是否要指出来,正因为它太糟糕了。你可以(尽管这对我们来说很痛苦)完全将你的类中的逻辑绑定到 Spring AOP,如下面的示例所示:
Java
public class SimplePojo implements Pojo {
public void foo() {
// this works, but... gah!
((Pojo) AopContext.currentProxy()).bar();
}
public void bar() {
// some logic...
}
}1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
这完全将你的代码与 Spring AOP 绑定在一起,并使类本身意识到它正在被用于 AOP 上下文中,这与 AOP 的原则相悖。当代理被创建时,它还需要一些额外的配置,如下面的示例所示:
Java
public class Main {
public static void main(String[] args) {
ProxyFactory factory = new ProxyFactory(new SimplePojo());
factory.addInterface(Pojo.class);
factory.addAdvice(new RetryAdvice());
factory.setExposeProxy(true);
Pojo pojo = (Pojo) factory.getProxy();
// this is a method call on the proxy!
pojo.foo();
}
}1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
最后,必须指出,AspectJ 没有这个自我调用的问题,因为它不是一个基于代理的 AOP 框架。
5. 以编程方式创建 @AspectJ 代理
除了使用 <aop:config> 或 <aop:aspectj-autoproxy> 在你的配置中声明切面外,还可以通过编程方式创建增强目标对象的代理。有关 Spring 的 AOP API 的完整细节,请参阅下一章。在这里,我们想要关注使用 @AspectJ 切面自动创建代理的能力。
你可以使用 org.springframework.aop.aspectj.annotation.AspectJProxyFactory 类为一个或多个 @AspectJ 切面增强的目标对象创建代理。这个类的基本用法非常简单,如下面的示例所示:
Java
// create a factory that can generate a proxy for the given target object
AspectJProxyFactory factory = new AspectJProxyFactory(targetObject);
// add an aspect, the class must be an @AspectJ aspect
// you can call this as many times as you need with different aspects
factory.addAspect(SecurityManager.class);
// you can also add existing aspect instances, the type of the object supplied must be an @AspectJ aspect
factory.addAspect(usageTracker);
// now get the proxy object...
MyInterfaceType proxy = factory.getProxy();1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
有关更多信息,请参阅 JavaDoc。
6. 在 Spring 应用中使用 AspectJ
我们在本章到目前为止所涵盖的所有内容都是纯粹的 Spring AOP。在这一部分,我们将探讨如何使用 AspectJ 编译器或织入器,作为 Spring AOP 的替代或补充,以满足你超出 Spring AOP 所提供设施的需求。
Spring 附带了一个小型的 AspectJ 切面库,它可以在你的发行版中作为 spring-aspects.jar 独立使用。你需要将它添加到你的类路径中,以便使用其中的切面。使用 AspectJ 和 Spring 对领域对象进行依赖注入和 AspectJ 的其他 Spring 切面讨论了这个库的内容以及你如何使用它。使用 Spring IoC 配置 AspectJ 切面讨论了如何对使用 AspectJ 编译器织入的 AspectJ 切面进行依赖注入。最后,Spring 框架中的 AspectJ 加载时织入为使用 AspectJ 的 Spring 应用程序提供了加载时织入的介绍。
6.1. 使用 AspectJ 和 Spring 对领域对象进行依赖注入
Spring 容器会实例化并配置你的应用程序上下文中定义的 Bean。也可以要求 Bean 工厂配置一个预先存在的对象,给定包含要应用的配置的 Bean 定义的名称。spring-aspects.jar 包含一个注解驱动的切面,利用这个能力允许任何对象的依赖注入。这种支持是为了被用于在任何容器控制之外创建的对象。领域对象经常属于这个类别,因为它们通常是通过 new 操作符或 ORM 工具作为数据库查询的结果编程创建的。
@Configurable 注解标记了一个类有资格进行 Spring 驱动的配置。在最简单的情况下,你可以纯粹地将其用作标记注解,如下面的示例所示:
Java
package com.xyz.myapp.domain;
import org.springframework.beans.factory.annotation.Configurable;
@Configurable
public class Account {
// ...
}1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
当以这种方式作为标记接口使用时,Spring 会使用与完全限定类型名(在这个例子中是 com.xyz.myapp.domain.Account)相同的 Bean 定义(通常是原型范围)来配置注解类型的新实例(在这个例子中是 Account)。由于 Bean 的默认名称是其类型的完全限定名称,因此声明原型定义的一种方便的方式是省略 id 属性,如下面的示例所示:
XML
<bean class="com.xyz.myapp.domain.Account" scope="prototype">
<property name="fundsTransferService" ref="fundsTransferService"/>
</bean>1
2
3
2
3
如果你想明确指定要使用的原型 Bean 定义的名称,你可以直接在注解中这样做,如下面的示例所示:
Java
package com.xyz.myapp.domain;
import org.springframework.beans.factory.annotation.Configurable;
@Configurable("account")
public class Account {
// ...
}1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
Spring 现在会查找名为 account 的 Bean 定义,并使用它作为配置新的 Account 实例的定义。
你也可以使用自动装配来避免必须指定专用的 Bean 定义。要让 Spring 应用自动装配,使用 @Configurable 注解的 autowire 属性。你可以分别指定 @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 操作符实例化的对象)以及正在进行反序列化的 Serializable 对象(例如,通过 readResolve())。
Note
上述段落中的关键词之一是 “本质上”。在大多数情况下,“从新对象的初始化返回后” 的确切语义是可以的。在这个上下文中,“初始化后” 意味着在对象构造完成后注入依赖项。这意味着在类的构造函数体中,依赖项是不可用的。如果你希望在构造函数体运行之前注入依赖项,从而在构造函数体中使用,你需要在
@Configurable声明中定义,如下所示:Java@Configurable(preConstruction = true)你可以在 AspectJ 编程指南的附录中找到更多关于 AspectJ 中各种切点类型语言语义的信息。
要使其工作,必须使用 AspectJ 织入器将注解类型织入在一起。你可以使用构建时的 Ant 或 Maven 任务来完成这个操作(例如,参见 AspectJ 开发环境指南),或者使用加载时织入(参见 Spring 框架中的 AspectJ 加载时织入)。AnnotationBeanConfigurerAspect 本身需要由 Spring 配置(以获取用于配置新对象的 Bean 工厂的引用)。如果你使用基于 Java 的配置,你可以在任何 @Configuration 类中添加 @EnableSpringConfigured,如下所示:
Java
@Configuration
@EnableSpringConfigured
public class AppConfig {
}1
2
3
4
2
3
4
如果你更喜欢基于 XML 的配置,Spring context 命名空间定义了一个方便的 context:spring-configured 元素,你可以按照以下方式使用:
XML
<context:spring-configured/>在切面配置完成之前创建的 @Configurable 对象的实例会导致向调试日志发出一条消息,并且不会对对象进行配置。一个例子可能是 Spring 配置中的一个 Bean,在由 Spring 初始化时创建域对象。在这种情况下,你可以使用 depends-on Bean 属性来手动指定 Bean 依赖于配置切面。以下示例展示了如何使用 depends-on 属性:
XML
<bean id="myService"
class="com.xzy.myapp.service.MyService"
depends-on="org.springframework.beans.factory.aspectj.AnnotationBeanConfigurerAspect">
<!-- ... -->
</bean>1
2
3
4
5
6
7
2
3
4
5
6
7
Tip:除非你真的打算在运行时依赖其语义,否则不要通过 Bean 配置器切面激活
@Configurable处理。特别是,确保你不要在注册为容器的常规 Spring Bean 的 Bean 类上使用@Configurable。这样做会导致双重初始化,一次通过容器,一次通过切面。
6.1.1. 对 @Configurable 对象进行单元测试
@Configurable 支持的目标之一是使域对象的独立单元测试能够在没有与硬编码查找相关的困难的情况下进行。如果 @Configurable 类型没有被 AspectJ 织入,那么在单元测试期间,注解将没有任何影响。你可以在测试对象中设置 Mock 或 Stub 属性引用,并像正常一样进行。如果 @Configurable 类型已经被 AspectJ 织入,你仍然可以像正常一样在容器外进行单元测试,但是每次构造一个 @Configurable 对象时,你都会看到一个警告消息,表明它没有被 Spring 配置。
6.1.2. 使用多个应用程序上下文
用于实现 @Configurable 支持的 AnnotationBeanConfigurerAspect 是一个 AspectJ 单例切面。单例切面的范围与静态成员的范围相同:每个定义类型的类加载器都有一个切面实例。这意味着,如果你在同一个类加载器层次结构中定义了多个应用程序上下文,你需要考虑在哪里定义 @EnableSpringConfigured Bean,以及在类路径上放置 spring-aspects.jar 的位置。
考虑一个典型的 Spring Web 应用程序配置,它有一个共享的父应用程序上下文,定义了常见的业务服务,支持这些服务所需的一切,以及每个 Servlet 的一个子应用程序上下文(其中包含特定于该 Servlet 的定义)。所有这些上下文都在同一个类加载器层次结构中共存,因此 AnnotationBeanConfigurerAspect 只能引用其中一个。在这种情况下,我们建议在共享(父)应用程序上下文中定义 @EnableSpringConfigured Bean。这定义了你可能希望注入到域对象中的服务。一个后果是,你不能使用 @Configurable 机制将域对象配置为引用在子(特定于 Servlet)上下文中定义的 Bean(这可能不是你想做的事情)。
当在同一个容器中部署多个 Web 应用程序时,确保每个 Web 应用程序都使用自己的类加载器加载 spring-aspects.jar 中的类型(例如,通过将 spring-aspects.jar 放在 WEB-INF/lib 中)。如果 spring-aspects.jar 只被添加到容器范围的类路径(因此由共享的父类加载器加载),所有的 Web 应用程序都会共享同一个切面实例(这可能不是你想要的)。
6.2. AspectJ 的其他 Spring 切面
除了 @Configurable 切面外,spring-aspects.jar 还包含一个 AspectJ 切面,你可以用它来驱动用 @Transactional 注解标注的类型和方法的 Spring 事务管理。这主要是为了那些希望在 Spring 容器外使用 Spring 框架的事务支持的用户。
解释 @Transactional 注解的切面是 AnnotationTransactionAspect。当你使用这个切面时,你必须对实现类(或者该类中的方法或者两者都有)进行注解,而不是该类实现的接口(如果有的话)。AspectJ 遵循 Java 的规则,即接口上的注解不会被继承。
在类上的 @Transactional 注解指定了该类中任何公共操作执行的默认事务语义。
在类内部的方法上的 @Transactional 注解会覆盖由类注解(如果存在的话)给出的默认事务语义。任何可见性的方法都可以被注解,包括私有方法。直接对非公共方法进行注解是获取这种方法执行的事务划分的唯一方式。
Note:自 Spring 框架 4.2 版本开始,
spring-aspects提供了一个类似的切面,为标准的javax.transaction.Transactional注解提供了完全相同的功能。请查看JtaAnnotationTransactionAspect以获取更多详细信息。
对于想要使用 Spring 配置和事务管理支持但不想(或不能)使用注解的 AspectJ 程序员,spring-aspects.jar 还包含了你可以扩展以提供自己的切点定义的抽象切面。请查看 AbstractBeanConfigurerAspect 和 AbstractTransactionAspect 切面的源码以获取更多信息。作为一个例子,以下摘录展示了你如何编写一个切面,使用与全限定类名匹配的原型 Bean 定义来配置在域模型中定义的所有对象实例:
Java
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(..)) &&
CommonPointcuts.inDomainModel() &&
this(beanInstance);
}1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
6.3. 使用 Spring IoC 配置 AspectJ 切面
当你在 Spring 应用程序中使用 AspectJ 切面时,自然会希望并期望能够用 Spring 配置这些切面。AspectJ 运行时本身负责切面的创建,而通过 Spring 配置 AspectJ 创建的切面的方式取决于切面使用的 AspectJ 实例化模型(per-xxx 子句)。
大多数 AspectJ 切面都是单例切面。配置这些切面很容易。你可以创建一个正常引用切面类型的 Bean 定义,并包含 factory-method="aspectOf" 的 Bean 属性。这确保了 Spring 通过向 AspectJ 请求而不是试图自己创建实例来获取切面实例。以下示例展示了如何使用 factory-method="aspectOf" 属性:
XML
<bean id="profiler" class="com.xyz.profiler.Profiler"
factory-method="aspectOf">
<property name="profilingStrategy" ref="jamonProfilingStrategy"/>
</bean>1
2
3
4
2
3
4
非单例切面更难配置。然而,通过创建原型 Bean 定义并使用 spring-aspects.jar 中的 @Configurable 支持,一旦 AspectJ 运行时创建了切面实例,就可以配置它们。
如果你有一些 @AspectJ 切面,你希望用 AspectJ 织入(例如,对域模型类型使用加载时织入),还有其他 @AspectJ 切面,你希望与 Spring AOP 一起使用,而这些切面都在 Spring 中配置,你需要告诉 Spring AOP @AspectJ 自动代理支持,应该使用配置中定义的哪个确切的 @AspectJ 切面子集进行自动代理。你可以通过在 <aop:aspectj-autoproxy/> 声明中使用一个或多个 <include/> 元素来做到这一点。每个 <include/> 元素指定一个名称模式,只有名称至少匹配一个模式的 Bean 才用于 Spring AOP 自动代理配置。以下示例展示了如何使用 <include/> 元素:
XML
<aop:aspectj-autoproxy>
<aop:include name="thisBean"/>
<aop:include name="thatBean"/>
</aop:aspectj-autoproxy>1
2
3
4
2
3
4
Note:不要被
<aop:aspectj-autoproxy/>元素的名称误导。使用它会导致创建 Spring AOP 代理。这里使用的是@AspectJ风格的切面声明,但 AspectJ 运行时并未参与其中。
6.4. Spring 框架中的 AspectJ 加载时织入
加载时织入(LTW,Load-Time Weaving)指的是将 AspectJ 切面织入到应用程序的类文件中,这些类文件在被加载到 Java 虚拟机(JVM)时进行。本节的重点是在 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 示例应用程序。
6.4.1. 第一个示例
假设你是一个应用程序开发人员,被分配了诊断系统中一些性能问题的任务。我们将开启一个简单的性能分析切面,而不是使用性能分析工具,这让我们可以快速获取一些性能指标。然后,我们可以立即在该特定区域应用更细粒度的性能分析工具。
Note:这里展示的示例使用了 XML 配置。你也可以使用 Java 配置来配置和使用
@AspectJ。具体来说,你可以使用@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(){}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
我们还需要创建一个 META-INF/aop.xml 文件,以增强 AspectJ 织入器我们希望将我们的 ProfilingAspect 织入到我们的类中。这种文件约定,即 Java 类路径上存在名为 META-INF/aop.xml 的文件,是 AspectJ 的标准。以下示例展示了 aop.xml 文件:
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>1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
现在我们可以转到配置的 Spring 特定部分。我们需要配置一个 LoadTimeWeaver(稍后解释)。这个加载时织入器是负责将一个或多个 META-INF/aop.xml 文件中的切面配置织入到你的应用程序的类中的关键组件。好处是它不需要很多配置(你可以指定一些更多的选项,但这些将在后面详细介绍),如下例所示:
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">
<!-- 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>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
现在,所有必需的工件(切面、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");
// the profiling aspect is 'woven' around this method execution
entitlementCalculationService.calculateEntitlement();
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
我们还有最后一件事要做。本节的介绍确实说过,可以使用 Spring 在每个类加载器的基础上选择性地开启 LTW,这是真的。然而,对于这个示例,我们使用一个 Java 代理(由 Spring 提供)来开启 LTW。我们使用以下命令来运行前面显示的 Main 类:
Bash
java -javaagent:C:/projects/foo/lib/global/spring-instrument.jar foo.Main-javaagent 是一个用于指定和启用代理以对运行在 JVM 上的程序进行检测的标志。Spring 框架附带了这样一个代理,即 InstrumentationSavingAgent,它被打包在 spring-instrument.jar 中,该 Jar 文件在前面的示例中作为 -javaagent 参数的值提供。
执行 Main 程序的输出看起来像下一个示例。(我在 calculateEntitlement() 实现中引入了一个 Thread.sleep(..) 语句,以便性能分析器实际捕获到的不仅仅是 0 毫秒(01234 毫秒并不是 AOP 引入的开销)。以下列表显示了我们运行性能分析器时得到的输出:
Text
Calculating entitlement
StopWatch 'ProfilingAspect': running time (millis) = 1234
------ ----- ----------------------------
ms % Task name
------ ----- ----------------------------
01234 100% calculateEntitlement1
2
3
4
5
6
7
2
3
4
5
6
7
由于这个 LTW 是通过使用完整的 AspectJ 来实现的,我们不仅仅限于对 Spring Bean 进行增强。下面对 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();
// the profiling aspect will be 'woven' around this method execution
entitlementCalculationService.calculateEntitlement();
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
注意,在前面的程序中,我们引导 Spring 容器,然后完全在 Spring 的上下文之外创建了一个新的 StubEntitlementCalculationService 实例。性能分析增强仍然被织入。
诚然,这个示例过于简单。然而,Spring 中 LTW 支持的基础已经在前面的示例中全部介绍过了,本节的其余部分将详细解释每一部分配置和使用背后的 “为什么”。
Note:在这个示例中使用的
ProfilingAspect可能很基础,但它非常有用。这是一个开发时切面的很好例子,开发人员可以在开发过程中使用它,然后可以轻松地从部署到 UAT 或生产环境的应用程序的构建中排除它。
6.4.2. 切面
你在 LTW 中使用的切面必须是 AspectJ 切面。你可以用 AspectJ 语言本身编写它们,或者你可以用 @AspectJ 风格编写你的切面。然后,你的切面既是有效的 AspectJ 切面,也是 Spring AOP 切面。此外,编译后的切面类需要在类路径上可用。
6.4.3. META-INF/aop.xml
AspectJ LTW 基础设施是通过使用一个或多个位于 Java 类路径上的 META-INF/aop.xml 文件(直接或更常见的是在 Jar 文件中)来配置的。
这个文件的结构和内容在 AspectJ 参考文档的 LTW 部分有详细说明。因为 aop.xml 文件是 100% 的 AspectJ,所以我们在这里不再进一步描述它。
6.4.4. 所需的库(JARS)
至少,你需要以下库来使用 Spring 框架对 AspectJ LTW 的支持:
spring-aop.jaraspectjweaver.jar
如果你使用 Spring 提供的代理来启用检测,你还需要:
spring-instrument.jar
6.4.5. Spring 配置
Spring 的 LTW 支持中的关键组件是 LoadTimeWeaver 接口(在 org.springframework.instrument.classloading 包中),以及随 Spring 分发的众多实现。LoadTimeWeaver 负责在运行时向 ClassLoader 添加一个或多个 java.lang.instrument.ClassFileTransformers,这为各种有趣的应用打开了大门,其中一个就是切面的 LTW。
Note:如果你对运行时类文件转换的概念不熟悉,建议在继续之前查看
java.lang.instrument包的 JavaDoc API 文档。虽然那份文档并不全面,但至少你可以看到关键的接口和类(作为你阅读本节的参考)。
为特定的 ApplicationContext 配置 LoadTimeWeaver 可能就像添加一行代码那么简单。(注意,你几乎肯定需要使用 ApplicationContext 作为你的 Spring 容器 —— 通常来说,BeanFactory 是不够的,因为 LTW 支持使用 BeanFactoryPostProcessors。)
要启用 Spring 框架的 LTW 支持,你需要配置一个 LoadTimeWeaver,通常是通过使用 @EnableLoadTimeWeaving 注解来完成的,如下所示:
Java
@Configuration
@EnableLoadTimeWeaving
public class AppConfig {
}1
2
3
4
2
3
4
或者,如果你更喜欢基于 XML 的配置,可以使用 <context:load-time-weaver/> 元素。请注意,该元素在 context 命名空间中定义。以下示例展示了如何使用 <context:load-time-weaver/>:
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">
<context:load-time-weaver/>
</beans>1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
前面的配置自动为你定义和注册了一些 LTW 特定的基础设施 Bean,如 LoadTimeWeaver 和 AspectJWeavingEnabler。默认的 LoadTimeWeaver 是 DefaultContextLoadTimeWeaver 类,它试图装饰一个自动检测到的 LoadTimeWeaver。“自动检测” 到的 LoadTimeWeaver 的确切类型取决于你的运行环境。下表总结了各种 LoadTimeWeaver 实现:
| 运行时环境 | LoadTimeWeaver 实现 |
|---|---|
| 运行在 Apache Tomcat 中 | TomcatLoadTimeWeaver |
| 运行在 GlassFish 中(仅限于 EAR 部署) | GlassFishLoadTimeWeaver |
| 运行在 Red Hat 的 JBoss AS 或 WildFly 中 | JBossLoadTimeWeaver |
| 运行在 IBM 的 WebSphere 中 | WebSphereLoadTimeWeaver |
| 运行在 Oracle 的 WebLogic 中 | WebLogicLoadTimeWeaver |
JVM 启动时使用 Spring InstrumentationSavingAgent(java -javaagent:path/to/spring-instrument.jar) | InstrumentationLoadTimeWeaver |
后备选项,期望底层的 ClassLoader 遵循常见的约定(即 addTransformer 和可选的 getThrowawayClassLoader 方法) | ReflectiveLoadTimeWeaver |
DefaultContextLoadTimeWeaver LoadTimeWeavers请注意,该表格只列出了在使用 DefaultContextLoadTimeWeaver 时自动检测到的 LoadTimeWeavers。你可以准确地指定要使用的 LoadTimeWeaver 实现。
要使用 Java 配置指定特定的 LoadTimeWeaver,需要实现 LoadTimeWeavingConfigurer 接口并重写 getLoadTimeWeaver() 方法。以下示例指定了一个 ReflectiveLoadTimeWeaver:
Java
@Configuration
@EnableLoadTimeWeaving
public class AppConfig implements LoadTimeWeavingConfigurer {
@Override
public LoadTimeWeaver getLoadTimeWeaver() {
return new ReflectiveLoadTimeWeaver();
}
}1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
如果你使用基于 XML 的配置,你可以将完全限定的类名指定为 <context:load-time-weaver/> 元素上的 weaver-class 属性的值。同样,以下示例指定了一个 ReflectiveLoadTimeWeaver:
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">
<context:load-time-weaver
weaver-class="org.springframework.instrument.classloading.ReflectiveLoadTimeWeaver"/>
</beans>1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
通过配置定义和注册的 LoadTimeWeaver 可以稍后通过使用众所周知的名称 loadTimeWeaver 从 Spring 容器中检索。请记住,LoadTimeWeaver 只存在作为 Spring 的 LTW 基础设施添加一个或多个 ClassFileTransformers 的机制。实际执行 LTW 的 ClassFileTransformer 是 ClassPreProcessorAgentAdapter 类(来自 org.aspectj.weaver.loadtime 包)。有关 ClassPreProcessorAgentAdapter 类的更多详细信息,请参阅该类的 JavaDoc,因为如何实际进行织入的具体细节超出了本文档的范围。
还有一个最后的配置属性需要讨论:aspectjWeaving 属性(如果你使用 XML,则为 aspectj-weaving)。此属性控制是否启用 LTW。它接受三个可能的值之一,如果属性不存在,则默认值为 autodetect。下表总结了三个可能的值:
| 注解值 | XML 值 | 说明 |
|---|---|---|
ENABLED | on | AspectJ 织入已开启,适当的切面将在加载时织入 |
DISABLED | off | LTW 已关闭。没有切面会在加载时织入 |
AUTODETECT | autodetect | 如果 Spring LTW 基础设施可以找到至少一个 META-INF/aop.xml 文件,那么 AspectJ 织入就会开启。否则,它将关闭。这是默认值 |
6.4.6. 针对环境的配置
这最后一部分包含了你在使用 Spring 的 LTW 支持时,如在应用服务器和 Web 容器等环境中所需的所有额外设置和配置。
6.4.6.1. Tomcat,JBoss,WebSphere,WebLogic
Tomcat、JBoss/WildFly、IBM WebSphere Application Server 和 Oracle WebLogic Server 都提供了一个能够进行本地检测的通用 App ClassLoader。Spring 的原生 LTW 可能会利用这些 ClassLoader 实现来提供 AspectJ 织入。你可以简单地启用加载时织入,如前面所述。具体来说,你不需要修改 JVM 启动脚本来添加 -javaagent:path/to/spring-instrument.jar。
请注意,在 JBoss 上,你可能需要禁用应用服务器扫描,以防止它在应用程序实际启动之前加载类。一个快速的解决方法是在你的工件中添加一个名为 WEB-INF/jboss-scanning.xml 的文件,内容如下:
XML
<scanning xmlns="urn:jboss:scanning:1.0"/>6.4.6.2. 通用 Java 应用程序
当在不被特定 LoadTimeWeaver 实现支持的环境中需要类检测时,JVM 代理是通用的解决方案。对于这种情况,Spring 提供了 InstrumentationLoadTimeWeaver,它需要一个特定于 Spring(但非常通用)的 JVM 代理,spring-instrument.jar,由常见的 @EnableLoadTimeWeaving 和 <context:load-time-weaver/> 设置自动检测。
要使用它,你必须通过提供以下 JVM 选项来启动带有 Spring 代理的虚拟机:
Text
-javaagent:/path/to/spring-instrument.jar请注意,这需要修改 JVM 启动脚本,这可能会阻止你在应用服务器环境中使用这个(取决于你的服务器和你的操作策略)。也就是说,对于像独立的 Spring Boot 应用程序这样的一对一 JVM 部署,你通常可以控制整个 JVM 设置。
7. 更多资源
你可以在 AspectJ 网站上找到更多关于 AspectJ 的信息。
Adrian Colyer 等人的《Eclipse AspectJ》(Addison-Wesley,2005)为 AspectJ 语言提供了全面的介绍和参考。
Ramnivas Laddad 的《AspectJ 实战》第二版(Manning,2009)非常值得推荐。这本书的重点是 AspectJ,但也深入探讨了许多一般的 AOP 主题。