使用Spring进行面向切面编程
一、aop术语:
1、切面:所有切入点的集合
2、切入点:一组符合某种规则的连接点
3、连接点:狭义上通俗的讲指的是某个方法
4、通知:在某个连接点上的某种操作,该操作并非连接点中的操作,而是外来的操作。
5、引入(Introduction):引入(在AspectJ中被称为inter-type声明)使得一个切面可以定义被通知对象实现给定的接口, 并且可以为那些对象提供具体的实现
二、例子
一般你可以单纯的使用aspectj进行aop,也可以让spring和aspectj联合来开发,前者功能强大,但需另外的编译器和熟悉aspectj的语法,后者实现起来较为简便。在此我们使用后者。
Spring与Aspectj进行aop,也有2种方式,一是单纯的使用aspectj注解,二是在配置文件中进行定义。前者较为灵活强大,后者利于管理模块化。在此我们只讲前者。
2-1、添加aspectj支持
<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 http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd">
<aop:aspectj-autoproxy/>
</beans>
2-2、定义目标类
目标就是需要拦截的操作
如下,在调用方法时,附加一些log信息
public class RegisterService {
public boolean regist(String name){
if(name==null)
return false;
System.out.println(name+" regist");
return true;
}
public boolean login(String name){
if(name==null)
return false;
System.out.println(name+" login");
return true;
}
}
2-3、定义拦截者
@Aspect
public class LogInterceptor {
@Pointcut("execution(* regist*(..))||execution(* login*(..))")
private void log() {
}
@Before("log()")
public void logInterceptor_before() {
System.out.println("Before LOG:"+" info has loged to file");
}
@After("log() && args(a)")
public void logInterceptor_after(String a) {
System.out.println("After LOG:"+a+" info has loged to file");
}
}
2-4、在配置文件中配置拦截者和目标bean
<bean id="regist" + "args(account,..)") public void validateAccount(Account account) { // ... }
切入点表达式的 args(account,..) 部分有两个目的:首先它保证了 只会匹配那些接受至少一个参数的方法的执行,而且传入的参数必须是Account类型的实例, 其次它使得在通知体内可以通过account 参数访问实际的Account对象。
另外一个办法是定义一个切入点,这个切入点在匹配某个连接点的时候“提供”了 Account对象的值,然后直接从通知中访问那个命名切入点。看起来和下面的示例一样:
@Pointcut("com.xyz.myapp.SystemArchitecture.dataAccessOperation() &&" + "args(account,..)") private void accountDataAccessOperation(Account account) {} @Before("accountDataAccessOperation(account)") public void validateAccount(Account account) { // ... }
3-4. 通知顺序
如果有多个通知想要在同一连接点运行会发生什么?Spring AOP遵循跟AspectJ一样的优先规则来确定通知执行的顺序。 在“进入”连接点的情况下,最高优先级的通知会先执行(所以给定的两个前置通知中,优先级高的那个会先执行)。 在“退出”连接点的情况下,最高优先级的通知会最后执行。(所以给定的两个后置通知中, 优先级高的那个会第二个执行)。
当定义在不同的切面里的两个通知都需要在一个相同的连接点中运行, 那么除非你指定,否则执行的顺序是未知的。你可以通过指定优先级来控制执行顺序。 在标准的Spring方法中可以在切面类中实现org.springframework.core.Ordered 接口或者用Order注解做到这一点。在两个切面中, Ordered.getValue()方法返回值(或者注解值)较低的那个有更高的优先级。
当定义在相同的切面里的两个通知都需要在一个相同的连接点中运行, 执行的顺序是未知的(因为这里没有方法通过反射javac编译的类来获取声明顺序)。 考虑在每个切面类中按连接点压缩这些通知方法到一个通知方法,或者重构通知的片段到各自的切面类中 - 它能在切面级别进行排序。
3-5. 引入(Introduction)
引入(在AspectJ中被称为inter-type声明)使得一个切面可以定义被通知对象实现给定的接口, 并且可以为那些对象提供具体的实现。
使用@DeclareParents注解来定义引入。这个注解用来定义匹配的类型 拥有一个新的父类(所以有了这个名字)。比如,给定一个接口UsageTracked, 和接口的具体实现DefaultUsageTracked类, 接下来的切面声明了所有的service接口的实现都实现了UsageTracked接口。 (比如为了通过JMX输出统计信息)。
@Aspect public class UsageTracking { @DeclareParents(value="com.xzy.myapp.service.*+", defaultImpl=DefaultUsageTracked.class) public static UsageTracked mixin; @Before("com.xyz.myapp.SystemArchitecture.businessService() &&" + "this(usageTracked)") public void recordUsage(UsageTracked usageTracked) { usageTracked.incrementUseCount(); } }
实现的接口通过被注解的字段类型来决定。@DeclareParents注解的 value属性是一个AspectJ的类型模式:- 任何匹配类型的bean都会实现 UsageTracked接口。请注意,在上面的前置通知的例子中,service beans 可以直接用作UsageTracked接口的实现。如果需要编程式的来访问一个bean, 你可以这样写:
UsageTracked usageTracked = (UsageTracked) context.getBean("myService")
3-6 切面实例化模型
(这是一个高级主题,所以如果你刚开始学习AOP你可以跳过它到后面的章节)
默认情况下,在application context中每一个切面都会有一个实例。AspectJ把这个叫做单例化模型。 也可以用其他的生命周期来定义切面:Spring支持AspectJ的 perthis 和pertarget实例化模型(现在还不支持percflow、percflowbelow 和pertypewithin)。
一个"perthis" 切面通过在@Aspect注解中指定perthis 子句来声明。让我们先来看一个例子,然后解释它是如何运作的:
@Aspect("perthis(com.xyz.myapp.SystemArchitecture.businessService())") public class MyAspect { private int someState; @Before(com.xyz.myapp.SystemArchitecture.businessService()) public void recordServiceUsage() { // ... } }
这个'perthis'子句的效果是每个独立的service对象执行一个业务时都会 创建一个切面实例(切入点表达式所匹配的连接点上的每一个独立的对象都会绑定到'this'上)。 在service对象上第一次调用方法的时候,切面实例将被创建。切面在service对象失效的同时失效。 在切面实例被创建前,所有的通知都不会被执行,一旦切面对象创建完成, 定义的通知将会在匹配的连接点上执行,但是只有当service对象是和切面关联的才可以。 请参阅 AspectJ 编程指南了解更多关于per-clauses的信息。
'pertarget'实例模型的跟“perthis”完全一样,只不过是为每个匹配于连接点 的独立目标对象创建一个切面实例。
四、pring aspectj详解
1、声明切面
启用@AspectJ支持后,在application context中定义的任意带有一个@Aspect切面(拥有@Aspect注解)的bean都将被Spring自动识别并用于配置Spring AOP。以下例子展示了为完成一个不是非常有用的切面所需要的最小定义:
application context中一个常见的bean定义,它指向一个使用了@Aspect注解的bean类:
<bean id="myAspect" 的方法的执行:
@Pointcut("execution(* transfer(..))")// the pointcut expression_r private void anyOldTransfer() {}// the pointcut signature
切入点表达式,也就是组成@Pointcut注解的值,是正规的AspectJ 5切入点表达式。
3、入点指示符(PCD)的支持
Spring AOP支持在切入点表达式中使用如下的AspectJ切入点指示符:
其他的切入点类型
完整的AspectJ切入点语言支持额外的切入点指示符,但是Spring并不支持。它们分别是call, get, set, preinitialization, staticinitialization, initialization, handler, adviceexecution, withincode, cflow,cflowbelow, if, @this和@withincode。在Spring AOP中使用这些指示符将会导致抛出IllegalArgumentException异常。
Spring AOP支持的切入点指示符可能会在将来的版本中得到扩展,从而支持更多的AspectJ切入点指示符。
execution - 匹配方法执行的连接点,这是你将会用到的Spring的最主要的切入点指示符。
within - 限定匹配特定类型的连接点(在使用Spring AOP的时候,在匹配的类型中定义的方法的执行)。
this - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中bean reference(Spring AOP 代理)是指定类型的实例。
target - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中目标对象(被代理的应用对象)是指定类型的实例。
args - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中参数是指定类型的实例。
@target - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中正执行对象的类持有指定类型的注解。
@args - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中实际传入参数的运行时类型持有指定类型的注解。
@within - 限定匹配特定的连接点,其中连接点所在类型已指定注解(在使用Spring AOP的时候,所执行的方法所在类型已指定注解)。
@annotation - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中连接点的主题持有指定的注解。
另外,Spring AOP还提供了一个名为'bean'的PCD。这个PCD允许你限定匹配连接点到一个特定名称的Spring bean,或者到一个特定名称Spring bean的集合(当使用通配符时)。'bean' PCD具有下列的格式:
bean(idOrNameOfBean)
'idOrNameOfBean'标记可以是任何Spring bean的名字:限定通配符使用'*'来提供,如果你为Spring bean制定一些命名约定,你可以非常容易地编写一个'bean' PCD表达式将它们选出来。和其它连接点指示符一样,'bean' PCD也支持&&, ||和 !逻辑操作符。
注意
请注意'bean' PCD仅仅 被Spring AOP支持而不是AspectJ. 这是Spring对AspectJ中定义的标准PCD的一个特定扩展。
'bean' PCD不仅仅可以在类型级别(被限制在基于织入AOP上)上操作而还可以在实例级别(基于Spring bean的概念)上操作。
因为Spring AOP限制了连接点必须是方法执行级别的,上文pointcut指示符中的讨论也给出了一个定义,这个定义和AspectJ的编程指南中的定义相比显得更加狭窄。除此之外,AspectJ它本身有基于类型的语义,在执行的连接点'this'和'target'都是指同一个对象,也就是执行方法的对象。Spring AOP是一个基于代理的系统,并且严格区分代理对象本身(对应于'this')和背后的目标对象(对应于'target')
4、组合切入点表达式
切入点表达式可以使用'&', '||' 和 '!'来组合。还可以通过名字来指向切入点表达式。以下的例子展示了三种切入点表达式: anyPublicOperation(在一个方法执行连接点代表了任意public方法的执行时匹配);inTrading(在一个代表了在交易模块中的任意的方法执行时匹配)和 tradingOperation(在一个代表了在交易模块中的任意的公共方法执行时匹配)。
@Pointcut("execution(public * *(..))") private void anyPublicOperation() {} @Pointcut("within(com.xyz.someapp.trading..*") private void inTrading() {} @Pointcut("anyPublicOperation() && inTrading()") private void tradingOperation() {}
如上所示,用更少的命名组件来构建更加复杂的切入点表达式是一种最佳实践。当用名字来指定切入点时使用的是常见的Java成员可视性访问规则。(比如说,你可以在同一类型中访问私有的切入点,在继承关系中访问受保护的切入点,可以在任意地方访问公共切入点)。成员可视性访问规则不影响到切入点的匹配。
5、示例
Spring AOP 用户可能会经常使用 execution切入点指示符。执行表达式的格式如下:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)
除了返回类型模式(上面代码片断中的ret-type-pattern),名字模式和参数模式以外, 所有的部分都是可选的。返回类型模式决定了方法的返回类型必须依次匹配一个连接点。 你会使用的最频繁的返回类型模式是*,它代表了匹配任意的返回类型。 一个全限定的类型名将只会匹配返回给定类型的方法。名字模式匹配的是方法名。 你可以使用*通配符作为所有或者部分命名模式。 参数模式稍微有点复杂:()匹配了一个不接受任何参数的方法, 而(..)匹配了一个接受任意数量参数的方法(零或者更多)。 模式(*)匹配了一个接受一个任何类型的参数的方法。 模式(*,String)匹配了一个接受两个参数的方法,第一个可以是任意类型, 第二个则必须是String类型。更多的信息请参阅AspectJ编程指南中 语言语义的部分。
下面给出一些通用切入点表达式的例子。
任意公共方法的执行:
execution(public * *(..))
任何一个名字以“set”开始的方法的执行:
execution(* set*(..))
AccountService接口定义的任意方法的执行:
execution(* com.xyz.service.AccountService.*(..))
在service包中定义的任意方法的执行:
execution(* com.xyz.service.*.*(..))
在service包或其子包中定义的任意方法的执行:
execution(* com.xyz.service..*.*(..))
在service包中的任意连接点(在Spring AOP中只是方法执行):
within(com.xyz.service.*)
在service包或其子包中的任意连接点(在Spring AOP中只是方法执行):
within(com.xyz.service..*)
实现了AccountService接口的代理对象的任意连接点 (在Spring AOP中只是方法执行):
this(com.xyz.service.AccountService)
'this'在绑定表单中更加常用:- 请参见后面的通知一节中了解如何使得代理对象在通知体内可用。
实现AccountService接口的目标对象的任意连接点 (在Spring AOP中只是方法执行):
target(com.xyz.service.AccountService)
'target'在绑定表单中更加常用:- 请参见后面的通知一节中了解如何使得目标对象在通知体内可用。
任何一个只接受一个参数,并且运行时所传入的参数是Serializable 接口的连接点(在Spring AOP中只是方法执行)
args(java.io.Serializable)
'args'在绑定表单中更加常用:- 请参见后面的通知一节中了解如何使得方法参数在通知体内可用。
请注意在例子中给出的切入点不同于 execution(* *(java.io.Serializable)): args版本只有在动态运行时候传入参数是Serializable时才匹配,而execution版本在方法签名中声明只有一个 Serializable类型的参数时候匹配。
目标对象中有一个 @Transactional 注解的任意连接点 (在Spring AOP中只是方法执行)
@target(org.springframework.transaction.annotation.Transactional)
'@target'在绑定表单中更加常用:- 请参见后面的通知一节中了解如何使得注解对象在通知体内可用。
任何一个目标对象声明的类型有一个 @Transactional 注解的连接点 (在Spring AOP中只是方法执行):
@within(org.springframework.transaction.annotation.Transactional)
'@within'在绑定表单中更加常用:- 请参见后面的通知一节中了解如何使得注解对象在通知体内可用。
任何一个执行的方法有一个 @Transactional 注解的连接点 (在Spring AOP中只是方法执行)
@annotation(org.springframework.transaction.annotation.Transactional)
'@annotation'在绑定表单中更加常用:- 请参见后面的通知一节中了解如何使得注解对象在通知体内可用。
任何一个只接受一个参数,并且运行时所传入的参数类型具有@Classified 注解的连接点(在Spring AOP中只是方法执行)
@args(com.xyz.security.Classified)
'@args'在绑定表单中更加常用:- 请参见后面的通知一节中了解如何使得注解对象在通知体内可用。
任何一个在名为'tradeService'的Spring bean之上的连接点 (在Spring AOP中只是方法执行):
bean(tradeService)
任何一个在名字匹配通配符表达式'*Service'的Spring bean之上的连接点 (在Spring AOP中只是方法执行):
bean(*Service)
6、引入(Introduction)
引入(在AspectJ中被称为inter-type声明)使得一个切面可以定义被通知对象实现给定的接口, 并且可以为那些对象提供具体的实现
使用@DeclareParents注解来定义引入。这个注解用来定义匹配的类型 拥有一个新的父类(所以有了这个名字)。比如,给定一个接口UsageTracked, 和接口的具体实现DefaultUsageTracked类, 接下来的切面声明了所有的service接口的实现都实现了UsageTracked接口。 (比如为了通过JMX输出统计信息)。
@Aspect public class UsageTracking { @DeclareParents(value="com.xzy.myapp.service.*+", defaultImpl=DefaultUsageTracked.class) public static UsageTracked mixin; @Before("com.xyz.myapp.SystemArchitecture.businessService() &&" + "this(usageTracked)") public void recordUsage(UsageTracked usageTracked) { usageTracked.incrementUseCount(); } }
实现的接口通过被注解的字段类型来决定。@DeclareParents注解的 value属性是一个AspectJ的类型模式:- 任何匹配类型的bean都会实现 UsageTracked接口。请注意,在上面的前置通知的例子中,service beans 可以直接用作UsageTracked接口的实现。如果需要编程式的来访问一个bean, 你可以这样写:
UsageTracked usageTracked = (UsageTracked) context.getBean("myService");
7、匹配注解
为改进切面,使之仅仅重试idempotent操作,我们可以定义一个 Idempotent注解:
@Retention(RetentionPolicy.RUNTIME) public @interface Idempotent { // marker annotation }
并且对service操作的实现进行注解。为了只重试idempotent操作,切面的修改只需要改写切入点表达式, 使得只匹配@Idempotent操作: