转自:https://blog.csdn.net/yusheng8711/article/details/21622773
Spring有两大核心,IOC和AOP。IOC在java web项目中无时无刻不在使用。然而AOP用的比较少,的确也是一般的项目用的场所不多。事务控制基本都用,但却是Spring封装的不需要我们再去实现,但Spring的AOP远不止这些,不能因为项目中没有使用,而不去学习及理解。我觉得这是作为一个java web软件开发人员必须具备的技能。业内很多将AOP应用在日志记录上,可惜我们项目没这么做,后面需要学习下。在这先把Spring AOP的基本用法,在脑子里理一边,做一次积累。
1、概念术语
在开始之前,需要理解Spring aop 的一些基本的概念术语(总结的个人理解,并非Spring官方定义):
切面(aspect):用来切插业务方法的类。
连接点(joinpoint):是切面类和业务类的连接点,其实就是封装了业务方法的一些基本属性,作为通知的参数来解析。
通知(advice):在切面类中,声明对业务方法做额外处理的方法。
切入点(pointcut):业务类中指定的方法,作为切面切入的点。其实就是指定某个方法作为切面切的地方。
目标对象(target object):被代理对象。
AOP代理(aop proxy):代理对象。
通知:
前置通知(before advice):在切入点之前执行。
后置通知(after returning advice):在切入点执行完成后,执行通知。
环绕通知(around advice):包围切入点,调用方法前后完成自定义行为。
异常通知(after throwing advice):在切入点抛出异常后,执行通知。
2、Spring AOP环境
要在项目中使用Spring AOP 则需要在项目中导入除了spring jar包之外,还有aspectjweaver.jar,aopalliance.jar ,asm.jar 和cglib.jar 。
好了,前提工作准备完成,Spring 提供了很多的实现AOP的方式,在学习过程中,循序渐进。进行Spring 接口方式,schema配置方式和注解的三种方式进行学习。好了废话不多说了,开始spring aop学习之旅:
3、方式一:AOP接口
利用Spring AOP接口实现AOP,主要是为了指定自定义通知来供spring AOP机制识别。主要接口:前置通知 MethodBeforeAdvice ,后置通知:AfterReturningAdvice,环绕通知:MethodInterceptor,异常通知:ThrowsAdvice 。见例子代码:
a、业务接口:
1 /** 2 * 代理类接口,也是业务类接口<br> 3 * 4 * 利用接口的方式,spring aop 将默认通过jdk 动态代理来实现代理类<br> 5 * 不利用接口,则spring aop 将通过cglib 来实现代理类 6 * 7 * @author yanbin 8 * 9 */ 10 public interface IBaseBusiness { 11 12 /** 13 * 用作代理的切入点方法 14 * 15 * @param obj 16 * @return 17 */ 18 public String delete(String obj); 19 20 /** 21 * 这方法不被切面切 22 * 23 * @param obj 24 * @return 25 */ 26 public String add(String obj); 27 28 /** 29 * 这方法切不切呢?可以设置 30 * 31 * @param obj 32 * @return 33 */ 34 public String modify(String obj); 35 36 }
b、业务类:
1 /** 2 * 业务类,也是目标对象 3 * 4 * @author yanbin 5 * 6 */ 7 public class BaseBusiness implements IBaseBusiness { 8 9 /** 10 * 切入点 11 */ 12 public String delete(String obj) { 13 System.out.println("==========调用切入点:" + obj + "说:你敢删除我!===========\n"); 14 return obj + ":瞄~"; 15 } 16 17 public String add(String obj) { 18 System.out.println("================这个方法不能被切。。。============== \n"); 19 return obj + ":瞄~ 嘿嘿!"; 20 } 21 22 public String modify(String obj) { 23 System.out.println("=================这个也设置加入切吧====================\n"); 24 return obj + ":瞄改瞄啊!"; 25 } 26 27 }
c、通知类:
前置通知:
1 /** 2 * 前置通知。 3 * 4 * @author yanbin 5 * 6 */ 7 public class BaseBeforeAdvice implements MethodBeforeAdvice { 8 9 /** 10 * method : 切入的方法 <br> 11 * args :切入方法的参数 <br> 12 * target :目标对象 13 */ 14 @Override 15 public void before(Method method, Object[] args, Object target) throws Throwable { 16 System.out.println("===========进入beforeAdvice()============ \n"); 17 18 System.out.print("准备在" + target + "对象上用"); 19 System.out.print(method + "方法进行对 '"); 20 System.out.print(args[0] + "'进行删除!\n\n"); 21 22 System.out.println("要进入切入点方法了 \n"); 23 } 24 25 }
后置通知:
1 /** 2 * 后置通知 3 * 4 * @author yanbin 5 * 6 */ 7 public class BaseAfterReturnAdvice implements AfterReturningAdvice { 8 9 /** 10 * returnValue :切入点执行完方法的返回值,但不能修改 <br> 11 * method :切入点方法 <br> 12 * args :切入点方法的参数数组 <br> 13 * target :目标对象 14 */ 15 @Override 16 public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable { 17 System.out.println("==========进入afterReturning()=========== \n"); 18 System.out.println("切入点方法执行完了 \n"); 19 20 System.out.print(args[0] + "在"); 21 System.out.print(target + "对象上被"); 22 System.out.print(method + "方法删除了"); 23 System.out.print("只留下:" + returnValue + "\n\n"); 24 } 25 26 }
环绕通知:
1 /** 2 * 环绕通知 3 * 4 * @author yanbin 5 * 6 */ 7 public class BaseAroundAdvice implements MethodInterceptor { 8 9 /** 10 * invocation :连接点 11 */ 12 @Override 13 public Object invoke(MethodInvocation invocation) throws Throwable { 14 System.out.println("===========进入around环绕方法!=========== \n"); 15 16 // 调用目标方法之前执行的动作 17 System.out.println("调用方法之前: 执行!\n"); 18 19 // 调用方法的参数 20 Object[] args = invocation.getArguments(); 21 // 调用的方法 22 Method method = invocation.getMethod(); 23 // 获取目标对象 24 Object target = invocation.getThis(); 25 // 执行完方法的返回值:调用proceed()方法,就会触发切入点方法执行 26 Object returnValue = invocation.proceed(); 27 28 System.out.println("===========结束进入around环绕方法!=========== \n"); 29 30 System.out.println("输出:" + args[0] + ";" + method + ";" + target + ";" + returnValue + "\n"); 31 32 System.out.println("调用方法结束:之后执行!\n"); 33 34 return returnValue; 35 } 36 37 }
异常通知:
1 /** 2 * 异常通知,接口没有包含任何方法。通知方法自定义 3 * 4 * @author yanbin 5 * 6 */ 7 public class BaseAfterThrowsAdvice implements ThrowsAdvice { 8 9 /** 10 * 通知方法,需要按照这种格式书写 11 * 12 * @param method 13 * 可选:切入的方法 14 * @param args 15 * 可选:切入的方法的参数 16 * @param target 17 * 可选:目标对象 18 * @param throwable 19 * 必填 : 异常子类,出现这个异常类的子类,则会进入这个通知。 20 */ 21 public void afterThrowing(Method method, Object[] args, Object target, Throwable throwable) { 22 System.out.println("删除出错啦"); 23 } 24 25 }
d、定义指定切点:
1 /** 2 * 定义一个切点,指定对应方法匹配。来供切面来针对方法进行处理<br> 3 * 4 * 继承NameMatchMethodPointcut类,来用方法名匹配 5 * 6 * @author yanbin 7 * 8 */ 9 public class Pointcut extends NameMatchMethodPointcut { 10 11 private static final long serialVersionUID = 3990456017285944475L; 12 13 @SuppressWarnings("rawtypes") 14 @Override 15 public boolean matches(Method method, Class targetClass) { 16 // 设置单个方法匹配 17 this.setMappedName("delete"); 18 // 设置多个方法匹配 19 String[] methods = { "delete", "modify" }; 20 21 //也可以用“ * ” 来做匹配符号 22 // this.setMappedName("get*"); 23 24 this.setMappedNames(methods); 25 26 return super.matches(method, targetClass); 27 } 28 29 }
e、配置:
1 <?xml version="1.0" encoding="UTF-8"?> 2 <beans xmlns="http://www.springframework.org/schema/beans" 3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" 4 xmlns:context="http://www.springframework.org/schema/context" 5 xmlns:aop="http://www.springframework.org/schema/aop" 6 xsi:schemaLocation=" 7 http://www.springframework.org/schema/beans 8 http://www.springframework.org/schema/beans/spring-beans-3.0.xsd 9 http://www.springframework.org/schema/context 10 http://www.springframework.org/schema/context/spring-context-3.0.xsd 11 http://www.springframework.org/schema/aop 12 http://www.springframework.org/schema/aop/spring-aop-3.0.xsd" 13 default-autowire="byName"> 14 15 <!-- ==============================利用spring自己的aop配置================================ --> 16 <!-- 声明一个业务类 --> 17 <bean id="baseBusiness" class="aop.base.BaseBusiness" /> 18 19 <!-- 声明通知类 --> 20 <bean id="baseBefore" class="aop.base.advice.BaseBeforeAdvice" /> 21 <bean id="baseAfterReturn" class="aop.base.advice.BaseAfterReturnAdvice" /> 22 <bean id="baseAfterThrows" class="aop.base.advice.BaseAfterThrowsAdvice" /> 23 <bean id="baseAround" class="aop.base.advice.BaseAroundAdvice" /> 24 25 <!-- 指定切点匹配类 --> 26 <bean id="pointcut" class="aop.base.pointcut.Pointcut" /> 27 28 <!-- 包装通知,指定切点 --> 29 <bean id="matchBeforeAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor"> 30 <property name="pointcut"> 31 <ref bean="pointcut" /> 32 </property> 33 <property name="advice"> 34 <ref bean="baseBefore" /> 35 </property> 36 </bean> 37 38 <!-- 使用ProxyFactoryBean 产生代理对象 --> 39 <bean id="businessProxy" class="org.springframework.aop.framework.ProxyFactoryBean"> 40 <!-- 代理对象所实现的接口 ,如果有接口可以这样设置 --> 41 <property name="proxyInterfaces"> 42 <value>aop.base.IBaseBusiness</value> 43 </property> 44 45 <!-- 设置目标对象 --> 46 <property name="target"> 47 <ref local="baseBusiness" /> 48 </property> 49 <!-- 代理对象所使用的拦截器 --> 50 <property name="interceptorNames"> 51 <list> 52 <value>matchBeforeAdvisor</value> 53 <value>baseAfterReturn</value> 54 <value>baseAround</value> 55 </list> 56 </property> 57 </bean> 58 </beans>
f、测试类:
1 public class Debug { 2 3 public static void main(String[] args) { 4 ApplicationContext context = new ClassPathXmlApplicationContext("aop/schema_aop.xml"); 5 IBaseBusiness business = (IBaseBusiness ) context.getBean("businessProxy"); 6 business.delete("猫"); 7 } 8 9 }
g、测试结果:运行下测试类,清晰明了。由于结果呈现太长就不贴了。
具体的代码实现可以从代码注释中很容易理解 接口方式的实现。结果也可想而知,前置方法会在切入点方法之前执行,后置会在切入点方法执行之后执行,环绕则会在切入点方法执行前执行同事方法结束也会执行对应的部分。主要是调用proceed()方法来执行切入点方法。来作为环绕通知前后方法的分水岭。然后在实现的过程中,有几点却是可以细揣摩一下的。
可以看出在xml 配置 businessProxy这个bean的时候,ProxyFactoryBean类中指定了,proxyInterfaces参数。这里我把他配置了IBaseBusiness接口。因为在项目开发过程中,往往业务类都会有对应的接口,以方便利用IOC解耦。但Spring AOP却也能支持没有接口的代理。这就是为什么需要导入cglib.jar的包了。看过spring的源码,知道在目标切入对象如果有实现接口,spring会默认走jdk动态代理来实现代理类。如果没有接口,则会通过cglib来实现代理类。
这个业务类现在有 前置通知,后置通知,环绕三个通知同时作用,可能以及更多的通知进行作用。那么这些通知的执行顺序是怎么样的?就这个例子而言,同时实现了三个通知。在例子xml中,则显示执行before通知,然后执行around的前处理,执行切点方法,再执行return处理。最后执行around的后处理。经过测试,知道spring 处理顺序是按照xml配置顺序依次处理通知,以队列的方式存放前通知,以压栈的方式存放后通知。所以是前通知依次执行,后通知到切入点执行完之后,从栈里在后进先出的形式把后通知执行。
在实现过程中发现通知执行对应目标对象的整个类中的方法,如何精确到某个方法,则需要定义一个切点匹配的方式:spring提供了方法名匹配或正则方式来匹配。然后通过DefaultPointcutAdvisor来包装通知,指定切点。