使用Spring进行面向切面(AOP)编程

1.aop理论知识

横切性关注点:对哪些方法拦截,拦截后怎么处理,这些关注就称之为横切性关注点.

Aspect(切面):指横切性关注点的抽象即为切面,它与类相似,只是两者的关注点不一样,类是对物体特征的抽象,而切面是横切性关注点的抽象。
Joinpoint(连接点):所谓连接点是指那些被拦截到的点。在Spring中,这些点指的是方法,因为Spring只支持方法类型的连接点,实际上joinpoint还可以是field或类构造器。
Pointcut(切入点):所谓切入点是指我们要对那些joinpoin进行拦截的定义。
Advice(通知):所谓通知是指拦截到joinpoint之后所要做的事情就是通知。通知分为前置通知,后置通知,异常通知,最终通知,环绕通知。
Target(目标对象):代理的目标对象
Weave(织入):指将aspects应用到target对象并导致proxy对象创建的过程称为织入。
Introduction(引入):在不修改类代码的前提下,Introduction可以在运行期为类动态地添加一些方法或Field.

 

     要进行AOP编程,首先我们要在Spring的配置文件中引入aop命名空间,如下面文件中的红色部分所示,配置文件的内容如下:
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    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/context           http://www.springframework.org/schema/context/spring-context-2.5.xsd
           http://www.springframework.org/schema/aop           http://www.springframework.org/schema/aop/spring-aop-2.5.xsd ">
    <aop:aspectj-autoproxy /><!-- 启动对@Aspectj注解的支持-->
</beans>

Spring提供了两种切面使用方式,实际工作中我们可以选用其中任何一种:

1.使用XML配置方式进行AOP开发

2. 基于注解方式进行AOP开发

首先,必须在配置文件中beans的节点下有如下配置信息:<aop:aspectj-autoproxy/> -- 启动对@Aspectj注解的支持

 

下面通过实例来讲解基于spring的AOP实现:

1.导入spring开发的基本包(包括切面及注解包)

2.定义PersonService接口及bean类PersonServiceBean,如下所示:

package cn.itcast.service;

public interface PersonService {
	public int save(String name);
	public void update(String name, Integer id);
	public String getPersonName(Integer id);
}
 
package cn.itcast.service.impl;

import cn.itcast.service.PersonService;

public class PersonServiceBean implements PersonService{

	public String getPersonName(Integer id) {
		System.out.println("我是getPersonName()方法");
		return "xxx";
	}

	public int save(String name) {
	//throw new RuntimeException("我爱例外");
		//int i = 10/0;
		System.out.println("我是save()方法");
             return 0;
	}

	public void update(String name, Integer id) {
		System.out.println("我是update()方法");
	}
}

 3.编写切面类MyInterceptor,代码如下:

package cn.itcast.service;

/**
 * 切面
 *
 */
@Aspect  //声明一个切面
public class MyInterceptor {
	@Pointcut("execution (* cn.itcast.service.impl.PersonServiceBean.*(..))")
	private void anyMethod() {}//声明一个切入点
	
	@Before("anyMethod() && args(name)")//定义前置通知,拦截的方法不但要满足声明的切入点的条件,而且要有一个String类型的输入参数,否则不会拦截
	public void doAccessCheck(String name) {
		System.out.println("前置通知:"+ name);
	}
	
	@AfterReturning(pointcut="anyMethod()",returning="result") //定义后置通知,拦截的方法的返回值必须是int类型的才能拦截
	public void doAfterReturning(int result) {
		System.out.println("后置通知:"+ result);
	}
	
@AfterThrowing(pointcut="anyMethod()",throwing="e") //定义例外通知
	public void doAfterThrowing(Exception e) {
		System.out.println("例外通知:"+ e);
	}

	@After("anyMethod()") //定义最终通知
	public void doAfter() {
		System.out.println("最终通知");
	}
	
