Spring AOP(二)——在XML中配置切面
2016-03-16 22:32
639 查看
在Spring AOP(一)中介绍了AOP的基本概念和几个术语,现在学习一下在XML中如何配置AOP。
在XML中AOP的配置元素有以下几种:
总之,个人理解AOP就是将切面的功能(通知),通过切点织入程序的执行过程中。下面是AOP术语的配置和关系图:
下面通过一个具体的栗子来学习,这个例子描述的是在一个正在表演的节目(相当于应用中一个正常的业务功能)中加入观众们的反响(相当于切面),要求在表演前观众们入座、手机关机或静音,表演中记录一下表演所用时间,表演后观众们喝彩或不满。
表演类接口
表演类的一个实现:话剧演出
观众类作为切面
aopalliance-1.0.jar
aspectjrt-1.7.4.jar
aspectjweaver-1.7.4.jar
在XML文件中加入包含关于Spring AOP命名空间的内容
ref里面对应audience是切面bean的ID,也就是观众类对应的bean。观众类的bean和话剧表演的bean分别如下:
上面的所有指示器中,execution是主要使用的,是必须的;其他的指示器都是限制所匹配的切点。另外,当Spring尝试使用除了上面的AspectJ的其他指示器时,将会抛出IllegalArgumentException异常。
id标识这个切点的id,expression里面就是上面讲到的AspectJ切点表达式语言。
该切面应用了4个不同的通知。两个
下面我们用测试类来测试AOP实现
结果
环绕通知的作用在这里就体现出来了,它可以在一个方法中实现整个逻辑,即将切点的方法内嵌到环绕通知的方法中去。
对于这个通知方法,注意它使用了ProceedingJoinPoint作为方法的入参。这个对象能让我们在通知里调用被通知方法。通知方法首先完成它需要做的事情,如果希望把控制权转给被通知的方法,我们可以调用ProceedingJoinPoint的proceed()方法。
然后我们通过测试类测试一下
输出结果:
还是通过一个实例来说明:某位魔术师自称自己继承了其师傅刘半仙的衣钵,可以未卜先知知道他人内心所想;有一个志愿者不服,说要让他猜猜自己到底想了什么。这里就将魔术师作为切面。
读心术接口
魔术师类实现读心术接口
思考者接口
志愿者类实现思考者接口
XML配置
从XML配置中我们可以看出,实现被通知方法向通知传参的关键有两点:
切点元素
通知元素
测试类
输出结果
现在来说引入,什么是引入呢?就是在不改变切入的目标类的基础上为其添加新方法或属性。我们都知道Java不是动态语言,一旦类编译完成了,我们就很难再为该类添加新的功能了,那么所谓的“添加新方法或属性”是不是扯淡呢?是也不是,因为我们无法真的实现可以虚拟一个嘛,但你不说我不说谁知道呢,这就又用到了我们牛逼轰轰的代理。如下图所示:
这里,可以让代理发布一个新的接口,当引入接口的方法被调用时,代理就将此调用委托给了实现新接口的某个其他对象,也就是Bean的实现被拆分到了多个类,而不仅仅是原来的目标切点对应的那一个类。但是在外面看来,所有的功能都是通过切点对应的目标类的Bean实现的。
辅助表演接口
助人为乐者实现辅助表演接口
XML配置
引入是通过
具体说明一下
types-matching:Performer接口所实现的子类,也就是对应的目标切点类。
implement-interface:新加入的接口类。
default-impl:新加入的接口的默认实现类。
除了使用default-impl来指定接口的实现外,还可以使用delegate-ref属性来标识:
其中“gracious”是新添加的接口的实现类对应Bean的ID:
测试类
注意一点,我们要实现的是新加入的AssistPerformer 接口的assist()方法,但是我们是通过切点的BeanID,也就是”drama“得到AssistPerformer 的实现的一个实例,这就仿佛是”drama“对应的切点Bean实现了AssistPerformer 接口一样,这就是AOP引入的神奇之处,可以为目标类添加新的方法。
输出结果
在XML中AOP的配置元素有以下几种:
AOP配置元素 | 描述 |
---|---|
<aop:config> | 顶层的AOP配置元素,大多数的<aop:*>元素必须包含在 <aop:config>元素内 |
<aop:aspect> | 定义切面 |
<aop:aspect-autoproxy> | 启用@AspectJ注解驱动的切面 |
<aop:pointcut> | 定义切点 |
<aop:advisor> | 定义AOP通知器 |
<aop:before> | 定义AOP前置通知 |
<aop:after> | 定义AOP后置通知(不管被通知的方法是否执行成功) |
<aop:after-returning> | 定义成功返回后的通知 |
<aop:after-throwing> | 定义抛出异常后的通知 |
<aop:around> | 定义AOP环绕通知 |
<aop:declare-parents> | 为被通知的对象引入额外的接口,并透明地实现 |
下面通过一个具体的栗子来学习,这个例子描述的是在一个正在表演的节目(相当于应用中一个正常的业务功能)中加入观众们的反响(相当于切面),要求在表演前观众们入座、手机关机或静音,表演中记录一下表演所用时间,表演后观众们喝彩或不满。
表演类接口
package com.springinaction.aop; public interface Performer { public void perform(); }
表演类的一个实现:话剧演出
package com.springinaction.aop; public class Drama implements Performer { @Override public void perform() { for (int i = 0; i < 1000; i++) { System.out.println("话剧正在进行中——"); } } }
观众类作为切面
package com.springinaction.aop; public class Audience { public void takeSeats() { // 节目开始之前 System.out.println("演出前——观众开始入座"); } public void turnOffCellPhones() { // 节目开始之前 System.out.println("演出前——观众关机或静音"); } public void applaud() { // 节目成功结束之后 System.out.println("成功演出很成功——观众鼓掌:啪啪啪"); } public void demandRefund() { // 节目表演失败之后 System.out.println("节目演出很失败——切!一点都不好看,我们要求退钱!"); } }
1 准备工作
引入以下几个AOP包:aopalliance-1.0.jar
aspectjrt-1.7.4.jar
aspectjweaver-1.7.4.jar
在XML文件中加入包含关于Spring AOP命名空间的内容
<?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:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd"> </beans>
2 定义切面
在XML中通过元素<aop:config>开启AOP配置,使用
<aop:aspect>配置切面,如下:
<aop:config> <aop:aspect ref="audience"> </aop:aspect> </aop:config>
ref里面对应audience是切面bean的ID,也就是观众类对应的bean。观众类的bean和话剧表演的bean分别如下:
<bean id="audience" class="com.springinaction.aop.Audience" /> <bean id="drama" class="com.springinaction.aop.Drama" />
3 定义切点
3.1 AspectJ切点表达式语言
Spring AOP中使用的是AspectJ的切点表达式语言来定义的切点,前面讲过Spring AOP是基于代理的,而AspectJ的某些切点表达式是与基于代理的AOP无关的,因此Spring AOP支持的AspectJ切点指示器仅有有限的几种,如下表所示:AspectJ指示器 | 描述 |
---|---|
execution{} | 匹配到的连接点的执行方法 如:execution(* com.springinaction.aop.Performer.perform(..)) 表达式表示当Performer的perform()方法执行时会触发通知, * 表明我们不关心方法返回值的类型, (..) 标识切点选择任意的paly()方法,无论该方法的参数是什么 |
args() | 限制连接点匹配参数为指定类型的执行方法 如:execution(* com.springinaction.aop.Thinker.thinkOfSomething(String)) and args(thoughts) 注意到 thinkOfSomething(String) 有参数类型String,后面跟了一个 args(thoughts) ,这个是 thinkOfSomething() 方法要传给通知的参数,两个AspectJ知识器用and相连接,同样也可以使用or和not,当然我们可以使用&&、 || 和 ! 来代替and、or和not |
@args() | 限制连接点匹配参数由制定注解标注的执行方法 |
this() | 限制连接点匹配AOP代理的Bean引用为指定类型的类 |
target() | 限制连接点匹配目标对象为制定类型的类 |
within() | 限制连接点匹配特定的类型 如:execution(* com.springinaction.aop.Performer.perform(..)) && within(com.springinaction.aop.*) within(com.springinaction.aop.*)表示com.springinaction.aop包下任意类的方法被调用时 |
@within() | 限制连接点匹配指定注解所标注的类型(当使用Spring AOP时,方法定义在由指定的注解所标注的类里) |
@annotation | 限制匹配带有指定注解连接点 |
bean() | 限制连接点只匹配特定的bean 如:execution(* com.springinaction.aop.Performer.perform(..)) adn bean(drama) 其中drama是一个类的beanID,这个bean是Performer接口的一个实现。加上了bean(drama)以后就只能匹配这个bean了 |
3.2 在XML中配置切点
使用元素<aop:pointcut>来配置切点,
<aop:pointcut>既可以放在
<aop:aspect>元素的作用域内,也可以放在
<aop:config>元素的作用域下,对应的切点的作用范围就不同了。具体配置如下:
<aop:pointcut id="performance" expression="execution(* com.springinaction.aop.Performer.perform(..))" />
id标识这个切点的id,expression里面就是上面讲到的AspectJ切点表达式语言。
4 定义通知
4.1 声明前置和后置通知
分别使用<aop:before>和
<aop:after-returning>以及
<aop:after-throwing>元素来声明前置通知、返回结果后置通知和抛出异常后置通知,注意这几个元素是放在
<aop:aspect>元素的作用域内。具体配置如下:
<aop:before pointcut-ref="performance" method="takeSeats"/> <aop:before pointcut-ref="performance" method="turnOffCellPhones"/> <aop:after-returning pointcut-ref="performance" method="applaud"/> <aop:after-throwing pointcut-ref="performance" method="demandRefund"/>
该切面应用了4个不同的通知。两个
<aop:before>元素定义了匹配切点的方法执行之前调用前置通知方法——audience Bean的takeSeats()和turnOffCellPhones()方法(由method属性所声明)。
<aop:after-returning>元素定义了一个返回后(after-returning)通知,在切点所匹配的方法调用之后再执行applaud()方法。同样,
<aop:after-throwing>元素定义了抛出后通知,如果所匹配的方式执行时抛出任何异常,都将调用demandRefund()方法。下图展示了通知逻辑如何编织到业务逻辑中。
下面我们用测试类来测试AOP实现
package com.springinaction.aop; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class AudienceTest { public static void main(String[] args) { ApplicationContext ctx = new ClassPathXmlApplicationContext( "com/springinaction/aop/aop.xml"); Performer drama = (Performer)ctx.getBean("drama"); drama.perform(); } }
结果
演出前——观众开始入座 演出前——观众关机或静音 话剧正在进行中—— ......(重复一千次) 成功演出很成功——观众鼓掌:啪啪啪
4.2 声明环绕通知
前面的前置通知和后置通知分别发生在业务功能的前面和后面,如果想要实现跨越业务功能的整个事件段呢?例如表演节目除了进场关机和结束了鼓掌,我们还希望观众一直关注演出,报告演出的时间。如果通过前置和后置通知来实现的话,我们可以在一个成员变量中保存时间。但是由于Audience是单例,所以会存在线程安全问题。环绕通知的作用在这里就体现出来了,它可以在一个方法中实现整个逻辑,即将切点的方法内嵌到环绕通知的方法中去。
4.2.1 编写环绕通知方法
下面是我们在观众类Audience中新添加的一个watchPerformance()方法作为AOP环绕通知。public void watchPerformance(ProceedingJoinPoint joinpoint) { try { System.out.println("演出前——观众开始入座"); System.out.println("演出前——观众关机或静音"); long start = System.currentTimeMillis(); joinpoint.proceed(); // 执行被通知的方法 long end = System.currentTimeMillis(); System.out.println("成功演出很成功——观众鼓掌:啪啪啪"); System.out.println("演出持续了 " + (end - start) + " milliseconds"); } catch (Throwable e) { System.out.println("节目演出很失败——切!一点都不好看,我们要求退钱!"); } }
对于这个通知方法,注意它使用了ProceedingJoinPoint作为方法的入参。这个对象能让我们在通知里调用被通知方法。通知方法首先完成它需要做的事情,如果希望把控制权转给被通知的方法,我们可以调用ProceedingJoinPoint的proceed()方法。
4.2.2 在XML声明环绕通知
声明环绕通知与声明其他类型的通知没有太大的区别,我们需要做的仅仅是使用<aop:around>元素。
<aop:around pointcut-ref="performance" method="watchPerformance"/>
然后我们通过测试类测试一下
package com.springinaction.aop; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class AudienceTest { public static void main(String[] args) { ApplicationContext ctx = new ClassPathXmlApplicationContext( "com/springinaction/aop/aop.xml"); Performer drama = (Performer)ctx.getBean("drama"); drama.perform(); } }
输出结果:
演出前——观众开始入座 演出前——观众关机或静音 话剧正在进行中—— ......(重复1000次) 演出很成功——观众鼓掌:啪啪啪 演出持续了 42 milliseconds
4.3 为通知传递参数
上面的栗子通知和被通知的方法之间虽然实现了有了先后逻辑,但是并没有参数的传递,有时候我们需要校验传递给方法的参数值,这个时候为通知传递参数就非常有用了。还是通过一个实例来说明:某位魔术师自称自己继承了其师傅刘半仙的衣钵,可以未卜先知知道他人内心所想;有一个志愿者不服,说要让他猜猜自己到底想了什么。这里就将魔术师作为切面。
读心术接口
package com.springinaction.aop; public interface MindReader { void interceptThoughts(String thoughts); String getThoughts(); }
魔术师类实现读心术接口
package com.springinaction.aop; public class Magician implements MindReader { private String thoughts; @Override public void interceptThoughts(String thoughts) { System.out.println("让我猜猜你在想什么?"); this.thoughts = thoughts; System.out.println(this.getThoughts()); } @Override public String getThoughts() { System.out.print("你在想:"); return thoughts; } }
思考者接口
package com.springinaction.aop; public interface Thinker { void thinkOfSomething(String thoughts); }
志愿者类实现思考者接口
package com.springinaction.aop; public class Volunteer implements Thinker { private String thoughts; @Override public void thinkOfSomething(String thoughts) { this.thoughts = thoughts; } public String getThoughts() { return thoughts; } }
XML配置
<aop:config> <aop:aspect ref="magician"> <aop:pointcut id="thinker" expression="execution(* com.springinaction.aop.Thinker.thinkOfSomething(String)) and args(thoughts)"/> <aop:before pointcut-ref="thinker" method="interceptThoughts" arg-names="thoughts"/> </aop:aspect> </aop:config>
从XML配置中我们可以看出,实现被通知方法向通知传参的关键有两点:
切点元素
<aop:pointcut>的expression属性中使用了AspectJ切点表达式语言中的args()指示器,在本例中是args(thoughts),传递的是thinkOfSomething(String thoughts)方法的参数。
通知元素
<aop:before>中加入了 arg-names 属性,这个属性的值就是切点传过来的对应的参数thoughts。
测试类
package com.springinaction.aop; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class MagicianTest { public static void main(String[] args) { ApplicationContext ctx = new ClassPathXmlApplicationContext( "com/springinaction/aop/aop.xml"); Thinker thinker = (Thinker) ctx.getBean("volunteer"); thinker.thinkOfSomething("你猜我在想什么?"); } }
输出结果
让我猜猜你在想什么? 你在想:你猜我在想什么?
5 定义引入
5.1 通过切面引入的原理
讲解引入之前先思考一下我们之前都做到了什么。我们已经通过AOP实现了为目标对象拥有的方法添加了新的功能,这是通过代理实现的,即切面为目标bean创建一个代理,通知发出的方法调用看似发往了目标bean,其实是被代理拦截了,即代理代替目标bean实现原有功能并添加了切面的新功能。现在来说引入,什么是引入呢?就是在不改变切入的目标类的基础上为其添加新方法或属性。我们都知道Java不是动态语言,一旦类编译完成了,我们就很难再为该类添加新的功能了,那么所谓的“添加新方法或属性”是不是扯淡呢?是也不是,因为我们无法真的实现可以虚拟一个嘛,但你不说我不说谁知道呢,这就又用到了我们牛逼轰轰的代理。如下图所示:
这里,可以让代理发布一个新的接口,当引入接口的方法被调用时,代理就将此调用委托给了实现新接口的某个其他对象,也就是Bean的实现被拆分到了多个类,而不仅仅是原来的目标切点对应的那一个类。但是在外面看来,所有的功能都是通过切点对应的目标类的Bean实现的。
5.2 XML配置引入
还是通过一个实例来说明:前面的例子中表演类是一个切点,这里我们加入一个新的辅助表演类,来引入我们想要添加给表演类的方法。辅助表演接口
package com.springinaction.aop; public interface AssistPerformer { void assist(); }
助人为乐者实现辅助表演接口
package com.springinaction.aop; public class GraciousAssist implements AssistPerformer { @Override public void assist() { System.out.println("GraciousAssist来助演了——"); } }
XML配置
引入是通过
<aop:declare-parents>元素来实现的,这个元素在
<aop:aspect>元素的作用域内,它声明了此切面所通知的Bean在它的对象层次结构中拥有新的父类,也就是它的代理拥有了新的父类。具体配置如下:
<aop:declare-parents types-matching="com.springinaction.aop.Performer+" implement-interface="com.springinaction.aop.AssistPerformer" default-impl="com.springinaction.aop.GraciousAssist"/>
具体说明一下
<aop:declare-parents>的属性:
types-matching:Performer接口所实现的子类,也就是对应的目标切点类。
implement-interface:新加入的接口类。
default-impl:新加入的接口的默认实现类。
除了使用default-impl来指定接口的实现外,还可以使用delegate-ref属性来标识:
<aop:declare-parents types-matching="com.springinaction.aop.Performer+" implement-interface="com.springinaction.aop.AssistPerformer" delegate-ref="gracious"/>
其中“gracious”是新添加的接口的实现类对应Bean的ID:
<bean id="gracious" class="com.springinaction.aop.GraciousAssist" />
测试类
package com.springinaction.aop; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class AudienceTest { public static void main(String[] args) { ApplicationContext ctx = new ClassPathXmlApplicationContext( "com/springinaction/aop/aop.xml"); Performer drama = (Performer)ctx.getBean("drama"); drama.perform(); AssistPerformer ap = (AssistPerformer) ctx.getBean("drama"); ap.assist(); } }
注意一点,我们要实现的是新加入的AssistPerformer 接口的assist()方法,但是我们是通过切点的BeanID,也就是”drama“得到AssistPerformer 的实现的一个实例,这就仿佛是”drama“对应的切点Bean实现了AssistPerformer 接口一样,这就是AOP引入的神奇之处,可以为目标类添加新的方法。
输出结果
演出前——观众开始入座 演出前——观众关机或静音 话剧正在进行中—— ......(重复1000次) 演出很成功——观众鼓掌:啪啪啪 演出持续了 42 milliseconds
GraciousAssist来助演了——
相关文章推荐
- java中时间24小时和12小时设置
- 二进制 源码 反码 补码 只需存在 补码
- java如何利用反射机制调用类的私有方法
- [置顶] eclipse android程序运行报错:Conversion to Dalvik format failed: Unable to execute dex:
- 运行Eclipse出错:Failed to load the JNI shared library
- spring笔记--事务管理之声明式事务
- Java 多线程(六)——进程间通信与线程间通信
- Struts1的实现原理
- JAVA中的反射机制
- Java多线程安全问题及解决方案
- Java内存溢出与内存泄漏
- Java 多线程(五)——线程通信(共享内存、管道流、wait()、notify()等)
- JSP Servlet JavaBean 三者的联系和分工
- java泛型
- SpringMvc环境搭建
- maven 项目出现 java.lang.ClassNotFoundException: org.springframework.web.context.ContextLoaderListener
- Java之泛型——实践准则
- JAVA学习路线
- Java面向对象02-方法的签名,重载,构造方法和引用类型数组
- Spring aop报错:com.sun.proxy.$Proxyxxx cannot be cast to yyy