1、 AOP的基本概念
面向切面的的程序设计(aspect-oriented programming,AOP),是指一种程序设计泛型。该泛型以一种称为切面的语言构造为基础,切面是一种新的模块化机制。用来描述分散在对象、类,方法中的横切关注点,切面的概念源于对面向对象程序设计的
改进。
关注点:可以认为是关注的任何东西,比如一个任务模块。
横切关注点:一个组件无法完成需要的功能,需要其他组件协作完成,比如日志组件。
连接点:表示需要在程序中插入横切关注点的扩展点,连接点可能是类初始化、方法执行、方法调用、字段调用或异常处理等。spring只支持方法执行连接点。
切入点:选择一组相关的连接点,即可以认为是连接点的集合。
通知:在连接点上执行的行为,通知提供了在AOP中需要在切入点所选择的连接点处进行扩展现有行为的手段;包括前置通知、后置通知、环绕通知,在spring中通过代理实现AOP,并通过拦截器模式以环绕连接点的拦截器织入通知。
切面:横切关注点的模块化,比如上面的日志组件。可以认为是通知、引入、和切入点的组合。在spring中可以使用schema和AspectJ方式进行组织实现。
引入:也称内部类型声明,为已有类添加新的字段或方法,spring允许引入新的接口(有实现)到所有被代理的对象。
目标对象:需要被织入横切关注点的对象,即该对象是切入点选择的对象,需要被通知的对象,从而也可以被称为“通知对象”,由于spring AOP是通过代理实现的,从而这个对象是被代理对象。
代理对象:AOP框架使用代理模式创建的对象,从而在连接点钟插入通知,就是通过代理来对目标对象应用切面。在spring中AOP代理可以用JDK动态代理或CGLIB搭理实现,而通过拦截器模型应用切面。
织入:织入是一个过程,是将切面应用到目标对象从而创建出AOP代理对象的过程。织入可以在编译器,类转载期,运行期进行。
在AOP中,通过切入点选择目标对象,然后在目标对象的相应连接点处织入通知,而切入点和通知就是切面(横切关注点),而在目标对象连接点处应用切面实现的方式是通过AOP代理对象。
通知类型:
-
- 前置通知(Before Advice):
在切入点选择连接点处的方法之前执行的通知,该程序不影响正常程序执行流程(报异常除外)。
-
- 后置通知(After Advice):
在切入点选择连接点的方法只会执行的通知,具体包括
-
-
-
-
- 后置返回通知(After returning Advice):
-
-
-
在切入点选择的连接点处方法正常执行完毕时执行的通知,必须是连接点处的方法没有抛出任何异常,正常返回时才调用的通知。
-
-
-
-
- 后置异常通知(After throwing Advice):
-
-
-
在切入点选择的连接点处方法抛出异常返回时执行的通知,必须是连接点处方法抛出异常返回时才调用异常通知。
-
-
-
-
- 后置最终通知(After finally Advice):
-
-
-
在切入点选择的连接点处方法返回时执行的通知,不管是否抛异常都执行,类似于java中的finally。
-
- 环绕通知(Around Advices):
环绕通知在切入点选择的连接点处的方法所执行的通知,环绕通知可以在方法调用之前和之后自定义任何行为,并且可以决定是否执行连接点处的方法,替换返回值,抛出异常等。
2、基于schema的AOP
基于schema的AOP从spring2.x之后就通过aop命名空间来定义切面,切入点,及声明通知。
1 <aop:config> <!-AOP定义开始(有序)--> 2 <aop:pointcut/> <!-切入点定义(零个或多个)--> 3 <aop:advisor/> <!-Advisior定义--> 4 <aop:aspect> <!-切面定义开始(零个或多个,无序)--> 5 <aop:pointcut/> <!-切入点定义(零个或多个)--> 6 <aop:before/> <!-前置通知(零个或多个)--> 7 <aop:after-returning/> <!-后置返回通知(零个或多个)--> 8 <aop:after-throwing/> <!-后置异常通知(零个或多个)--> 9 <aop:after/> <!-后置最终通知(零个或多个)--> 10 <aop:around/> <!-环绕通知(零个或多个)--> 11 <aop:declare-parents/> <!-引入定义(零个或多个)--> 12 </aop:aspect> <!-切面定义结束--> 13 </aop:config> <!-AOP定义结束-->
2.1、声明切面
切面就是包含切入点和通知的对象,在spring容器中被定义为一个bean,schema方式切面需要一个切面支持bean,该bean的字段和方法提供了切面的状态和行为信息,并通过配置方式来指定切入点和通知实现。
切面使用<aop:aspect>标签指定,ref属性用来引用切面支持bean。
1 <bean id="aspectSupportBean" class=""/> 2 <aop:config> 3 <aop:aspect id="aspectId" ref="aspectSupportBean"> 4 ..... 5 </aop:aspect> 6 </aop:config>
2.2、声明切入点
切入点在spring中也是一个bean,定义方式有入下三种:
1)在<aop:config>标签下使用<aop:pointcut>声明一个切入点bean,该切入点可以被多个切面使用,对于需要共享的切入点最好使用该方式,该切入点使用id属性指定bean名字,在通知定义时使用pointcut-ref属性,通过该id引入切入点,
expression属性指定切入点表达式。
1 <aop:config> 2 <aop:pointcut id="pointcut" expression="execution(* cn.javass..*.*(..))"/> 3 <aop:aspect ref="aspectSupportBean"> 4 <aop:before pointcut-ref="pointcut" method="before"/> 5 </aop:aspect> 6 </aop:config>
2)在<aop:aspect>标签下使用<aop:pointcut>声明一个bean,该切入点可以被多个切面使用,但是一般该切入点只被该切入点使用。该切入点使用id属性指定bean名字,在通知定义时使用pointcut-ref属性通过id引入该切入点。
1 <aop:config> 2 <aop:aspect ref="aspectSupportBean"> 3 <aop:pointcut id=" pointcut" expression="execution(* cn.javass..*.*(..))"/> 4 <aop:before pointcut-ref="pointcut" method="before"/> 5 </aop:aspect> 6 </aop:config>
3)匿名切入点bean,可以在声明通知的时候指定pointcut属性指定切入点表达式,该切入点是匿名切入点,只被该通知使用。
1 <aop:config> 2 <aop:aspect ref="aspectSupportBean"> 3 <aop:after pointcut="execution(* cn.javass..*.*(..))" method="afterFinallyAdvice"/> 4 </aop:aspect> 5 </aop:config>
2.3、声明通知
基于schema的方式支持前面介绍的五种通知类型。
1)前置通知:在切入点选择的方法之前执行,通过<aop:aspect>标签下的<aop:before>标签声明
1 <aop:before pointcut="切入点表达式" pointcut-ref="切入点Bean引用" method="前置通知实现方法名" arg-names="前置通知实现方法参数列表参数名字"/>
-
-
- pointcut、pointcut-ref:二者选一,声明切入点。
- method:指定前置通知方法名,如果是多多态的需要加上参数类型,多个参数用“,”分隔,如beforeAdvice(java.lang.String)
- arg-names:指定通知方法的参数名字,多个用“,”分隔。
-
注:在class文件中没有生成变量调试信息是获取不到方法参数名字的,因此只有在类没有生成调试信息时才需要使用arg-names属性来指定参数名。
1 //目标接口 2 public interface HelloWorldService{ 3 public void sayBefore(String param); 4 } 5 //目标实现 6 public class HelloWorldServiceImpl implements HelloWorldService{ 7 @Override 8 public void sayBefore(String param) { 9 System.out.println("============say " + param); 10 } 11 } 12 //切面类 13 public class Aspect{ 14 public void beforeAdvice(String param) { 15 System.out.println("===========before advice param:" + param); 16 } 17 }
1 <bean id="helloWorldService" class="cn.javass.spring.chapter6.service.impl.HelloWorldService"/> 3 <bean id="aspect" class="cn.javass.spring.chapter6.aop.HelloWorldAspect"/> 4 <aop:config> 5 <aop:aspect ref="aspect"> 6 <aop:before pointcut="execution(* cn.javass..*.sayBefore(..)) and args(param)" method="beforeAdvice(java.lang.String)" arg-names="param"/> 7 </aop:aspect> 8 </aop:config>
1 @Test 2 public void testSchemaBeforeAdvice(){ 3 System.out.println("======================================"); 4 ApplicationContext ctx = new ClassPathXmlApplicationContext("chapter6/advice.xml"); 5 IHelloWorldService helloworldService = ctx.getBean("helloWorldService", IHelloWorldService.class); 6 helloworldService.sayBefore("before"); 7 System.out.println("======================================"); 8 }
1 =========输出结果============================ 2 ===========before advice param:before 3 ============say before 4 ==========================================
-
-
- 切入点匹配:在配置中使用“execution(* cn.javass..*.sayBefore(..))”匹配目标方法sayBefore,且使用“args(param)”匹配目标方法只有一个参数且传入的参数类型为通知实现方法中同名的参数类型。
- 通知方法定义:使用“beforeAdvice(java.lang.String)”指定前置通知实现方法,且该通知有一个参数类型为java.lang.String参数。
- 通知方法参数命名:其中使用“arg-names="param" ”指定通知实现方法参数名为param,切入点中使用“args(param)”匹配目标方法参数将字段传递给通知实现方法的同名参数。
-
2)后置返回通知:在切入点选择的方法正常返回时执行,通过<aop:aspect>标签下的<aop:after-returning>标签声明
1 <aop:after-returning pointcut="切入点表达式" pointcut-ref="切入点Bean引用" method="后置返回通知实现方法名" arg-names="后置返回通知实现方法参数列表参数名字" returning="返回值对应的后置返回通知实现方法参数名"/>
-
-
- pointcut、pointcut-ref:和前置通知同义。
- method:同前置通知同义。
- arg-names:同前置通知同义。
- returning:定义一个名字,该名字用于匹配通知实现方法的一个参数名,当目标方法正常执行返回后,将把目标方法返回值传给通知方法;returning限定了只有目标方法返回值与通知方法相应参数类型相同时才能执行后置返回通知,否则
-
不执行,对于returning对应的通知方法参数为Object类型将匹配任何目标返回值。
3)后置异常通知:在切入点选择的方法抛出异常时执行,通过<aop:aspect>标签下的<aop:after-throwing>标签声明:
1 <aop:after-throwing pointcut="切入点表达式" pointcut-ref="切入点Bean引用" method="后置异常通知实现方法名" arg-names="后置异常通知实现方法参数列表参数名字" throwing="将抛出的异常赋值给的通知实现方法参数名"/>
-
-
- pointcut、pointcut-ref:同前置通知同义。
- method:同前置通知同义。
- arg-names:同前置通知同义。
- throwing:定义一个名字,改名字用于匹配通知实现方法的一个参数名字,当目标方法抛出异常返回后,将把目标方法抛出的异常传给通知方法;throwing限定了只有目标方法抛出的异常与通知方法相应参数异常类型相同时才执行后置异
-
常通知,否则不执行,对于throwing对应的通知方法参数为Throwable类型将匹配任何异常。
4)后置最终通知:在切入点选择的方法返回时执行,不管方法是正常执行还是抛出异常都执行,通过<aop:aspect>标签下的<aop:after>标签声明。
1 <aop:after pointcut="切入点表达式" pointcut-ref="切入点Bean引用" method="后置最终通知实现方法名" arg-names="后置最终通知实现方法参数列表参数名字"/>
-
-
- pointcut、pointcut-ref:同前置通知同义。
- method:同前置通知同义。
- arg-names:同前置通知同义。
-
5)环绕通知:在切入点选择的方法处执行的通知,可通过<aop:aspect>标签下的<aop:around>标签声明。
1 <aop:around pointcut="切入点表达式" pointcut-ref="切入点Bean引用" method="后置最终通知实现方法名" arg-names="后置最终通知实现方法参数列表参数名字"/>
-
-
- pointcut、pointcut-ref:同前置通知同义。
- method:同前置通知同义。
- arg-names:同前置通知同义。
-
2.4、引入
spring允许为目标对象引入新的接口,在<aop:aspect>标签内使用<aop:declare-parents>标签进行引入,定义方式如下。
1 <aop:declare-parents types-matching="AspectJ语法类型表达式" implement-interface=引入的接口" default-impl="引入接口的默认实现" delegate-ref="引入接口的默认实现Bean引用"/>
-
-
- type-matching:匹配需要引入接口的目标对象的AspectJ语法类型表达式。
- implements-interface:定义需要引入的接口。
- defaul-impl、delegate-ref:定义引入接口的默认实现,二者选一,default-impl是接口的默认实现类的全限定名,而delegate-ref是默认实现的引用。
-
2.5、Advisor
Advisor表示只有一个通知和一个切入点的切面,Advisor使用<aop:config>标签下<aop:advisor>标签定义。
1 <aop:advisor pointcut="切入点表达式" pointcut-ref="切入点Bean引用" advice-ref="通知 API 实现引用"/>
-
-
- pointcut、pointcut-ref:二者选一指定切入点表达式。
- advice-ref:引用通知API实现实现bean。
-
1 //目标接口 2 public interface HelloWorldService{ 3 public void sayAdvisorBefore(String param); 4 } 5 //目标实现类 6 public class HelloWorldServiceImpl implements HelloWorldService{ 7 @Override 8 public void sayAdvisorBefore(String param) { 9 System.out.println("============say " + param); 10 } 11 } 12 //前置通知API 13 public class BeforeAdviceImpl implements MethodBeforeAdvice { 14 @Override 15 public void before(Method method, Object[] args, Object target) throws Throwable { 16 System.out.println("===========before advice"); 17 } 18 }
1 <bean id="beforeAdvice" class="cn.javass.spring.chapter6.aop.BeforeAdviceImpl"/> 2 <aop:advisor pointcut="execution(* cn.javass..*.sayAdvisorBefore(..))" advice-ref="beforeAdvice"/>
注:不推荐使用Advisor,除了在进行事务控制的情况下,其他情况一般不推荐使用该方式,该方式属于侵入式设计,必须实现通知API。