	@Around("anyMethod()") //定义环绕通知
	public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
		//if(){//判断用户是否在权限
		System.out.println("进入方法");
		Object result = pjp.proceed();//当使用环绕通知时,这个方法必须调用,否则拦截到的方法就不会再执行了
		System.out.println("退出方法");
		//}
		return result;
	}
	
}

   说明:

       1. //* cn.itcast.service..*.*(..))第一个*代表返回的类型可以是任意的,service后面的..代表包service以及下面的所有子包,*代表这些包下面的所有类;.*代表所有这些类下面的所有方法;(..)代表这些方法的输入参数可以是0或多个.

      2.在前置通知的方法中有一个参数,然后再把此参数作为拦截条件(即是说拦截带有一个String类型参数的方法)。args的名字和beforeAdd 方法参数名字相同。

      3. afterReturningRes方法的参数就是要返回的参数类型,returning标记的就是的结果,它的取值与该方法参数名相同。

      4. 环绕通知要注意的是方法的参数及抛出异常类型的固定写法(方法名可以是任意得),另在该方法中必须执行pjp.proceed()才能让环绕通知中的两处打印代码得以执行。即是说要想环绕通知的拦截处理代码起作用必须调用pjp.proceed方法。 补充:环绕通知通常可以用来测试方法的执行时间,在pjp.proceed前获取一个时间,在pjp.proceed方法后再获取一个时间。最后两个时间相减即可得方法执行时间。

 

上面基于注解的AOP换成基于XML的方式如下所示:

<bean id="aspetbean" class="cn.itcast.service.MyInterceptor" />
	<!--配置aop -->
	<aop:config>
		<aop:aspect id="asp" ref="aspetbean"><!--声明一个切面类-->
			<!--定义切入点  -->
			<aop:pointcut id="mycut"
				expression="execution(* cn.itcast.service.impl.PersonServiceBean.*(..)) " />
			<!-- pointcut-ref :切入点引用-->
			<!--<aop:before pointcut-ref="mycut" method="doAccessCheck" arg-names="name" />-->
			<aop:after-returning pointcut-ref="mycut"
				method="doAfterReturning" returning="result" />
			<aop:after-throwing pointcut-ref="mycut"
				method="doAfterThrowing" throwing="e" />
			<aop:after pointcut-ref="mycut" method="doAfter" />
			<aop:around pointcut-ref="mycut" method="doBasicProfiling" />
		</aop:aspect>
	</aop:config>

     当基于xml方式时,遇到一个未解决问题:不能成功传参给前置通知。当我定义一个前置通知如下所示时:

<aop:before pointcut-ref="mycut" method="doAccessCheck" arg-names="name" />

 运行测试程序,他不能成功拦截方法:

personService.save("xx");

 

     上面的这个拦截类中的拦截方法除了切入点条件外,还必须满足一些辅助条件,使用拦截更精确了,如果你不想太精确,则可以简单点如下所示:

@Aspect
public class MyInterceptor {
	@Pointcut("execution(* cn.itcast.service..*.*(..))")
	private void anyMethod() {}//声明一个切入点
	
	@Before("anyMethod())")//定义前置通知
	public void doAccessCheck() {
		System.out.println("前置通知");
	}
	
	@AfterReturning(pointcut="anyMethod()") //定义后置通知
	public void doAfterReturning() {
		System.out.println("后置通知");
	}
	
	@AfterThrowing(pointcut="anyMethod()") //定义例外通知
	public void doAfterThrowing(Exception e) {
		System.out.println("例外通知");
	}
	
	..............
	
}

  替换成xml方式为:

<bean id="aspetbean" class="cn.itcast.service.MyInterceptor"/>
        <aop:config>
        	<aop:aspect id="asp" ref="aspetbean">
        		<aop:pointcut id="mycut" expression="execution(* cn.itcast.service..*.*(..))"/>
        		<aop:before pointcut-ref="mycut" method="doAccessCheck"/>
        		<aop:after-returning pointcut-ref="mycut" method="doAfterReturning"/>
			  	<aop:after-throwing pointcut-ref="mycut" method="doAfterThrowing"/>
			  	<aop:after pointcut-ref="mycut" method="doAfter"/>
			  	<aop:around pointcut-ref="mycut" method="doBasicProfiling"/>
        	</aop:aspect>
        </aop:config>

