在说AOP之前,先来看看切片面包,我们是把面包看成一个对象,而如果想在面包中间夹点番茄酱或者其他什么东西怎么办呢,是不是需要把面包切片,在每一片都要夹点东西,在编程中给对象重复性 执行某一动作是不是特别繁琐也降低了效率。今天要学习的AOP能很好的解决这种问题 。
一、AOP
AOP(Aspect-Oriented Programming,面向切面编程):是一种新的方法论,是对传统 OOP(Object-Oriented Programming,面向对象编程)的补充。
传统OOP是面向对象的编程,针对业务处理过程的实体及其属性和行为进行抽象封装,以获得更加清晰高效的逻辑单元划分。将面包看成一个对象,执行面包对象本身的业务逻辑,而在面包的每一片前后添加番茄酱这种反复横向的动作如果实现起来就导致大量代码的重复。
而AOP是将对象横向的切面看成一个处理对象,只要找到面包横向切入的点,也就是需要知道在哪里添加番茄酱,AOP自动切入,执行添加番茄酱的动作,而且对于每一片面包执行添加的动作都是一样的。这就像编程中需要给逻辑业务添加日志,现在专门找一个AOP切面类从横向方向考虑给每一个业务逻辑添加日志。AOP处理某个步骤和阶段,从中进行切面的提取,也就是说,如果几个或更多个逻辑过程中,有重复的操作行为,AOP就可以提取出来,运用动态代理,实现程序功能的统一维护。
这么一看AOP与OOP取长补短,相互补充。
这种在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程。
二、AOP的核心概念
1.横向关注点
对哪些方法进行拦截,拦截后怎么处理,这些关注点称之为横切关注点
2. 切面(Aspect)
封装横切关注点信息的类,每个关注点体现为一个通知方法。
3.连接点(joinpoint)
横切关注点在程序代码中的具体体现,对应程序执行的某个特定位置,比如方法调用的时候或者异常的时候都是连接点。spring AOP中,一个连接点总是表示一个方法的执行。例如:类某个方法调用前、调用后、方法捕获到异常后等。
4.通知(Advice)
通知指的就是指拦截到连接点之后要执行的代码,通知分为前置、后置、异常、最终、环绕通知五类;许多AOP框架(包括Spring)都是以拦截器做通知模型,并维护一个以连接点为中心的拦截器链。
5. 目标(Target)
被通知的对象,这个对象为真正对象的代理对象。
6.代理(Proxy)
AOP框架创建的对象,用来实现切面契约(例如通知方法执行等等)。
三、通知类型:
前置通知(Before advice):在某连接点之前执行的通知,但这个通知不能阻止连接点之前的执行流程(除非它抛出一个异常)。
后置通知(After returning advice):在某连接点正常完成后执行的通知:例如,一个方法没有抛出任何异常,正常返回。
异常通知(After throwing advice):在方法抛出异常退出时执行的通知。
最终通知(After (finally) advice):当某连接点退出的时候执行的通知(不论是正常返回还是异常退出)。
环绕通知(Around Advice):包围一个连接点的通知,如方法调用。这是最强大的一种通知类型。环绕通知可以在方法调用前后完成自定义的行为。它也会选择是否继续执行连接点或直接返回它自己的返回值或抛出异常来结束执行。
四、AOP原理---动态代理
参照:http://www.cnblogs.com/Actexpler-S/p/7455649.html (动态代理)
http://www.cnblogs.com/Actexpler-S/p/7469607.html (代理模式)
下图是我自己对AOP实现原理的理解
五、Spring AOP 实现
在Spring2.5.6中,常用的AOP实现的两种方法:
第一种,是基于注解方式实现的。
第二种,是基于xml配置文件方式的实现。
情景举例
①数学计算器接口[MathCalculator]
public interface MathCaculator { public int add(int i,int j); public int sub(int i,int j); public int multi(int i,int j); public int div(int i,int j); }
②提供简单实现[EasyImpl]
@Repository public class RawCaculator implements MathCaculator{ @Override public int add(int i, int j) { int result=i+j; System.out.println("add方法执行"); return result; } @Override public int sub(int i, int j) { System.out.println("sub方法执行"); return i-j; } @Override public int multi(int i, int j) { System.out.println("multi方法执行"); return i*j; } @Override public int div(int i, int j) { System.out.println("div方法执行"); return i/j; } }
③在简单实现的基础上让每一个计算方法都能够打印日志[LoginImpl]
(一)Spring中可以使用注解的方式实现AOP。
1)导入jar包
com.springsource.net.sf.cglib -2.2.0.jar com.springsource.org.aopalliance-1.0.0 .jar com.springsource.org.aspectj.weaver-1.6.8 .RELEASE.jar commons-logging-1.1.3. jar spring-aop-4.0.0.RELEASE.jar spring-aspects-4.0.0.RELEASE.jar spring-beans-4.0.0.RELEASE.jar spring-context-4.0.0.RELEASE.jar spring-core-4.0.0.RELEASE.jar spring-expression-4.0.0.RELEASE. jar
2)开启基于注解的AOP功能
< aop:aspectj-autoproxy />
3)声明一个切面类,并把这个切面类加入到IOC容器中
在切面类中声明通知方法
[1]前置通知:@Before
[2]返回通知:@AfterReturning
[3]异常通知:@AfterThrowing
[4]后置通知:@After
[5]环绕通知:@Around :环绕通知是前面四个通知的集合体!
package com.neuedu.spring.aop; import static org.hamcrest.CoreMatchers.nullValue; import java.util.Arrays; import java.util.List; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.Signature; import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.AfterThrowing; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; /* * 项目名称:spring-aop01 * @author:wzc * @date 创建时间:2017年8月29日 上午9:51:01 * @Description://表明当前类是一个切面类 * @parameter * */ @Component @Aspect @Order(value=50) public class Caculatoraspest { /* * 前置方法,可以获取目标方法名以及方法的参数, */ @Before(value="execution(public int com.neuedu.spring.aop.MathCaculator.*(..))")
public void showBeginlog(JoinPoint Point){ //getArgs 获取参数 Object[] args = Point.getArgs(); //将参数转换成list List<Object> asList=Arrays.asList(args); //getSignature获取方法的签名 Signature signatureame = Point.getSignature(); ///获取方法的名 String name=signatureame.getName(); System.out.println("[日志]【方法开始】目标方法名为:"+name +",参数为:"+asList); } @After(value="showLog()") public void showAfterLog(){ System.out.println("[日志]【方法正常结束】"); } @AfterThrowing(value="showLog()",throwing ="ex") public void showExceptionlog(JoinPoint Point,Exception ex){ System.out.println("[日志]【方法异常】"+ex.getMessage()); } /* 返回通知,可以获取方法执行的返回值 */ @AfterReturning(value="execution(public int com.neuedu.spring.aop.MathCaculator.*(..))") public void showAfter(JoinPoint Point,Object result){ System.out.println("[日志]【方法最终结束】目标方法的返回值为:"+result); } /* * 环绕通知 * * */ @Around(value="execution(public int com.neuedu.spring.aop.MathCaculator.*(..))") public Object AroundShowLog(ProceedingJoinPoint Point){ Object result=null; //getArgs 获取参数 Object[] args = Point.getArgs(); //将参数转换成list List<Object> asList=Arrays.asList(args); //getSignature获取方法的签名 Signature signatureame = Point.getSignature(); ///获取方法的名 String name=signatureame.getName(); try { try { System.out.println("[日志]【方法开始】目标方法名为:"+name +",参数为:"+asList); result=Point.proceed(args); } finally { System.out.println("[日志]【方法执行结束】"); } System.out.println("[日志]【方法最终结束】目标方法的返回值为:"+result); } catch ( Throwable e) { System.out.println("[日志]【方法异常】" +e.getMessage()); } return result; } }
测试一下
@Test public void test() { ApplicationContext ioc=new ClassPathXmlApplicationContext("applicationContext.xml"); MathCaculator bean = (MathCaculator) ioc.getBean("rawCaculator"); bean.add(10, 5); System.out.println(); bean.sub(10, 5); System.out.println(); bean.multi(10, 5); System.out.println(); bean.div(10, 5); System.out.println(); }
结果:
因为在切面类中还定义了环绕通知,所以每个方法的日志记录有两条
基于注解的切入点表达式
切入点表达式的语法格式
execution([权限修饰符] [返回值类型] [简单类名/全类名] [方法名]([参数列表])) |
最详细的切入点表达式:
execution(public int com.neuedu.aop.target.MathCalculatorImpl.add(int, int))
最模糊的切入点表达式:
execution (* *.*(..))
第一个“*”代表任意修饰符及任意返回值。
第二个“*”代表若目标类、接口与该切面类在同一个包中可以省略包名。
第三个“*”代表任意方法。
“..”匹配任意数量、任意类型的参数。
在AspectJ中,切入点表达式可以通过 “&&”、“||”、“!”等操作符结合起来。
表达式 |
execution (* *.add(int,..)) || execution(* *.sub(int,..)) |
含义 |
任意类中第一个参数为int类型的add方法或sub方法 |
统一声明切入点表达式
@Pointcut(value="execution(public int com.neuedu.spring.aop.MathCaculator.*(..))")
public void showLog(){}
package com.neuedu.spring.aop; import static org.hamcrest.CoreMatchers.nullValue; import java.util.Arrays; import java.util.List; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.Signature; import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.AfterThrowing; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; /* * 项目名称:spring-aop01 * @author:wzc * @date 创建时间:2017年8月29日 上午9:51:01 * @Description://表明当前类是一个切面类 * @parameter * */ @Component @Aspect @Order(value=50) public class Caculatoraspest { @Pointcut(value="execution(public int com.neuedu.spring.aop.MathCaculator.*(..))") public void showLog(){ } /* * 前置方法,可以获取目标方法名以及方法的参数, */ @Before(value="showLog()") public void showBeginlog(JoinPoint Point){ //getArgs 获取参数 Object[] args = Point.getArgs(); //将参数转换成list List<Object> asList=Arrays.asList(args); //getSignature获取方法的签名 Signature signatureame = Point.getSignature(); ///获取方法的名 String name=signatureame.getName(); System.out.println("[日志]【方法开始】目标方法名为:"+name +",参数为:"+asList); } @After(value="showLog()") public void showAfterLog(){ System.out.println("[日志]【方法正常结束】"); } @AfterThrowing(value="showLog()",throwing ="ex") public void showExceptionlog(JoinPoint Point,Exception ex){ System.out.println("[日志]【方法异常】"+ex.getMessage()); } /* 返回通知,可以获取方法执行的返回值 */ @AfterReturning(value="showLog()",returning="result") public void showAfter(JoinPoint Point,Object result){ System.out.println("[日志]【方法最终结束】目标方法的返回值为:"+result); } /* * 环绕通知 * * */ @Around(value="execution(public int com.neuedu.spring.aop.MathCaculator.*(..))") public Object AroundShowLog(ProceedingJoinPoint Point){ Object result=null; //getArgs 获取参数 Object[] args = Point.getArgs(); //将参数转换成list List<Object> asList=Arrays.asList(args); //getSignature获取方法的签名 Signature signatureame = Point.getSignature(); ///获取方法的名 String name=signatureame.getName(); try { try { System.out.println("[日志]【方法开始】目标方法名为:"+name +",参数为:"+asList); result=Point.proceed(args); } finally { System.out.println("[日志]【方法执行结束】"); } System.out.println("[日志]【方法最终结束】目标方法的返回值为:"+result); } catch ( Throwable e) { System.out.println("[日志]【方法异常】" +e.getMessage()); } return result; } }
通知方法的细节
①在通知中获取目标方法的方法名和参数列表
[1]在通知方法中声明一个JoinPoint类型的形参
[2]调用JoinPoint对象的getSignature()方法获取目标方法的签名
[3]调用JoinPoint对象的getArgs()方法获取目标方法的实际参数列表
②在返回通知中获取方法的返回值
[1]在@AfterReturning注解中添加returning属性
@AfterReturning (value="myPointCut()", returning= "result")
[2]在返回通知的通知方法中声明一个形参,形参名和returning属性的值一致
showReturnLog(JoinPoint joinPoint, Object result)
③在异常通知中获取异常对象
[1]在@ AfterThrowing注解中添加throwing属性
@AfterThrowing (value="myPointCut()",throwing= "throwable" )
[2]在异常通知的通知方法中声明一个形参,形参名和throwing属性值一致
showExceptinLog(JoinPoint joinPoint, Throwable throwable)
环绕通知:@Around
1.环绕通知需要在方法的参数中指定JoinPoint的子接口类型ProceedingJoinPoint为参数
@Around(value="pointCut()")
public void around(ProceedingJoinPoint joinPoint){
}
2.环绕通知会将其他4个通知能干的,自己都给干了!所以写了环绕通知就不用写其他4个通知了。
注意:@Around修饰的方法一定要将方法的返回值返回!本身相当于代理!
(二)基于XML配置的AOP
<!-- 1.将需要加载到IOC容器中的bean配置好 --> <bean id="twoCaculator" class="com.neuedu.spring.aop.TwoCaculator"></bean> <bean id="oneAspect" class="com.neuedu.spring.aop.OneAspect"></bean> <!-- 2.配置AOP,需要导入AOP名称空间 --> <aop:config> <!-- 配置切面表达式 --> <aop:pointcut expression="execution(public int com.neuedu.spring.aop.TwoCaculator.*(..))" id="Pointcut"/> <!-- 配置OneAspect切面 --> <aop:aspect ref="oneAspect"> <!-- 通过method属性指定切面类的切面方法,通过pointcut-ref指定切入点表达式 --> <aop:before method="showBeginlog" pointcut-ref="Pointcut"/> <aop:after method="showAfterLog" pointcut-ref="Pointcut"/> <aop:after-throwing method="showExceptionlog" throwing="ex" pointcut-ref="Pointcut"/> <aop:after-returning method="showAfter" pointcut-ref="Pointcut" returning="result"/> </aop:aspect> </aop:config>
切面类
package com.neuedu.spring.aop; import java.util.Arrays; import java.util.List; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.Signature; /** 项目名称:spring-aop01 * @author:wzc * @date 创建时间:2017年8月29日 下午3:49:42 * @Description: * @parameter * */ public class OneAspect { public void showBeginlog(JoinPoint Point){ //getArgs 获取参数 Object[] args = Point.getArgs(); //将参数转换成list List<Object> asList=Arrays.asList(args); //getSignature获取方法的签名 Signature signatureame = Point.getSignature(); ///获取方法的名 String name=signatureame.getName(); System.out.println("[日志]【方法开始】目标方法名为:"+name +",参数为:"+asList); } public void showAfterLog(){ System.out.println("[日志]【方法正常结束】"); } public void showExceptionlog(JoinPoint Point,Exception ex){ System.out.println("[日志]【方法异常】"+ex.getMessage()); } /* 返回通知,可以获取方法执行的返回值*/ public void showAfter(JoinPoint Point,Object result){ System.out.println("[日志]【方法最终结束】目标方法的返回值为:"+result); } }
测试一下:
@Test public void test() { ApplicationContext ioc=new ClassPathXmlApplicationContext("applicationContext.xml"); MathCaculator bean= (MathCaculator)ioc.getBean("twoCaculator"); bean.add(10, 5); System.out.println(); bean.sub(10, 5); System.out.println(); bean.multi(10, 5); System.out.println(); bean.div(10, 5); System.out.println(); }
结果:
环绕通知的XML配置和上面的四种通知的 配置一样
<aop:aspect ref="twoAspext"> <aop:around method="AroundShowLog" pointcut-ref="Pointcut"/> </aop:aspect>
六、多切面
(一)优先级
我们可以声明多个切面类,它们可以同时应用在同一个Target上,但是问题出现了,它们的执行顺序是怎么样的,Spring AOP提供了一个注解用于解决这个问题,@Order,该注解只有一个字段值,默认是整数最大值,也就是最小优先级,说明它的值越小,优先级越高。
基于配置的也可以加入order
(二)执行的顺序问题
spring aop就是一个同心圆,要执行的方法为圆心,最外层的order最小。从最外层按照AOP1、AOP2的顺序依次执行doAround方法,doBefore方法。然后执行method方法,最后按照AOP2、AOP1的顺序依次执行doAfter、doAfterReturn方法。也就是说对多个AOP来说,先before的,一定后after。