本文是一篇Spring AOP的基础知识分析文章,其中不牵扯源码分析,只包含AOP中重要概念的讲解,分析,以及Spring AOP的用法。java
Spring 从2.0版本引入了更加简单却强大的基于xml和AspectJ注解的面向切面的编程方式。在深刻了解如何用Spring 进行面向切面的编程前,咱们先了解AOP中的几个重要的基本概念,这几个概念并不是Spring特有的,而且从字面上看有些难于理解,不过我会尽可能用实例和通俗的语言来进行阐述。程序员
首先,到底什么是AOP呢,它有什么用处呢,对咱们程序员有什么好处呢,相信这是全部第一次接触AOP的开发者最想知道的几个问题。咱们不妨用一些例子(事务处理或者权限认证等)来稍做解释,一般,在一个应用中咱们会为不一样的业务模块建立不一样的服务类,而大多时候每一个服务类中都包含save/remove等业务逻辑不一样但应用逻辑相同的接口(此处业务逻辑表示不一样的业务需求,而应用逻辑表示增删改查等应用中常见的逻辑)。然而,大多数状况下咱们须要对这些方法进行事务控制,好比在全部save/remove 方法执行前开启一个事务,而后方法执行完成后提交事务。这时,若是没有AOP的支持,咱们可能就要对于每个方法都要写一大串重复的毫无兴奋点的代码(固然咱们这里暂不提动态代理,其实Spring AOP默认使用动态代理实现)。然而利用AOP咱们就能够避免这样的麻烦了,那我该怎样作呢?
web
第一步,咱们须要定义咱们的事务类,该类中包含事务的启用,提交等方法,这个类咱们称它为切面(Aspect)。
spring
第二步,切面定义好了,咱们还须要定义其中事务行为,也就是咱们须要为当前方法添加的额外行为,咱们称之为通知(或者加强)(Advice)。
编程
第三步,咱们须要经过某种定义来决定哪些方法将会被通知(即须要事物处理),咱们将这个定义秤为切入点(Pointcut),而每个被处理的方法咱们称之为链接点(Join Point)。
数组
经过以上几步,咱们即可以大体了解了这几个基本概念,切面,通知,切入点,链接点。若是还不明白,你能够这样理解,首先咱们须要肯定哪些(切入点)方法须要被处理,而后就是对这些方法(链接点)执行哪些额外的代码(通知),以及这些代码在什么时机执行(前置通知。。。), 最后封装通知,切入点的类或接口就是咱们的切面)。 另外,对于通知,咱们一般分为前置通知,后置通知,环绕通知等等,用于肯定通知在链接点执行的何种时机被调用,具体咱们下面分析。架构
这里要说明下,因为Spring AOP是使用JDK动态代理和CGLIB代理实现的,所以Spring AOP只能够对方法的执行进行拦截,若是须要拦截字段的访问或更新,则须要像AspectJ这样的AOP语言。另外Spring能够无缝的集成IOC,Spring AOP 以及AspectJ AOP。
app
上面已经提到Spring AOP提供了基于AspectJ注解和XML两种编程方式,可是这篇文章咱们只分析如何给予注解进行编程。
ide
@AspectJ 注解方式,指的是一种使用Java 注解的方式来进行AOP编程的方式,而这些注解都是AspectJ 项目中引入的,而Spring 可使用AspectJ库解释这些注解以完成切入点(@Pointcut)的解析和匹配,而且这一切都不须要依赖于AspectJ的编译器和切面编织器。源码分析
为了使用@AspectJ注解,咱们须要导入aspectjweaver.jar包,而且启用beans的自动代理,不管这些beans是否被切面拦截,换句话说,自动代理会检测被切面拦截的beans,而后为这些beans自动生成代理以完成对相应方法的加强。咱们可使用XML或者Java代码的方式启用自动代理配置。
<aop:aspectj-autoproxy/>
Java方式咱们不作额外介绍。
准备工做完成后,咱们即可以进行切面编程了:
在一个订单系统,和支付系统中,咱们都须要严格的用户身份认证以及日志记录功能,如用户下订单,浏览订单,支付前,都须要判断用户是否登陆等,而在下订单,支付完成功一般都须要进行log,或发信通知,而这些功能相对重复,咱们能够将它看作一个切面用在任何须要的地方,而不须要repeat yourself。既然需求定下了咱们开始编码。
下面是咱们的applicationContext.xml的内容,咱们使用注解的方式配置beans,并启用Spring包自动扫描,若是对这个配置,能够阅读个人其余关于spring的文章:
<context:component-scan base-package="aop"/>//包名就aop吧省事 <context:annotation-config/> <aop:aspectj-autoproxy/>
下面是咱们的OrderService 和PaymentService接口与实现:
package aop; //interfaces public interface OrderService { public void save(); public void read(); } public interface PayService { public void save(); } //implementations import org.springframework.stereotype.Component; @Component("orderService") public class OrderServiceImpl implements OrderService { @Override public void save() { System.out.println("order saved."); } @Override public void read() { System.out.println("order read."); } } @Component("payService") public class PayServiceImpl implements PayService { @Override public void save() { System.out.println("pay saved."); } }
服务接口定义完了,下面咱们须要定义咱们的切面了,下面是拦截规则,也就是切点Pointcut:
import org.aspectj.lang.annotation.Pointcut; import org.springframework.stereotype.Component; @Component public class Pointcuts { @Pointcut("execution(* aop.*.save())") public void notice(){} @Pointcut("execution(* aop.*.*())") public void securityCheck(){} }
以上定义了两个切点aop.Pointcuts.notice()和aop.Pointcuts.securityCheck(),分别指定了哪些方法须要notice拦截和securitycheck拦截。接下来就是定义对被拦截的方法执行哪些额外操做了,也就是咱们的通知。
import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.Aspect; import org.springframework.stereotype.Component; @Aspect @Component public class NoticeAspect { @AfterReturning(pointcut="aop.Pointcuts.notice()") public void notice(){ System.out.println("Users have been noticed."); } }
这个类有一个@Aspect 注解,代表该类是一个切面,其中定义的方法有一个@AfterReturning方法,该注解代表相应方法为一个后置通知,它有一个pointcut属性来引用上面定义的切面,肯定拦截哪些方法。好了,这样一个再简单不过的切面编程就完成了,咱们看下启动方法:
import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class Main { public static void main(String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext("/application.xml"); OrderService orderService = (OrderService)context.getBean("orderService"); orderService.save(); //orderService.read(); PayService payService = (PayService)context.getBean("payService"); payService.save(); } }
最终的运行结果是:
order saved. Users have been noticed. pay saved. Users have been noticed.
上面的代码能够说是最简单的AOP了吧,下面咱们就深刻的分析一下,AOP的方方面面。
Spring AOP中的切面能够当作一个常规的类,该类须要被@Aspect注解,其中能够包含切片,通知等声明,上面的例子中,咱们在切面中声明了后置通知。你能够能够将切片声明其中,具体怎样作,还看我的习惯,以及系统组织架构,业务逻辑须要而定了。
切片的做用是用来决定咱们声明的通知在何时被执行。一个切片的生命有两部分组成:1)由名称和任意参数组成的切片签名,2)切片表达式。在Spring AOP 中的注解方式中,切片的签名就是该常规方法定义的签名,如上例的
@Pointcut("execution(* aop.*.save())")//切片表达式 public void notice(){}//切片签名
注:做为切片签名的方法必须是void返回值。
Spring AOP目前仅支持部分AspectJ中定义的切片标识符, 下面即是完整的支持列表:
execution - 切片定义的主要用法,用于匹配方法链接点的执行。 within - 将链接点的匹配限定在某个特定类型中 this - 将AOP代理的类型限定在某个特定的类型中 target - 将目标对象的类型限定在某个特定的类型中 args - 限定链接点的参数为某些特定类型 @target - 限定目标对象为具备某个注解的特定类型 @args - 限定链接点的参数类型具备特定的注解 @within - 将链接点的匹配限定在某个具备特定注解的类型中 @annotation - 限定执行该链接点的对象为某个特定的类型
如下则是Spring AOP还未实现的标识符call, get, set, preinitialization, staticinitialization, initialization, handler, adviceexecution, withincode, cflow, cflowbelow, if, @this
, and @withincode
若是无心使用了这些还没支持的切片,则Spring会抛出IllegalArgumentException。
前面曾提到Spring AOP是基于代理的实现方式,所以咱们会用target表示被代理对象(也就是上例的OrderServiceImpl),用this表示代理对象(也就是上例的Spring 为拦截被代理对象所自动建立的对象,详细分析可阅读关于Java动态代理的文章)
另外,Spring 还支持bean切片,用于将链接点的匹配限定在指定的bean内。用法以下:
bean(idOrNameOfBean)
其中idOrNameOfBean能够是任意的Spring Bean的名字或者ID,而且支持*通配符,所以若是你的项目遵循好的命名规范,你能够很容易的写出强大的bean切片。
再编写切片时,咱们一般会用到一下几个小技巧:
A,切片表达式支持%%, || 以及!。
B,切片表达式支持对切片签名的引用。
C,咱们能够将经常使用的切片进行统一管理(参考上例)
public class Pointcuts { @Pointcut("execution(public * *(..))")//公共方法切片 public void publicOperation(){} @Pointcut("within(aop.service..*)") public void notice(){} @Pointcut("publicOperation() && notice()") public void noticePublic(){} }
经过以上技巧,咱们能够组合各类各样复杂的切片。另外须要注意,在引用切片签名时,须要遵循常规的Java方法的可见性约束,如同一个类型中能够访问private 切片定义。如下是一个Spring 提供的可能的企业开发中的切片组织方案:
package com.xyz.someapp; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; @Aspect public class SystemArchitecture { //用来匹配web层的切片,匹配位于web及其子包中定义的类中的方法 @Pointcut("within(com.xyz.someapp.web..*)") public void inWebLayer() {} //同上,用于匹配service层的切片 @Pointcut("within(com.xyz.someapp.service..*)") public void inServiceLayer() {} //用来匹配dao层 @Pointcut("within(com.xyz.someapp.dao..*)") public void inDataAccessLayer() {} //如下切片表达式,假设咱们的包结构为 //com.aop.app.order.service... //com.aop.app.pay.service... //该切片会匹配这些service包下的全部类的全部方法的执行 @Pointcut("execution(* com.xyz.someapp..service.*.*(..))") public void businessService() {} //匹配dao包下的全部方法的执行 @Pointcut("execution(* com.xyz.someapp.dao.*.*(..))") public void dataAccessOperation() {} }
这样你就能够在任意地方来引用这些切片定义了。
Execution 表达式
execution是最经常使用的一种切片标识符,咱们有必要分析下该切片表达式的格式:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)
有上述表达式能够看出,除了返回值类型(ret-type-pattern),名字(name-pattern),参数(param-pattern),其余部分都是可选的。其中ret-type-pattern决定了链接点的返回值,大多数状况下咱们用*通配符类匹配全部的返回值。name-pattern用来匹配方法名称,咱们一样能够用*来做为方法名的所有或部分。
对于param-pattern来讲:()匹配没有参数的方法,(..)匹配任意数量参数的方法,(*)匹配有一个任意参数的方法,(*, String)匹配两个参数的方法,第一个参数为任意类型,第二个参数为String类型。如下是一些经常使用的切片表达式:
execution(public * *(..)) execution(* set*(..)) execution(* com.xyz.service.AccountService.*(..)) execution(* com.xyz.service.*.*(..)) execution(* com.xyz.service..*.*(..)) within(com.xyz.service.*) within(com.xyz.service..*) this(com.xyz.service.AccountService) target(com.xyz.service.AccountService) args(java.io.Serializable) @target(org.springframework.transaction.annotation.Transactional) @within(org.springframework.transaction.annotation.Transactional) bean(*Service)
咱们不一一分析了,相信你能理解这些表达式的意思。
明白了切片,咱们再来了解下通知。一般,通知是须要跟切片结合在一块儿使用,而且会在与切片匹配的方法的先后被执行。而此处的切片既能够是对其它切片的简单引用(经过切片签名),亦能够是一个切片表达式。
上面已经说过,Spring中支持前置通知,环绕通知,AfterReturning通知,After通知,异常抛出通知,下面咱们逐个介绍。
@Aspect public class BeforeAdvice{ @Before("aop.Pointcuts.notice()")//reference to another pointcut definition. public void before(){ //... } @Before("execution(* aop.OrderService.*())") public void before1(){ //... } }
上面的两种前置通知定义方式都是可行的,before与before1两个方法会在匹配的链接点的执行以前被执行。
@Aspect public class AfterReturningExample { @AfterReturning("com.xyz.myapp.SystemArchitecture.dataAccessOperation()") public void doAccessCheck() { // ... } @AfterReturning( pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()", returning="retVal") public void doAccessCheck(Object retVal) { // ... } }
上面是AfterReturning通知的两个实例,其中第二个实例中,咱们能够经过@AfterReturning注解中的returning属性来访问链接点方法执行后返回的结果。
@Aspect public class AfterThrowingExample { @AfterThrowing("com.xyz.myapp.SystemArchitecture.dataAccessOperation()") public void doRecoveryActions() { // ... } @AfterThrowing( pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()", throwing="ex") public void doRecoveryActions(DataAccessException ex) { // ... } }
AfterThrowing 通知会在匹配的链接点方法中抛出异常后执行,而且你能够像第二个实例中那样来捕获链接点中抛出的异常实例。
@Aspect public class AfterFinallyExample { @After("com.xyz.myapp.SystemArchitecture.dataAccessOperation()") public void doReleaseLock() { // ... } }
After 通知不管链接点方法的执行结果如何都会获得执行。
环绕通知顾名思义,会在链接点的先后都被执行,而且能够决定链接点是否被执行,一般环绕通知会被用在链接点执行的先后须要共享数据的场景中。可是Spring建议咱们不要一味的使用Around 通知,而是使用能知足你需求的最简单的通知类型。好比Before ,AfterReturning等。
Around 通知经过@Advice注解声明,而且通知方法的第一个参数必须是ProceedingJoinPoint。而后在方法体内调用该实例的proceed()来调用链接点方法,proceed()也可接受Object[]参数,其中每一个元素都做为链接点方法的参数。
@Aspect public class AroundExample { @Around("com.xyz.myapp.SystemArchitecture.businessService()") public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable { // start stopwatch Object retVal = pjp.proceed(); // stop stopwatch return retVal; } }
前面提到在AfterReturning通知和AfterThrowing通知中均可以经过参数来访问到返回值或异常实例等,然而有的时候咱们可能须要在通知方法中访问链接点方法中的变量,好比咱们须要拦截一个含有user参数的方法,而且但愿在通知也操做该user实例,那么咱们能够这样作:
@Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(user,..)") public void validate(User user) { // ...} //或者 @Pointcut("com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(user,..)") private void accountDataAccessOperation(User user) {} @Before("accountDataAccessOperation(user)") public void validate(User user) { // ...}
args(user, ...)是切片表达式的一部分,它定义了链接点至少应该有一个参数,而且应该是User类型的,另外它可使得该user实例做为通知方法的参数来使用。
文章到这也就基本结束了,Spring AOP中的经常使用概念也基本分析了,固然还有不少没有提到,不过哪些已经超出本文的定位了,另外以上内容应该也能够应对平常开发工做中的大部分需求了。
欢迎讨论,拍砖