当不能前置通知传参数时,基于xml方式配置的这个AOP也能成功拦截save()方法.

personService.save("xx");

 

 4.将这些内纳入Spring管理(要想切面类起作用,首先要把切面类纳入spring容器管理。),配置文件如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context" 
       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/context http://www.springframework.org/schema/context/spring-context-2.5.xsd
           http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd">
        <aop:aspectj-autoproxy/> 
        <bean id="myInterceptor" class="cn.itcast.service.MyInterceptor"/>
        <bean id="personService" class="cn.itcast.service.impl.PersonServiceBean"></bean>
</beans>

 5. 编写junit测试单元如下:

public class SpringAOPTest {

	@Test
	public void interceptorTest(){
		ApplicationContext cxt = new ClassPathXmlApplicationContext("beans.xml");
		PersonService personService = (PersonService)cxt.getBean("personService");
              //PersonServiceBean personService = (PersonServiceBean)cxt.getBean("personService");
		//PersonService personService = new PersonServiceBean();
		personService.save("xx");
               //personService.getPersonName(90);
	}
}

1. 执行测试程序后,控制台打印如下所示:

前置通知:xx
进入方法
我是save()方法
后置通知:0
最终通知
退出方法
从打印的结果可以看出当执行

personService.save("xx");

方法时,它有String类型的输入参数,拦截时前置通知执行了,由于它的返回类型为int所以后置通知也执行.

 

2.执行方法:

personService.getPersonName(90);

控制台打印的信息如下所示:

进入方法
我是getPersonName()方法
最终通知
退出方法

由于getPersonName()方法是Integer类型的输入参数,同时它的返回类型又是String类型的,所以前置通知和后置通知都没有执行.

 

3. 将PersonServiceBean中程序

// int i = 10/0;

前的注释去掉,重新执行

personService.save("xx");

控制台打印信息如下所示:

前置通知:xx
进入方法
例外通知:java.lang.ArithmeticException: / by zero
最终通知

    当获取代理对象并调用save方法时会抛出异常,例外通知便会得以执行。

 

4.由于开启了切面编程功能,所以当我们获取一个被切面类监控管理的bean对象—PersonServiceBean时,它实际上获取的是此对象的一个代理对象,而在spring中对代理对象的处理有如下原则:(1)如果要代理的对象实现了接口,则会按照Proxy的方式来产生代理对象,这即是说产生的代理对象只能是接口类型,比如起用上面注掉的代码

 //PersonServiceBean personService = (PersonServiceBean)cxt.getBean("personService");

就会报错,报错信息如下所示:java.lang.ClassCastException: $Proxy12 cannot be cast to cn.itcast.service.impl.PersonServiceBean.

(2)要代理的对象未实现接口,则按cglib方式来产生代理对象。让PersonServiceBean不再实现PersonService接口。这时只能执行

 //PersonServiceBean personService = (PersonServiceBean)cxt.getBean("personService");

(3)另外还要注意:要想spring的切面技术起作用,被管理的bean对象只能是通过spring容器获取的对象。比如这里如果直接new出PersonServiceBean对象,则new出的对象是不能被spring的切面类监控管理。如果执行测试:

PersonService personService = new PersonServiceBean();
		personService.save("xx");

 则不会拦截save()方法,save()方法正常执行,控制台打印信息如下所示:我是save()方法.

 

小结:(1)声明aspect的切面类要纳入spring容器管理才能起作用。(2)被管理的bean实例要通过容器的getBeans方法获取。 (3)依据被管理的bean是否实现接口,spring采取两种方式来产生代理对象。(4)在xml文件中启用<aop:aspectj-autoproxy/>

 

有疑问的地方:

如果我在MyInterceptor类中调换后置通知我最终通知的定义顺序,如下所示:

@After("anyMethod()") //定义最终通知
	public void doAfter() {
		System.out.println("最终通知");
	}
	
	@AfterReturning(pointcut="anyMethod()",returning="result") //定义后置通知
	public void doAfterReturning(int result) {
		System.out.println("后置通知:"+ result);
	}

 则控制台打倒信息如下所示:

前置通知:xx
进入方法
我是save()方法
最终通知
后置通知:0
退出方法

 

则最终通知与后置通知的执行顺序与MyInterceptor类中通知定义的顺序相一致.但如果你把最终通知的定义放在MyInterceptor类的最前面,它的执行顺序也与上面的一致.

同时如果将最终通知的定义位于例外通知的前面,则再执行测试的第3步,也会出现与测试3不一致的结果,打印结果如下所示:

前置通知:xx
进入方法
最终通知
例外通知:java.lang.ArithmeticException: / by zero

执行顺序也与它们的定义顺序一致.

 

不管通知的定义顺序如何,反正前置通知会第一个执行,最终通知与后置通知和例外通知定义的顺序不一致时,他们执行的顺序也会发生改变,最终通知竟可以先于例外通知和后置通知先执行.因为在Spring里有可能在同一个AOP代理里混合通知器和通知类型。 例如,你可以在一个代理配置里使用一个拦截环绕通知,一个异常通知和一个前置通知:Spring将负责自动创建所需的拦截器链。 可能Spring创建拦截器链时与通知定义的顺序有一定关系,所以当最终通知的定义先于后置通知和例外通知时,它也将先于他俩执行.只是我的一种猜测.官方的解释如下:

下面这段话是Spring reference中的原话,不过我也没太看明白是咋回事.

通知顺序
    如果有多个通知想要在同一连接点运行会发生什么?Spring AOP遵循跟AspectJ一样的优先规则来确定通知执行的顺序。 在“进入”连接点的情况下,最高优先级的通知会先执行(所以给定的两个前置通知中,优先级高的那个会先执行)。 在“退出”连接点的情况下,最高优先级的通知会最后执行。(所以给定的两个后置通知中, 优先级高的那个会第二个执行)。
     当定义在不同的切面里的两个通知都需要在一个相同的连接点中运行, 那么除非你指定,否则执行的顺序是未知的。你可以通过指定优先级来控制执行顺序。 在标准的Spring方法中可以在切面类中实现org.springframework.core.Ordered 接口或者用Order注解做到这一点。在两个切面中, Ordered.getValue()方法返回值(或者注解值)较低的那个有更高的优先级。
     当定义在相同的切面里的两个通知都需要在一个相同的连接点中运行, 执行的顺序是未知的(因为这里没有方法通过反射javac编译的类来获取声明顺序)。 考虑在每个切面类中按连接点压缩这些通知方法到一个通知方法,或者重构通知的片段到各自的切面类中 - 它能在切面级别进行排序。

 

5.解析切入点表达式

下面给出一些通用切入点表达式的例子。

  • 任意公共方法的执行:

    execution(public * *(..))
  • 任何一个名字以“set”开始的方法的执行:

    execution(* set*(..))
  • AccountService 接口定义的任意方法的执行:

    execution(* com.xyz.service.AccountService.*(..))
  • 在service包中定义的任意方法的执行:

    execution(* com.xyz.service.*.*(..))
  • 在service包或其子包中定义的任意方法的执行:

    execution(* com.xyz.service..*.*(..))
  • 如果只拦截返回方法为String类型的,表达式应为::
    execution(java.lang.String com.xyz.service..*.*(..))
  • 如果只拦截第一个输入参数为String类型的方法,表达式应为::
    execution(* com.xyz.service..*.*(java.lang.String,..))
  • 如果只拦截非void返回类型的方法,表达式应为::
    execution(!void  com.xyz.service..*.*(..))
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值