在编程中经常会遇到些许可复用、开发意义不大、技巧性的代码段,我也一直在追求一种通用性的东西,而我CSDN博客的初衷也在于此。 希望在CSDN上认识到更多的朋友,能够与大家共同学习、共同进步! ----- 伏笔的诗人
理解spring aop的路径:最初级的做法是通过使用代理将业务代码和系统代码分离,也就是在向代理类中注入业务接口实现类,然后在调用业务接口代码时调用系统代码; Java代码 //******* TimeBook.java************** import org.apache.log4j.Level; import org.apache.log4j.Logger; public class TimeBook { private Logger logger = Logger.getLogger(this.getClass().getName()); //审核数据的相关程序 public void doAuditing(String name) { logger.log(Level.INFO, name + " 开始审核数据...."); //审核数据的相关程序 …… logger.log(Level.INFO, name + " 审核数据结束...."); } } //******* TestHelloWorld.java************** package com.gc.test; import com.gc.action.TimeBook; public class TestHelloWorld { public static void main(String[] args) { TimeBook timeBook = new TimeBook(); timeBook.doAuditing("张三"); } } //******* TimeBookInterface.java************** package com.gc.impl; import org.apache.log4j.Level; //通过面向接口编程实现日志输出 public interface TimeBookInterface { public void doAuditing(String name); } //******* TimeBook.java************** package com.gc.action; import com.gc.impl.TimeBookInterface; public class TimeBook implements TimeBookInterface { public void doAuditing(String name) { //审核数据的相关程序 …… } } //******* TimeBookProxy.java************** package com.gc.action; import org.apache.log4j.Level; import org.apache.log4j.Logger; import com.gc.impl.TimeBookInterface; public class TimeBookProxy { private Logger logger = Logger.getLogger(this.getClass().getName()); private TimeBookInterface timeBookInterface; //在该类中针对前面的接口TimeBookInterface编程,而不针对具体的类 public TimeBookProxy(TimeBookInterface timeBookInterface) { this.timeBookInterface = timeBookInterface; } //实际业务处理 public void doAuditing(String name) { logger.log(Level.INFO, name + " 开始审核数据...."); timeBookInterface.doAuditing(name); //调用方法 logger.log(Level.INFO, name + " 审核数据结束...."); } } //******* TestHelloWorld.java************** package com.gc.test; import com.gc.action.TimeBook; import com.gc.action.TimeBookProxy; public class TestHelloWorld { public static void main(String[ ] args) { //这里针对接口进行编程 TimeBookProxy timeBookProxy = new TimeBookProxy(new TimeBook()); timeBookProxy .doAuditing("张三"); } } 为了更加通用, 引入java的动态代理机制来解除代理类注入的业务类必须实现指定接口的限制, 这个要将前面所说的代理类进行修改,让其实现InvocationHandler 接口, 该接口有两个方法:bind and invoke methods;在bind方法中和前面一样用来注入业务类(只不过该业务类不在是特定的类, 而是一个Object对象), 在invoke中将业务逻辑调用代码和系统代码进行混合(其中也使用了反射机制); Java代码 //******* LogProxy.java************** package com.gc.action; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import org.apache.log4j.Level; import org.apache.log4j.Logger; //代理类实现了接口InvocationHandler public class LogProxy implements InvocationHandler { private Logger logger = Logger.getLogger(this.getClass().getName()); private Object delegate; //绑定代理对象 public Object bind(Object delegate) { this.delegate = delegate; return Proxy.newProxyInstance(delegate.getClass().getClas sLoader(), delegate.getClass(). getInterfaces(), this); } //针对接口编程 public Object invoke(Object proxy, Method method, Object[ ] args) throws Throwable { Object result = null; try { //在方法调用前后进行日志输出 logger.log(Level.INFO, args[0] + " 开始审核数据...."); result = method.invoke(delegate, args); //调用绑定对象的方法 logger.log(Level.INFO, args[0] + " 审核数据结束...."); } catch (Exception e) { logger.log(Level.INFO, e.toString()); } return result; } } //******* TestHelloWorld.java************** package com.gc.test; import com.gc.action.TimeBook; import com.gc.action.TimeBookProxy; import com.gc.impl.TimeBookInterface; import com.gc.action.LogProxy; public class TestHelloWorld { public static void main(String[ ] args) { //实现了对日志类的重用 LogProxy logProxy = new LogProxy(); TimeBookInterface timeBookProxy = (TimeBookInterface)logProxy.bind(new TimeBook()); timeBookProxy.doAuditing("张三"); } } spring 的aop实现正是建立在java的动态代理机制上。要理解aop还必须理解几个概念, 第一个就是PointCut(切入点),可以将其理解为所有要进行代理的业务对象及其方法的集合(也可以理解为JoinPoint的集合,说穿了就是注入业务代码的位置, 而这个位置就是JoinPoint), 这一点可以从Spring AOP的PointCut接口定义中看出来: Java代码 package org.springframework.aop; public interface Pointcut { //用来将切入点限定在给定的目标类中 ClassFilter getClassFilter(); //用来判断切入点是否匹配目标类给定的方法 MethodMatcher getMethodMatcher(); Pointcut TRUE = TruePointcut.INSTANCE; } 跟PointCut对应的是JoinPoint(连接点),也就是插入系统代码的方法调用、异常抛出等 最后一个概念就是通知(Advice)也就是用来放系统代码的地方, 而Advisor = Advise+PointCut(这里是指的具体的位置,比如指定的方法名) 常用的Advisor是org.springframework.aop.support.RegexpMethodPointc utAdvisor 这个需要理解正则表达式的一些概念: 引用 (1)".",可以用来匹配任何一个字符。比如:正则表达式为"g.f",它就会匹配"gaf"、"g1f"、"g*f"和"g #f"等。 (2)"[]",只有[]里指定的字符才能匹配。比如:正则表达式为"g[abc]f",它就只能匹配"gaf"、"gbf"和"gcf",而不会匹配"g1f"、"g*f"和"g#f"等。 (3)"*",表示匹配次数,可以任意次,用来确定紧靠该符号左边的符号出现的次数。比如:正则表达式为"g.*f",它能匹配"gaf"、"gaaf"、"gf"和"g*f"等。 (4)"?",可以匹配0或1次,用来确定紧靠该符号左边的符号出现的次数。比如:正则表达式为"g.?f",它能匹配"gaf""g*f"等。 (5)"\",是正则表达式的连接符。比如:正则表达式为"g.\-f",它能匹配"g-f"、"ga-f"和"g*-f"等。 Xml代码 .*doAuditing.* com.gc.impl.TimeBookInterface logAdvisor spring提供了四种Advice: 第1种:在需要调用方面的方法前后都调用处理方面的代码 第2种:在需要调用方面的方法之前调用处理方面的代码 第3种:在需要调用方面的方法之后都调用处理方面的代码 第4种:在需要调用方面的方法发生异常时调用处理方面的代码 示例配置代码如下 Xml代码 com.gc.impl.TimeBookInterface log 所有的配置都一样, 只是Advice不同而已: Java代码 import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; import org.apache.log4j.Level; import org.apache.log4j.Logger; public class LogAround implements MethodInterceptor{ private Logger logger = Logger.getLogger(this.getClass().getName()); public Object invoke(MethodInvocation methodInvocation) throws Throwable { logger.log(Level.INFO, methodInvocation.getArguments()[0] + " 开始审核数据...."); try { Object result = methodInvocation.proceed(); return result; } finally { logger.log(Level.INFO, methodInvocation.getArguments()[0] + " 审核数据结束...."); } } } import java.lang.reflect.Method; import org.apache.log4j.Level; import org.apache.log4j.Logger; import org.springframework.aop.MethodBeforeAdvice; public class LogBefore implements MethodBeforeAdvice { private Logger logger = Logger.getLogger(this.getClass().getName()); public void before(Method method, Object[] args, Object target) throws Throwable { logger.log(Level.INFO, args[0] + " 开始审核数据...."); } } import java.lang.reflect.Method; import org.apache.log4j.Level; import org.apache.log4j.Logger; import org.springframework.aop.AfterReturningAdvice; public class LogAfterReturning implements AfterReturningAdvice { private Logger logger = Logger.getLogger(this.getClass().getName()); public void afterReturning(Method method, Object[] args, Object target) throws Throwable { logger.log(Level.INFO, args[0] + " 开始审核数据...."); } } import java.lang.reflect.Method; import org.apache.log4j.Level; import org.apache.log4j.Logger; import org.springframework.aop.ThrowsAdvice; public class LogThrow implements ThrowsAdvice { private Logger logger = Logger.getLogger(this.getClass().getName()); public void afterThrowing(Method method, Object[] args, Object target,Throwable subclass) throws Throwable { logger.log(Level.INFO, args[0] + " 开始审核数据...."); } } 除了使用ProxyFactoryBean来创建AOP代理外,还可以使用DefaultAdvisorAutoProxyCreator来创建自动代理, 当在配置文件中包括DefaultAdvisorAutoProxyCreator bean定义,那么在Bean定义档被读取完之后, DefaultAdvisorAutoProxyCreator会自动搜寻所有的Advisor(因为DefaultAdvisorAutoProxyCreator实现了BeanProcessor接口),并自动将Advisor应用至符合Pointcut的目标业务类上。实际上这是一个偷懒的做法, 将advisor和具体业务类的关联管理处理交给spring去处理了。 在指定业务对象的同时还需要指定业务对象所实现的接口(面向接口编程), 如果业务对象没有实现接口就需要借助cglib(这个一般是针对那些不能修改源代码的遗留系统的做法),对应的配置文件应该这样写: Xml代码 //增加如下属性,就表示使用的是CGLIB代理(对目标类直接代理) true /*然后去掉下面的属性,也就是说此种方法不需要面向接口,或不需要指出接口 com.gc.impl.TimeBookInterface */ log 在spring2.0之后, 提供了基于schema的aop配置, 与以前的配置相比,它对spring的一些aop实现细节做了进一步的屏蔽(比如代理和拦截器),对spring aop的使用者来说更简单了。 基于schema的aop借用了一些AspectJ中的一些做法, 如果对AspectJ比较熟悉的话, 使用起来是非常容易的。 首先对里面的一些aop元素进行一下说明: aop:config是aop配置中的一个顶级元素, 所有的aop的配置定义都必须包含在该元素中 aop:aspect类似于以前spring2.0以前配置中那个Advisor(但是又不完全是,因为还有一个aop:advisor元素与之对应),它包含了PointCut和Advice信息, 它会有一个对应的bean,许多advice信息也包含在里面,不过不用在象以前那样实现指定的BeforeXxx, AroundXxxx之类的接口了, 可以直接通过 org.aspectj.lang.ProceedingJoinPoint来调用指定切面对象的方法。 比如: Xml代码 ... 方法doBasicProfiling就是aBean中定义的一个方法, 它的定义是这样的: Java代码 public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable { // start stopwatch Object retVal = pjp.proceed(); // stop stopwatch return retVal; } aop:pointcut 是切入点定义,跟以前的切入点定义没有什么区别,不过象以前那种正则表达式定义放到了expression属性中, 而且使用了AspectJ的语法。它可以定义在aop:aspect中, 也可以定义在aop:config中 aop:after-returning(before, after-throwing, after, around)这个是Advice的定义, 它里面指定了要跟已定义的哪个切入点关联(pointcut-ref属性), 并且使用aspect中定义的哪个方法(method属性)。 Spring的AOP是上面代理模式的深入。使用Spring AOP,开发者无需实现业务逻辑对象工厂,无需实现代理工厂,这两个工厂都由Spring容器充当。Spring AOP不仅允许使用XML文件配置目标方法,ProxyHandler也允许使用依赖注入管理,Spring AOP提供了更多灵活的选择。 在下面Spring AOP的示例中,InvocationHandler采用动态配置,需要增加的方法也采用动态配置,一个目标对象可以有多个拦截器(类似于代理模式中的代理处理器)。 下面是原始的目标对象: //目标对象的接口 public interface Person { //该接口声明了两个方法 void info(); void run(); } 下面是原始目标对象的实现类,实现类的代码如下: //目标对象的实现类,实现类实现Person接口 public class PersonImpl implements Person { //两个成员属性 private String name; private int age; //name属性的 setter方法 public void setName(String name) { this.name = name; } //age属性的setter方法 public void setAge(int age) { this.age = age; } //info方法,该方法仅仅在控制台打印一行字符串 public void info() { System.out.println("我的名字是: " + name + " , 今年年龄为: " + age); } //run方法,该方法也在控制台打印一行字符串。 public void run() { if (age Wawa 51 正则表达式模式--> 正则表达式列表--> .*run.* lee.Person runAdvisor myAdvice myAroundInterceptor 该配置文件使用ProxyFactoryBean来生成代理对象,配置ProxyFactoryBean工厂bean时,指定了target属性,该属性值就是目标对象,该属性值为personTarget,指定代理的目标对象为personTarget。通过interceptorNames属性确定代理需要的拦截器,拦截器可以是普通的Advice,普通Advice将对目标对象的所有方法起作用,拦截器也可以是Advisor,Advisor是Advice和切面的组合,用于确定目标对象的哪些方法需要增加处理,以及怎样的处理。在上面的配置文件中,使用了三个拦截器,其中myAdvice、myAroundInterceptor都是普通Advice,它们将对目标对象的所有方法起作用。而runAdvisor则使用了正则表达式切面,匹配run方法,即该拦截器只对目标对象的run方法起作用。 下面是测试代理的主程序: public class BeanTest { public static void main(String[] args)throws Exception { //创建Spring容器 ApplicationContext ctx = new FileSystemXmlApplicationContext("bean.xml"); //获取代理对象 Person p = (Person)ctx.getBean("person"); //执行info方法 p.info(); System.out.println("============================== ============="); //执行run方法 p.run(); } } 下面是程序的执行结果: 方法调用之前... 下面是方法调用的信息: 所执行的方法是:public abstract void lee.Person.info() 调用方法的参数是:null 目标对象是:lee.PersonImpl@b23210 调用方法之前: invocation对象:[invocation: method 'info', arguments []; target is of class [lee.PersonImpl]] 我的名字是: Wawa , 今年年龄为: 51 调用结束... =========================================== 方法调用之前... 下面是方法调用的信息: 所执行的方法是:public abstract void lee.Person.run() 调用方法的参数是:null 目标对象是:lee.PersonImpl@b23210 调用方法之前: invocation对象:[invocation: method 'run', arguments [ ]; target is of class [lee.PersonImpl]] 我年老体弱,只能慢跑... 调用结束... 方法调用结束... 目标方法的返回值是 : null 目标方法是 : public abstract void lee.Person.run() 目标方法的参数是 : null 目标对象是 : lee.PersonImpl@b23210 程序的执行结果中一行"="用于区分两次调用的方法。在调用info方法时,只有myAdvice和myAroundInterceptor两个拦截器起作用,调用run方法时候,三个拦截器都起作用了。 通过上面的介绍,可看出Spring的AOP框架是对代理模式简化,并拓展了代理模式的使用。 Spring AOP是Spring声明式事务的基础。了解Spring AOP对深入理解Spring的声明式事务管理是非常有好处的。Spring AOP还可以完成很多功能,例如基于AOP的权限检查。 正式进入主题:AOP 即 Aspect Oriented Programming 的缩写,中文译为"面向切面编程"。本篇没那么学术化,只是为了快速入门了解真实的使用方式!我们不用那么早去想这个名词的意义,fellow me,用真实的代码透析AOP的含义。 不知各位有没有项目开发的经验,如果有的话你应该可以清楚地了解到我们的代码中常常充斥着大量的日志记录代码,我们用log4j等日志记录工具一段一段地记录程序运行的信息。也许有个别是十分特殊的,但是似乎大部分都是例行公事吧!或者在丢出exception的时候捕获其message然后记入日志对吗?这样的代码难道不觉得碍眼?修改起来是不是也很麻烦?要是我们换了一个日志记录工具怎么办?呵呵,别吓到了,没这么严重啦,这些都是极端情况,平时我们还是很开心地写着这些东西。但是今天要说到的这个aop可以将一些特别的操作提取出来,作为我们的"通知-advice",在运行时加载到对象中。就是说,我们可以在代码中不写日志记录段,但是在运行时加载一些"通知"到需要记录日志的"切入点-pointcut"动态地加入日志记录功能。 呼~~~怎么还是文绉绉的……说个简单的例子,来配合我们的代码:一个人,他要吃饭,当然就要吃咯,我们实现了一个Person借口,告诉有eat点方法!也写了一个该接口的实现PersonImpl.java 1 package cn.agatezone.spring.aop; 2 3 public interface Person { 4 public void eat(); 5 } 1 package cn.agatezone.spring.aop; 2 3 public class PersonImpl implements Person { 4 public void eat() { 5 System.out.println("oh! i'm eating now!"); 6 } 7 } 这个人原本是下层阶级的典型,饭前不洗手。但是有一天他变成了中层阶级的典型,要洗手再吃饭了,怎么办?重写Person借口,加入一个方法washhands()?还是在PersonImpl的实例中重写eat方法,在原本方法段前加入洗手方法调用?着不太好吧,这个还是简单的例子,要是项目大了,Person的实现多了好多,怎么办?还是一个一个重写?不用的,aop来帮助我们,不就是饭前洗手吗?我们只要实现一个MethodBeforeAdvice借口就解决这个问题啦!看看我的代码: 01 package cn.agatezone.spring.aop; 02 03 import java.lang.reflect.Method; 04 05 import org.springframework.aop.MethodBeforeAdvice; 06 07 public class BeforeEat implements MethodBeforeAdvice { 08 /* 09 * 这里的method就是被托管的bean(我们这里的Person)中的方法 10 * 这里的args是指 对应方法(这里是指Person中的任何方法)中的参数 11 */ 12 public void before(Method method, Object[] args, Object target) 13 throws Throwable { 14 if(method.getName().equals("eat")) { 15 System.out.println("before eat something i must wash my hands!"); 16 } 17 } 18 } 哦,如何使用呢?当然还是xml配置文件啦!详情看我的xml如下: 01 02 04 05 06 07 08 09 cn.agatezone.spring.aop.Person 10 11 12 13 14 15 beforeEat 16 17 18 19 20 21 22 23 现在配置好了,我们写个测试代码用用看! 01 package cn.agatezone.spring.aop; 02 03 import org.springframework.context.ApplicationContext; 04 import org.springframework.context.support.ClassPathXmlAp plicationContext; 05 06 import junit.framework.TestCase; 07 08 public class AopTest extends TestCase { 09 private static String path = 10 "cn/agatezone/spring/aop/applicationContext.xml"; 11 12 private ApplicationContext ctx; 13 14 protected void setUp() throws Exception { 15 ctx = new ClassPathXmlApplicationContext(path); 16 } 17 18 protected void tearDown() throws Exception { 19 ctx = null; 20 } 21 22 public void testAop() { 23 Person p = (Person) ctx.getBean("person"); 24 p.eat(); 25 } 26 } 运行吧!看看是不是出现了 before eat something i must wash my hands! oh! i'm eating now! 这就是一个再简单不过的aop例子了……至少我这么觉得。好让我们回味一下,什么是切入点?说白了就是一个方法嘛!处理这个方法无非在它1.运行前2.返回值的时候3.抛出异常的时候我们来针对其做"通知"就是我们这里的BeforeEat啦,但是我们例子只用了aop中的一种通知-运行前,即实现MethodBeforeAdvice这个接口的通知。还有返回值和 抛异常 的大家通知可以参照文档进行自我研究啦!本文只为Spring 的 aop 开个头,进一步的学习会在以后发布。 提供些学习资料: dev2dev 网站中有一个aop的学习文章分两部分 第一部分:http://dev2dev.bea.com.cn/techdoc/20051216709.html 第二部分:http://dev2dev.bea.com.cn/techdoc/20051223712.html 一、概念 1、连接点(join point):指程序执行过程中的一个特定点,比如方法调用、抛出异常、对象初始化等等,用来定义你的程序在什么地方加入新的逻辑。 2、通知(advice):特定的连接点出运行的代码称为通知。通知有很多种,比如前置通知、后置通知等。 3、切入点(point cut):指一个通知该何时执行的一组连接点,典型的切入点如对某个类所有方法调用的集合。 4、方面(aspect),通知和切入点的组合称为方面,也即定义了程序执行的逻辑以及何时应该被执行。 5、织入(weaving):方面被加入程序的过程,静态织入一般在编译时进行,而动态织入则在运行时进行,Spring AOP属于动态织入。 6、目标(target):也就是被aop的对象。 7、引入(introduce):就是向对象中加入新的属性或方法,比如可以修改它使之实现某个接口。 二、应用 1、使程序支持@Aspect: 在spring配置文件中加入: 2、定义一个方面: package org.xyz; import org.aspectj.lang.annotation.Aspect; @Aspect public class SimpleAspect { } 3、声明一个切入点: @Pointcut("execution(* transfer(..))") // the pointcut expression private void anyOldTransfer() {} // the pointcut signature 4、Spring AOP支持的切入点表达式: execution 匹配方法执行连接点,也是使用Spring AOP最常用到的。 within 匹配特定类型,只是为了简化特定类型执行AOP的方法执行声明。 this 连接点必须是指定类型的实例。 args 连接点的参数必须是指定类型的实例。 @target 连接点执行对象类型必须有指定类型的注解(annotation)。 @args 连接点实参的运行时类型必须有指定类型的注解(annotation)。 @within 匹配具有指定注解(annotation)的类型 @annotation 连接点必须有指定的注解(annotaion) 5、组合连接点表达式 可以用&&, ||, !进行组合 6、声明通知(advice) 1)、前置通知(Before advice) 一个切面里使用 @Before 注解声明前置通知: import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; @Aspect public class BeforeExample { @Before("com.xyz.myapp.SystemArchitecture.dataAcce ssOperation()") public void doAccessCheck() { // ... } } 如果使用一个in-place 的切入点表达式,我们可以把上面的例子换个写法: import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; @Aspect public class BeforeExample { @Before("execution(* com.xyz.myapp.dao.*.*(..))") public void doAccessCheck() { // ... } } 2)、返回后通知(After returning advice) 返回后通知通常在一个匹配的方法返回的时候执行。使用 @AfterReturning 注解来声明: import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.AfterReturning; @Aspect public class AfterReturningExample { @AfterReturning("com.xyz.myapp.SystemArchitecture. dataAccessOperation()") public void doAccessCheck() { // ... } } 说明:你可以在同一个切面里定义多个通知,或者其他成员。我们只是在展示如何定义一个简单的通知。这些例子主要的侧重点是正在讨论的问题。 有时候你需要在通知体内得到返回的值。你可以使用以 @AfterReturning 接口的形式来绑定返回值: import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.AfterReturning; @Aspect public class AfterReturningExample { @AfterReturning(pointcut="com.xyz.myapp.SystemArch itecture.dataAccessOperation()", returning="retVal") public void doAccessCheck(Object retVal) { // ... } } 在 returning 属性中使用的名字必须对应于通知方法内的一个参数名。 当一个方法执行返回后,返回值作为相应的参数值传入通知方法。 一个 returning 子句也限制了只能匹配到返回指定类型值的方法。 (在本例子中,返回值是 Object 类,也就是说返回任意类型都会匹配) 3)、抛出后通知(After throwing advice) 抛出后通知在一个方法抛出异常后执行。使用 @AfterThrowing 注解来声明: import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.AfterThrowing; @Aspect public class AfterThrowingExample { @AfterThrowing("com.xyz.myapp.SystemArchitecture.d ataAccessOperation()") public void doRecoveryActions() { // ... } } 你通常会想要限制通知只在某种特殊的异常被抛出的时候匹配,你还希望可以在通知体内得到被抛出的异常。 使用 throwing 属性不光可以限制匹配的异常类型(如果你不想限制,请使用 Throwable 作为异常类型),还可以将抛出的异常绑定到通知的一个参数上。 import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.AfterThrowing; @Aspect public class AfterThrowingExample { @AfterThrowing(pointcut="com.xyz.myapp.SystemArchi tecture.dataAccessOperation()", throwing="ex") public void doRecoveryActions(DataAccessException ex) { // ... } } 在 throwing 属性中使用的名字必须与通知方法内的一个参数对应。 当一个方法因抛出一个异常而中止后,这个异常将会作为那个对应的参数送至通知方法。 throwing 子句也限制了只能匹配到抛出指定异常类型的方法(上面的示例为 DataAccessException)。 4)、后通知(After (finally) advice) 不论一个方法是如何结束的,在它结束后(finally)后通知(After (finally) advice)都会运行。 使用 @After 注解来声明。这个通知必须做好处理正常返回和异常返回两种情况。通常用来释放资源。 import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.After; @Aspect public class AfterFinallyExample { @After("com.xyz.myapp.SystemArchitecture.dataAcces sOperation()") public void doReleaseLock() { // ... } } 5)、环绕通知(Around Advice) 最后一种通知是环绕通知。环绕通知在一个方法执行之前和之后执行。 它使得通知有机会既在一个方法执行之前又在执行之后运行。并且,它可以决定这个方法在什么时候执行,如何执行,甚至是否执行。 环绕通知经常在在某线程安全的环境下,你需要在一个方法执行之前和之后共享某种状态的时候使用。 请尽量使用最简单的满足你需求的通知。(比如如果前置通知(before advice)也可以适用的情况下不要使用环绕通知)。 环绕通知使用 @Around 注解来声明。通知的第一个参数必须是 ProceedingJoinPoint 类型。 在通知体内,调用 ProceedingJoinPoint 的 proceed() 方法将会导致潜在的连接点方法执行。 proceed 方法也可能会被调用并且传入一个 Object[] 对象-该数组将作为方法执行时候的参数。 当传入一个 Object[] 对象的时候,处理的方法与通过AspectJ编译器处理环绕通知略有不同。 对于使用传统AspectJ语言写的环绕通知来说,传入参数的数量必须和传递给环绕通知的参数数量匹配(不是后台的连接点接受的参数数量),并且特定顺序的传入参数代替了将要绑定给连接点的原始值(如果你看不懂不用担心)。 Spring采用的方法更加简单并且更好得和他的基于代理(proxy-based),只匹配执行的语法相适用。 如果你适用AspectJ的编译器和编织器来编译为Spring而写的@AspectJ切面和处理参数,你只需要了解这一区别即可。 有一种方法可以让你写出100%兼容Spring AOP和AspectJ的,我们将会在后续的通知参数(advice parameters)的章节中讨论它。 import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.ProceedingJoinPoint; @Aspect public class AroundExample { @Around("com.xyz.myapp.SystemArchitecture.business Service()") public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable { // start stopwatch Object retVal = pjp.proceed(); // stop stopwatch return retVal; } } 方法的调用者得到的返回值就是环绕通知返回的值。 例如:一个简单的缓存切面,如果缓存中有值,就返回该值,否则调用proceed()方法。 请注意proceed可能在通知体内部被调用一次,许多次,或者根本不被调用。 7、通知参数(Advice parameters) Spring 2.0 提供了完整的通知类型 这意味着你可以在通知签名中声明所需的参数,(就像在以前的例子中我们看到的返回值和抛出异常一样)而不总是使用Object[]。 我们将会看到如何在通知体内访问参数和其他上下文相关的值。首先让我们看以下如何编写普通的通知以找出正在被通知的方法。 1)、访问当前的连接点 任何通知方法可以将第一个参数定义为 org.aspectj.lang.JoinPoint 类型 (环绕通知需要定义为 ProceedingJoinPoint 类型的, 它是 JoinPoint 的一个子类。) JoinPoint 接口提供了一系列有用的方法, 比如 getArgs()(返回方法参数)、getThis()(返回代理对象)、getTarget()(返回目标)、getSignature()(返回正在被通知的方法相关信息)和 toString()(打印出正在被通知的方法的有用信息)。 2)、传递参数给通知(Advice) 我们已经看到了如何绑定返回值或者异常(使用后置通知(after returning)和异常后通知(after throwing advice)。 为了可以在通知(adivce)体内访问参数,你可以使用 args 来绑定。 如果在一个参数表达式中应该使用类型名字的地方使用一个参数名字,那么当通知执行的时候对应的参数值将会被传递进来。 可能给出一个例子会更好理解。假使你想要通知(advise)接受某个Account对象作为第一个参数的DAO操作的执行,你想要在通知体内也能访问到account对象,你可以写如下的代码: @Before("com.xyz.myapp.SystemArchitecture.dataAcce ssOperation() && args(account,..)") public void validateAccount(Account account) { // ... } 切入点表达式的 args(account,..) 部分有两个目的: 首先它保证了只会匹配那些接受至少一个参数的方法的执行,而且传入的参数必须是 Account 类型的实例, 其次它使得可以在通知体内通过 account 参数来访问那个account参数。 另外一个办法是定义一个切入点,这个切入点在匹配某个连接点的时候"提供"了一个Account对象, 然后直接从通知中访问那个命名的切入点。你可以这样写: @Pointcut("com.xyz.myapp.SystemArchitecture.dataAc cessOperation() && args(account,..)") private void accountDataAccessOperation(Account account) {} @Before("accountDataAccessOperation(account)") public void validateAccount(Account account) { // .. } 如果想要知道更详细的内容,请参阅 AspectJ 编程指南。 代理对象(this)、目标对象(target) 和注解(@within, @target, @annotation, @args)都可以用一种简单格式绑定。 以下的例子展示了如何使用 @Auditable 注解来匹配方法执行,并提取AuditCode。 首先是 @Auditable 注解的定义: @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Auditable { AuditCode value(); } 然后是匹配 @Auditable 方法执行的通知: @Before("com.xyz.lib.Pointcuts.anyPublicMethod() && @annotation(auditable)") public void audit(Auditable auditable) { AuditCode code = auditable.value(); // ... } 3)、决定参数名 绑定在通知上的参数依赖切入点表达式的匹配名,并借此在(通知(advice)和切入点(pointcut))的方法签名中声明参数名。 参数名 无法 通过Java反射来获取,所以Spring AOP使用如下的策略来决定参数名字: 如果参数名字已经被用户明确指定,则使用指定的参数名: 通知(advice)和切入点(pointcut)注解有一个额外的"argNames"属性,该属性用来指定所注解的方法的参数名 这些参数名在运行时是 可以 访问的。例子如下: @Before(value="com.xyz.lib.Pointcuts.anyPublicMeth od() && @annotation(auditable)", argNames="auditable") public void audit(Auditable auditable) { AuditCode code = auditable.value(); // ... } 如果一个@AspectJ切面已经被AspectJ编译器(ajc)编译过了,那么就不需要再添加 argNames 参数了,因为编译器会自动完成这一工作。 使用 'argNames' 属性有点不那么优雅,所以如果没有指定'argNames' 属性, Spring AOP 会寻找类的debug信息,并且尝试从本地变量表(local variable table)中来决定参数名字。 只要编译的时候使用了debug信息(至少要使用 '-g:vars' ),就可获得这些信息。 使用这个flag编译的结果是: (1)你的代码将能够更加容易的读懂(反向工程) (2)生成的class文件会稍许大一些(不重要的) (3)移除不被使用的本地变量的优化功能将会失效。 换句话说,你在使用这个flag的时候不会遇到任何困难。 如果不加上debug信息来编译的话,Spring AOP将会尝试推断参数的绑定。 (例如,要是只有一个变量被绑定到切入点表达式(pointcut expression)、通知方法(advice method)将会接受这个参数, 这是显而易见的)。 如果变量的绑定不明确,将会抛出一个 AmbiguousBindingException 异常。 如果以上所有策略都失败了,将会抛出一个 IllegalArgumentException 异常 8、常用切入点表达式 execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?) 除了返回类型模式(上面代码片断中的ret-type-pattern),名字模式和参数模式以外,所有的部分都是可选的。 返回类型模式决定了方法的返回类型必须依次匹配一个连接点。 你会使用的最频繁的返回类型模式是 *,它代表了匹配任意的返回类型。 一个全称限定的类型名将只会匹配返回给定类型的方法。名字模式匹配的是方法名。 你可以使用 * 通配符作为所有或者部分命名模式。 参数模式稍微有点复杂:() 匹配了一个不接受任何参数的方法,而 (..) 匹配了一个接受任意数量参数的方法(零或者更多)。 模式 (*) 匹配了一个接受一个任何类型的参数的方法。 模式 (*,String) 匹配了一个接受两个参数的方法,第一个可以是任意类型,第二个则必须是String类型。 下面给出一些常见切入点表达式的例子。 任意公共方法的执行: execution(public * *(..)) 任何一个以"set"开始的方法的执行: execution(* set*(..)) AccountService 接口的任意方法的执行: execution(* com.xyz.service.AccountService.*(..)) 定义在service包里的任意方法的执行: execution(* com.xyz.service.*.*(..)) 定义在service包或者子包里的任意方法的执行: execution(* com.xyz.service..*.*(..)) 在service包里的任意连接点(在Spring AOP中只是方法执行): within(com.xyz.service.*) 在service包或者子包里的任意连接点(在Spring AOP中只是方法执行): within(com.xyz.service..*) 实现了 AccountService 接口的代理对象的任意连接点(在Spring AOP中只是方法执行): this(com.xyz.service.AccountService) 'this'在binding form中用的更多:- 请常见以下讨论通知的章节中关于如何使得代理对象可以在通知体内访问到的部分。 实现了 AccountService 接口的目标对象的任意连接点(在Spring AOP中只是方法执行): target(com.xyz.service.AccountService) 任何一个只接受一个参数,且在运行时传入的参数实现了 Serializable 接口的连接点 (在Spring AOP中只是方法执行) args(java.io.Serializable) 请注意在例子中给出的切入点不同于 execution(* *(java.io.Serializable)): args只有在动态运行时候传入参数是可序列化的(Serializable)才匹配,而execution 在传入参数的签名声明的类型实现了 Serializable 接口时候匹配。 有一个 @Transactional 注解的目标对象中的任意连接点(在Spring AOP中只是方法执行) @target(org.springframework.transaction.annotation .Transactional) 任何一个目标对象声明的类型有一个 @Transactional 注解的连接点(在Spring AOP中只是方法执行) @within(org.springframework.transaction.annotation .Transactional) 任何一个执行的方法有一个 @Transactional annotation的连接点(在Spring AOP中只是方法执行) @annotation(org.springframework.transaction.annota tion.Transactional) 任何一个接受一个参数,并且传入的参数在运行时的类型实现了 @Classified annotation的连接点(在Spring AOP中只是方法执行) @args(com.xyz.security.Classified) 9、参考并抄袭自 http://static.springsource.org/spring/docs/2.5.x/r eference/aop.html http://hi.baidu.com/wangyongjin87/blog/item/c9cf2c ec4e19de232df534cb.html
理解spring aop的路径:最初级的做法是通过使用代理将业务代码和系统代码分离,也就是在向代理类中注入业务接口实现类,然后在调用业务接口代码时调用系统代码; Java代码 //******* TimeBook.java************** import org.apache.log4j.Level; import org.apache.log4j.Logger; public class TimeBook { private Logger logger = Logger.getLogger(this.getClass().getName()); //审核数据的相关程序 public void doAuditing(String name) { logger.log(Level.INFO, name + " 开始审核数据...."); //审核数据的相关程序 …… logger.log(Level.INFO, name + " 审核数据结束...."); } } //******* TestHelloWorld.java************** package com.gc.test; import com.gc.action.TimeBook; public class TestHelloWorld { public static void main(String[] args) { TimeBook timeBook = new TimeBook(); timeBook.doAuditing("张三"); } } //******* TimeBookInterface.java************** package com.gc.impl; import org.apache.log4j.Level; //通过面向接口编程实现日志输出 public interface TimeBookInterface { public void doAuditing(String name); } //******* TimeBook.java************** package com.gc.action; import com.gc.impl.TimeBookInterface; public class TimeBook implements TimeBookInterface { public void doAuditing(String name) { //审核数据的相关程序 …… } } //******* TimeBookProxy.java************** package com.gc.action; import org.apache.log4j.Level; import org.apache.log4j.Logger; import com.gc.impl.TimeBookInterface; public class TimeBookProxy { private Logger logger = Logger.getLogger(this.getClass().getName()); private TimeBookInterface timeBookInterface; //在该类中针对前面的接口TimeBookInterface编程,而不针对具体的类 public TimeBookProxy(TimeBookInterface timeBookInterface) { this.timeBookInterface = timeBookInterface; } //实际业务处理 public void doAuditing(String name) { logger.log(Level.INFO, name + " 开始审核数据...."); timeBookInterface.doAuditing(name); //调用方法 logger.log(Level.INFO, name + " 审核数据结束...."); } } //******* TestHelloWorld.java************** package com.gc.test; import com.gc.action.TimeBook; import com.gc.action.TimeBookProxy; public class TestHelloWorld { public static void main(String[ ] args) { //这里针对接口进行编程 TimeBookProxy timeBookProxy = new TimeBookProxy(new TimeBook()); timeBookProxy .doAuditing("张三"); } } 为了更加通用, 引入java的动态代理机制来解除代理类注入的业务类必须实现指定接口的限制, 这个要将前面所说的代理类进行修改,让其实现InvocationHandler 接口, 该接口有两个方法:bind and invoke methods;在bind方法中和前面一样用来注入业务类(只不过该业务类不在是特定的类, 而是一个Object对象), 在invoke中将业务逻辑调用代码和系统代码进行混合(其中也使用了反射机制); Java代码 //******* LogProxy.java************** package com.gc.action; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import org.apache.log4j.Level; import org.apache.log4j.Logger; //代理类实现了接口InvocationHandler public class LogProxy implements InvocationHandler { private Logger logger = Logger.getLogger(this.getClass().getName()); private Object delegate; //绑定代理对象 public Object bind(Object delegate) { this.delegate = delegate; return Proxy.newProxyInstance(delegate.getClass().getClas sLoader(), delegate.getClass(). getInterfaces(), this); } //针对接口编程 public Object invoke(Object proxy, Method method, Object[ ] args) throws Throwable { Object result = null; try { //在方法调用前后进行日志输出 logger.log(Level.INFO, args[0] + " 开始审核数据...."); result = method.invoke(delegate, args); //调用绑定对象的方法 logger.log(Level.INFO, args[0] + " 审核数据结束...."); } catch (Exception e) { logger.log(Level.INFO, e.toString()); } return result; } } //******* TestHelloWorld.java************** package com.gc.test; import com.gc.action.TimeBook; import com.gc.action.TimeBookProxy; import com.gc.impl.TimeBookInterface; import com.gc.action.LogProxy; public class TestHelloWorld { public static void main(String[ ] args) { //实现了对日志类的重用 LogProxy logProxy = new LogProxy(); TimeBookInterface timeBookProxy = (TimeBookInterface)logProxy.bind(new TimeBook()); timeBookProxy.doAuditing("张三"); } } spring 的aop实现正是建立在java的动态代理机制上。要理解aop还必须理解几个概念, 第一个就是PointCut(切入点),可以将其理解为所有要进行代理的业务对象及其方法的集合(也可以理解为JoinPoint的集合,说穿了就是注入业务代码的位置, 而这个位置就是JoinPoint), 这一点可以从Spring AOP的PointCut接口定义中看出来: Java代码 package org.springframework.aop; public interface Pointcut { //用来将切入点限定在给定的目标类中 ClassFilter getClassFilter(); //用来判断切入点是否匹配目标类给定的方法 MethodMatcher getMethodMatcher(); Pointcut TRUE = TruePointcut.INSTANCE; } 跟PointCut对应的是JoinPoint(连接点),也就是插入系统代码的方法调用、异常抛出等 最后一个概念就是通知(Advice)也就是用来放系统代码的地方, 而Advisor = Advise+PointCut(这里是指的具体的位置,比如指定的方法名) 常用的Advisor是org.springframework.aop.support.RegexpMethodPointc utAdvisor 这个需要理解正则表达式的一些概念: 引用 (1)".",可以用来匹配任何一个字符。比如:正则表达式为"g.f",它就会匹配"gaf"、"g1f"、"g*f"和"g #f"等。 (2)"[]",只有[]里指定的字符才能匹配。比如:正则表达式为"g[abc]f",它就只能匹配"gaf"、"gbf"和"gcf",而不会匹配"g1f"、"g*f"和"g#f"等。 (3)"*",表示匹配次数,可以任意次,用来确定紧靠该符号左边的符号出现的次数。比如:正则表达式为"g.*f",它能匹配"gaf"、"gaaf"、"gf"和"g*f"等。 (4)"?",可以匹配0或1次,用来确定紧靠该符号左边的符号出现的次数。比如:正则表达式为"g.?f",它能匹配"gaf""g*f"等。 (5)"\",是正则表达式的连接符。比如:正则表达式为"g.\-f",它能匹配"g-f"、"ga-f"和"g*-f"等。 Xml代码 .*doAuditing.* com.gc.impl.TimeBookInterface logAdvisor spring提供了四种Advice: 第1种:在需要调用方面的方法前后都调用处理方面的代码 第2种:在需要调用方面的方法之前调用处理方面的代码 第3种:在需要调用方面的方法之后都调用处理方面的代码 第4种:在需要调用方面的方法发生异常时调用处理方面的代码 示例配置代码如下 Xml代码 com.gc.impl.TimeBookInterface log 所有的配置都一样, 只是Advice不同而已: Java代码 import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; import org.apache.log4j.Level; import org.apache.log4j.Logger; public class LogAround implements MethodInterceptor{ private Logger logger = Logger.getLogger(this.getClass().getName()); public Object invoke(MethodInvocation methodInvocation) throws Throwable { logger.log(Level.INFO, methodInvocation.getArguments()[0] + " 开始审核数据...."); try { Object result = methodInvocation.proceed(); return result; } finally { logger.log(Level.INFO, methodInvocation.getArguments()[0] + " 审核数据结束...."); } } } import java.lang.reflect.Method; import org.apache.log4j.Level; import org.apache.log4j.Logger; import org.springframework.aop.MethodBeforeAdvice; public class LogBefore implements MethodBeforeAdvice { private Logger logger = Logger.getLogger(this.getClass().getName()); public void before(Method method, Object[] args, Object target) throws Throwable { logger.log(Level.INFO, args[0] + " 开始审核数据...."); } } import java.lang.reflect.Method; import org.apache.log4j.Level; import org.apache.log4j.Logger; import org.springframework.aop.AfterReturningAdvice; public class LogAfterReturning implements AfterReturningAdvice { private Logger logger = Logger.getLogger(this.getClass().getName()); public void afterReturning(Method method, Object[] args, Object target) throws Throwable { logger.log(Level.INFO, args[0] + " 开始审核数据...."); } } import java.lang.reflect.Method; import org.apache.log4j.Level; import org.apache.log4j.Logger; import org.springframework.aop.ThrowsAdvice; public class LogThrow implements ThrowsAdvice { private Logger logger = Logger.getLogger(this.getClass().getName()); public void afterThrowing(Method method, Object[] args, Object target,Throwable subclass) throws Throwable { logger.log(Level.INFO, args[0] + " 开始审核数据...."); } } 除了使用ProxyFactoryBean来创建AOP代理外,还可以使用DefaultAdvisorAutoProxyCreator来创建自动代理, 当在配置文件中包括DefaultAdvisorAutoProxyCreator bean定义,那么在Bean定义档被读取完之后, DefaultAdvisorAutoProxyCreator会自动搜寻所有的Advisor(因为DefaultAdvisorAutoProxyCreator实现了BeanProcessor接口),并自动将Advisor应用至符合Pointcut的目标业务类上。实际上这是一个偷懒的做法, 将advisor和具体业务类的关联管理处理交给spring去处理了。 在指定业务对象的同时还需要指定业务对象所实现的接口(面向接口编程), 如果业务对象没有实现接口就需要借助cglib(这个一般是针对那些不能修改源代码的遗留系统的做法),对应的配置文件应该这样写: Xml代码 //增加如下属性,就表示使用的是CGLIB代理(对目标类直接代理) true /*然后去掉下面的属性,也就是说此种方法不需要面向接口,或不需要指出接口 com.gc.impl.TimeBookInterface */ log 在spring2.0之后, 提供了基于schema的aop配置, 与以前的配置相比,它对spring的一些aop实现细节做了进一步的屏蔽(比如代理和拦截器),对spring aop的使用者来说更简单了。 基于schema的aop借用了一些AspectJ中的一些做法, 如果对AspectJ比较熟悉的话, 使用起来是非常容易的。 首先对里面的一些aop元素进行一下说明: aop:config是aop配置中的一个顶级元素, 所有的aop的配置定义都必须包含在该元素中 aop:aspect类似于以前spring2.0以前配置中那个Advisor(但是又不完全是,因为还有一个aop:advisor元素与之对应),它包含了PointCut和Advice信息, 它会有一个对应的bean,许多advice信息也包含在里面,不过不用在象以前那样实现指定的BeforeXxx, AroundXxxx之类的接口了, 可以直接通过 org.aspectj.lang.ProceedingJoinPoint来调用指定切面对象的方法。 比如: Xml代码 ... 方法doBasicProfiling就是aBean中定义的一个方法, 它的定义是这样的: Java代码 public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable { // start stopwatch Object retVal = pjp.proceed(); // stop stopwatch return retVal; } aop:pointcut 是切入点定义,跟以前的切入点定义没有什么区别,不过象以前那种正则表达式定义放到了expression属性中, 而且使用了AspectJ的语法。它可以定义在aop:aspect中, 也可以定义在aop:config中 aop:after-returning(before, after-throwing, after, around)这个是Advice的定义, 它里面指定了要跟已定义的哪个切入点关联(pointcut-ref属性), 并且使用aspect中定义的哪个方法(method属性)。 Spring的AOP是上面代理模式的深入。使用Spring AOP,开发者无需实现业务逻辑对象工厂,无需实现代理工厂,这两个工厂都由Spring容器充当。Spring AOP不仅允许使用XML文件配置目标方法,ProxyHandler也允许使用依赖注入管理,Spring AOP提供了更多灵活的选择。 在下面Spring AOP的示例中,InvocationHandler采用动态配置,需要增加的方法也采用动态配置,一个目标对象可以有多个拦截器(类似于代理模式中的代理处理器)。 下面是原始的目标对象: //目标对象的接口 public interface Person { //该接口声明了两个方法 void info(); void run(); } 下面是原始目标对象的实现类,实现类的代码如下: //目标对象的实现类,实现类实现Person接口 public class PersonImpl implements Person { //两个成员属性 private String name; private int age; //name属性的 setter方法 public void setName(String name) { this.name = name; } //age属性的setter方法 public void setAge(int age) { this.age = age; } //info方法,该方法仅仅在控制台打印一行字符串 public void info() { System.out.println("我的名字是: " + name + " , 今年年龄为: " + age); } //run方法,该方法也在控制台打印一行字符串。 public void run() { if (age Wawa 51 正则表达式模式--> 正则表达式列表--> .*run.* lee.Person runAdvisor myAdvice myAroundInterceptor 该配置文件使用ProxyFactoryBean来生成代理对象,配置ProxyFactoryBean工厂bean时,指定了target属性,该属性值就是目标对象,该属性值为personTarget,指定代理的目标对象为personTarget。通过interceptorNames属性确定代理需要的拦截器,拦截器可以是普通的Advice,普通Advice将对目标对象的所有方法起作用,拦截器也可以是Advisor,Advisor是Advice和切面的组合,用于确定目标对象的哪些方法需要增加处理,以及怎样的处理。在上面的配置文件中,使用了三个拦截器,其中myAdvice、myAroundInterceptor都是普通Advice,它们将对目标对象的所有方法起作用。而runAdvisor则使用了正则表达式切面,匹配run方法,即该拦截器只对目标对象的run方法起作用。 下面是测试代理的主程序: public class BeanTest { public static void main(String[] args)throws Exception { //创建Spring容器 ApplicationContext ctx = new FileSystemXmlApplicationContext("bean.xml"); //获取代理对象 Person p = (Person)ctx.getBean("person"); //执行info方法 p.info(); System.out.println("============================== ============="); //执行run方法 p.run(); } } 下面是程序的执行结果: 方法调用之前... 下面是方法调用的信息: 所执行的方法是:public abstract void lee.Person.info() 调用方法的参数是:null 目标对象是:lee.PersonImpl@b23210 调用方法之前: invocation对象:[invocation: method 'info', arguments []; target is of class [lee.PersonImpl]] 我的名字是: Wawa , 今年年龄为: 51 调用结束... =========================================== 方法调用之前... 下面是方法调用的信息: 所执行的方法是:public abstract void lee.Person.run() 调用方法的参数是:null 目标对象是:lee.PersonImpl@b23210 调用方法之前: invocation对象:[invocation: method 'run', arguments [ ]; target is of class [lee.PersonImpl]] 我年老体弱,只能慢跑... 调用结束... 方法调用结束... 目标方法的返回值是 : null 目标方法是 : public abstract void lee.Person.run() 目标方法的参数是 : null 目标对象是 : lee.PersonImpl@b23210 程序的执行结果中一行"="用于区分两次调用的方法。在调用info方法时,只有myAdvice和myAroundInterceptor两个拦截器起作用,调用run方法时候,三个拦截器都起作用了。 通过上面的介绍,可看出Spring的AOP框架是对代理模式简化,并拓展了代理模式的使用。 Spring AOP是Spring声明式事务的基础。了解Spring AOP对深入理解Spring的声明式事务管理是非常有好处的。Spring AOP还可以完成很多功能,例如基于AOP的权限检查。 正式进入主题:AOP 即 Aspect Oriented Programming 的缩写,中文译为"面向切面编程"。本篇没那么学术化,只是为了快速入门了解真实的使用方式!我们不用那么早去想这个名词的意义,fellow me,用真实的代码透析AOP的含义。 不知各位有没有项目开发的经验,如果有的话你应该可以清楚地了解到我们的代码中常常充斥着大量的日志记录代码,我们用log4j等日志记录工具一段一段地记录程序运行的信息。也许有个别是十分特殊的,但是似乎大部分都是例行公事吧!或者在丢出exception的时候捕获其message然后记入日志对吗?这样的代码难道不觉得碍眼?修改起来是不是也很麻烦?要是我们换了一个日志记录工具怎么办?呵呵,别吓到了,没这么严重啦,这些都是极端情况,平时我们还是很开心地写着这些东西。但是今天要说到的这个aop可以将一些特别的操作提取出来,作为我们的"通知-advice",在运行时加载到对象中。就是说,我们可以在代码中不写日志记录段,但是在运行时加载一些"通知"到需要记录日志的"切入点-pointcut"动态地加入日志记录功能。 呼~~~怎么还是文绉绉的……说个简单的例子,来配合我们的代码:一个人,他要吃饭,当然就要吃咯,我们实现了一个Person借口,告诉有eat点方法!也写了一个该接口的实现PersonImpl.java 1 package cn.agatezone.spring.aop; 2 3 public interface Person { 4 public void eat(); 5 } 1 package cn.agatezone.spring.aop; 2 3 public class PersonImpl implements Person { 4 public void eat() { 5 System.out.println("oh! i'm eating now!"); 6 } 7 } 这个人原本是下层阶级的典型,饭前不洗手。但是有一天他变成了中层阶级的典型,要洗手再吃饭了,怎么办?重写Person借口,加入一个方法washhands()?还是在PersonImpl的实例中重写eat方法,在原本方法段前加入洗手方法调用?着不太好吧,这个还是简单的例子,要是项目大了,Person的实现多了好多,怎么办?还是一个一个重写?不用的,aop来帮助我们,不就是饭前洗手吗?我们只要实现一个MethodBeforeAdvice借口就解决这个问题啦!看看我的代码: 01 package cn.agatezone.spring.aop; 02 03 import java.lang.reflect.Method; 04 05 import org.springframework.aop.MethodBeforeAdvice; 06 07 public class BeforeEat implements MethodBeforeAdvice { 08 /* 09 * 这里的method就是被托管的bean(我们这里的Person)中的方法 10 * 这里的args是指 对应方法(这里是指Person中的任何方法)中的参数 11 */ 12 public void before(Method method, Object[] args, Object target) 13 throws Throwable { 14 if(method.getName().equals("eat")) { 15 System.out.println("before eat something i must wash my hands!"); 16 } 17 } 18 } 哦,如何使用呢?当然还是xml配置文件啦!详情看我的xml如下: 01 02 04 05 06 07 08 09 cn.agatezone.spring.aop.Person 10 11 12 13 14 15 beforeEat 16 17 18 19 20 21 22 23 现在配置好了,我们写个测试代码用用看! 01 package cn.agatezone.spring.aop; 02 03 import org.springframework.context.ApplicationContext; 04 import org.springframework.context.support.ClassPathXmlAp plicationContext; 05 06 import junit.framework.TestCase; 07 08 public class AopTest extends TestCase { 09 private static String path = 10 "cn/agatezone/spring/aop/applicationContext.xml"; 11 12 private ApplicationContext ctx; 13 14 protected void setUp() throws Exception { 15 ctx = new ClassPathXmlApplicationContext(path); 16 } 17 18 protected void tearDown() throws Exception { 19 ctx = null; 20 } 21 22 public void testAop() { 23 Person p = (Person) ctx.getBean("person"); 24 p.eat(); 25 } 26 } 运行吧!看看是不是出现了 before eat something i must wash my hands! oh! i'm eating now! 这就是一个再简单不过的aop例子了……至少我这么觉得。好让我们回味一下,什么是切入点?说白了就是一个方法嘛!处理这个方法无非在它1.运行前2.返回值的时候3.抛出异常的时候我们来针对其做"通知"就是我们这里的BeforeEat啦,但是我们例子只用了aop中的一种通知-运行前,即实现MethodBeforeAdvice这个接口的通知。还有返回值和 抛异常 的大家通知可以参照文档进行自我研究啦!本文只为Spring 的 aop 开个头,进一步的学习会在以后发布。 提供些学习资料: dev2dev 网站中有一个aop的学习文章分两部分 第一部分:http://dev2dev.bea.com.cn/techdoc/20051216709.html 第二部分:http://dev2dev.bea.com.cn/techdoc/20051223712.html 一、概念 1、连接点(join point):指程序执行过程中的一个特定点,比如方法调用、抛出异常、对象初始化等等,用来定义你的程序在什么地方加入新的逻辑。 2、通知(advice):特定的连接点出运行的代码称为通知。通知有很多种,比如前置通知、后置通知等。 3、切入点(point cut):指一个通知该何时执行的一组连接点,典型的切入点如对某个类所有方法调用的集合。 4、方面(aspect),通知和切入点的组合称为方面,也即定义了程序执行的逻辑以及何时应该被执行。 5、织入(weaving):方面被加入程序的过程,静态织入一般在编译时进行,而动态织入则在运行时进行,Spring AOP属于动态织入。 6、目标(target):也就是被aop的对象。 7、引入(introduce):就是向对象中加入新的属性或方法,比如可以修改它使之实现某个接口。 二、应用 1、使程序支持@Aspect: 在spring配置文件中加入: 2、定义一个方面: package org.xyz; import org.aspectj.lang.annotation.Aspect; @Aspect public class SimpleAspect { } 3、声明一个切入点: @Pointcut("execution(* transfer(..))") // the pointcut expression private void anyOldTransfer() {} // the pointcut signature 4、Spring AOP支持的切入点表达式: execution 匹配方法执行连接点,也是使用Spring AOP最常用到的。 within 匹配特定类型,只是为了简化特定类型执行AOP的方法执行声明。 this 连接点必须是指定类型的实例。 args 连接点的参数必须是指定类型的实例。 @target 连接点执行对象类型必须有指定类型的注解(annotation)。 @args 连接点实参的运行时类型必须有指定类型的注解(annotation)。 @within 匹配具有指定注解(annotation)的类型 @annotation 连接点必须有指定的注解(annotaion) 5、组合连接点表达式 可以用&&, ||, !进行组合 6、声明通知(advice) 1)、前置通知(Before advice) 一个切面里使用 @Before 注解声明前置通知: import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; @Aspect public class BeforeExample { @Before("com.xyz.myapp.SystemArchitecture.dataAcce ssOperation()") public void doAccessCheck() { // ... } } 如果使用一个in-place 的切入点表达式,我们可以把上面的例子换个写法: import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; @Aspect public class BeforeExample { @Before("execution(* com.xyz.myapp.dao.*.*(..))") public void doAccessCheck() { // ... } } 2)、返回后通知(After returning advice) 返回后通知通常在一个匹配的方法返回的时候执行。使用 @AfterReturning 注解来声明: import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.AfterReturning; @Aspect public class AfterReturningExample { @AfterReturning("com.xyz.myapp.SystemArchitecture. dataAccessOperation()") public void doAccessCheck() { // ... } } 说明:你可以在同一个切面里定义多个通知,或者其他成员。我们只是在展示如何定义一个简单的通知。这些例子主要的侧重点是正在讨论的问题。 有时候你需要在通知体内得到返回的值。你可以使用以 @AfterReturning 接口的形式来绑定返回值: import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.AfterReturning; @Aspect public class AfterReturningExample { @AfterReturning(pointcut="com.xyz.myapp.SystemArch itecture.dataAccessOperation()", returning="retVal") public void doAccessCheck(Object retVal) { // ... } } 在 returning 属性中使用的名字必须对应于通知方法内的一个参数名。 当一个方法执行返回后,返回值作为相应的参数值传入通知方法。 一个 returning 子句也限制了只能匹配到返回指定类型值的方法。 (在本例子中,返回值是 Object 类,也就是说返回任意类型都会匹配) 3)、抛出后通知(After throwing advice) 抛出后通知在一个方法抛出异常后执行。使用 @AfterThrowing 注解来声明: import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.AfterThrowing; @Aspect public class AfterThrowingExample { @AfterThrowing("com.xyz.myapp.SystemArchitecture.d ataAccessOperation()") public void doRecoveryActions() { // ... } } 你通常会想要限制通知只在某种特殊的异常被抛出的时候匹配,你还希望可以在通知体内得到被抛出的异常。 使用 throwing 属性不光可以限制匹配的异常类型(如果你不想限制,请使用 Throwable 作为异常类型),还可以将抛出的异常绑定到通知的一个参数上。 import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.AfterThrowing; @Aspect public class AfterThrowingExample { @AfterThrowing(pointcut="com.xyz.myapp.SystemArchi tecture.dataAccessOperation()", throwing="ex") public void doRecoveryActions(DataAccessException ex) { // ... } } 在 throwing 属性中使用的名字必须与通知方法内的一个参数对应。 当一个方法因抛出一个异常而中止后,这个异常将会作为那个对应的参数送至通知方法。 throwing 子句也限制了只能匹配到抛出指定异常类型的方法(上面的示例为 DataAccessException)。 4)、后通知(After (finally) advice) 不论一个方法是如何结束的,在它结束后(finally)后通知(After (finally) advice)都会运行。 使用 @After 注解来声明。这个通知必须做好处理正常返回和异常返回两种情况。通常用来释放资源。 import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.After; @Aspect public class AfterFinallyExample { @After("com.xyz.myapp.SystemArchitecture.dataAcces sOperation()") public void doReleaseLock() { // ... } } 5)、环绕通知(Around Advice) 最后一种通知是环绕通知。环绕通知在一个方法执行之前和之后执行。 它使得通知有机会既在一个方法执行之前又在执行之后运行。并且,它可以决定这个方法在什么时候执行,如何执行,甚至是否执行。 环绕通知经常在在某线程安全的环境下,你需要在一个方法执行之前和之后共享某种状态的时候使用。 请尽量使用最简单的满足你需求的通知。(比如如果前置通知(before advice)也可以适用的情况下不要使用环绕通知)。 环绕通知使用 @Around 注解来声明。通知的第一个参数必须是 ProceedingJoinPoint 类型。 在通知体内,调用 ProceedingJoinPoint 的 proceed() 方法将会导致潜在的连接点方法执行。 proceed 方法也可能会被调用并且传入一个 Object[] 对象-该数组将作为方法执行时候的参数。 当传入一个 Object[] 对象的时候,处理的方法与通过AspectJ编译器处理环绕通知略有不同。 对于使用传统AspectJ语言写的环绕通知来说,传入参数的数量必须和传递给环绕通知的参数数量匹配(不是后台的连接点接受的参数数量),并且特定顺序的传入参数代替了将要绑定给连接点的原始值(如果你看不懂不用担心)。 Spring采用的方法更加简单并且更好得和他的基于代理(proxy-based),只匹配执行的语法相适用。 如果你适用AspectJ的编译器和编织器来编译为Spring而写的@AspectJ切面和处理参数,你只需要了解这一区别即可。 有一种方法可以让你写出100%兼容Spring AOP和AspectJ的,我们将会在后续的通知参数(advice parameters)的章节中讨论它。 import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.ProceedingJoinPoint; @Aspect public class AroundExample { @Around("com.xyz.myapp.SystemArchitecture.business Service()") public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable { // start stopwatch Object retVal = pjp.proceed(); // stop stopwatch return retVal; } } 方法的调用者得到的返回值就是环绕通知返回的值。 例如:一个简单的缓存切面,如果缓存中有值,就返回该值,否则调用proceed()方法。 请注意proceed可能在通知体内部被调用一次,许多次,或者根本不被调用。 7、通知参数(Advice parameters) Spring 2.0 提供了完整的通知类型 这意味着你可以在通知签名中声明所需的参数,(就像在以前的例子中我们看到的返回值和抛出异常一样)而不总是使用Object[]。 我们将会看到如何在通知体内访问参数和其他上下文相关的值。首先让我们看以下如何编写普通的通知以找出正在被通知的方法。 1)、访问当前的连接点 任何通知方法可以将第一个参数定义为 org.aspectj.lang.JoinPoint 类型 (环绕通知需要定义为 ProceedingJoinPoint 类型的, 它是 JoinPoint 的一个子类。) JoinPoint 接口提供了一系列有用的方法, 比如 getArgs()(返回方法参数)、getThis()(返回代理对象)、getTarget()(返回目标)、getSignature()(返回正在被通知的方法相关信息)和 toString()(打印出正在被通知的方法的有用信息)。 2)、传递参数给通知(Advice) 我们已经看到了如何绑定返回值或者异常(使用后置通知(after returning)和异常后通知(after throwing advice)。 为了可以在通知(adivce)体内访问参数,你可以使用 args 来绑定。 如果在一个参数表达式中应该使用类型名字的地方使用一个参数名字,那么当通知执行的时候对应的参数值将会被传递进来。 可能给出一个例子会更好理解。假使你想要通知(advise)接受某个Account对象作为第一个参数的DAO操作的执行,你想要在通知体内也能访问到account对象,你可以写如下的代码: @Before("com.xyz.myapp.SystemArchitecture.dataAcce ssOperation() && args(account,..)") public void validateAccount(Account account) { // ... } 切入点表达式的 args(account,..) 部分有两个目的: 首先它保证了只会匹配那些接受至少一个参数的方法的执行,而且传入的参数必须是 Account 类型的实例, 其次它使得可以在通知体内通过 account 参数来访问那个account参数。 另外一个办法是定义一个切入点,这个切入点在匹配某个连接点的时候"提供"了一个Account对象, 然后直接从通知中访问那个命名的切入点。你可以这样写: @Pointcut("com.xyz.myapp.SystemArchitecture.dataAc cessOperation() && args(account,..)") private void accountDataAccessOperation(Account account) {} @Before("accountDataAccessOperation(account)") public void validateAccount(Account account) { // .. } 如果想要知道更详细的内容,请参阅 AspectJ 编程指南。 代理对象(this)、目标对象(target) 和注解(@within, @target, @annotation, @args)都可以用一种简单格式绑定。 以下的例子展示了如何使用 @Auditable 注解来匹配方法执行,并提取AuditCode。 首先是 @Auditable 注解的定义: @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Auditable { AuditCode value(); } 然后是匹配 @Auditable 方法执行的通知: @Before("com.xyz.lib.Pointcuts.anyPublicMethod() && @annotation(auditable)") public void audit(Auditable auditable) { AuditCode code = auditable.value(); // ... } 3)、决定参数名 绑定在通知上的参数依赖切入点表达式的匹配名,并借此在(通知(advice)和切入点(pointcut))的方法签名中声明参数名。 参数名 无法 通过Java反射来获取,所以Spring AOP使用如下的策略来决定参数名字: 如果参数名字已经被用户明确指定,则使用指定的参数名: 通知(advice)和切入点(pointcut)注解有一个额外的"argNames"属性,该属性用来指定所注解的方法的参数名 这些参数名在运行时是 可以 访问的。例子如下: @Before(value="com.xyz.lib.Pointcuts.anyPublicMeth od() && @annotation(auditable)", argNames="auditable") public void audit(Auditable auditable) { AuditCode code = auditable.value(); // ... } 如果一个@AspectJ切面已经被AspectJ编译器(ajc)编译过了,那么就不需要再添加 argNames 参数了,因为编译器会自动完成这一工作。 使用 'argNames' 属性有点不那么优雅,所以如果没有指定'argNames' 属性, Spring AOP 会寻找类的debug信息,并且尝试从本地变量表(local variable table)中来决定参数名字。 只要编译的时候使用了debug信息(至少要使用 '-g:vars' ),就可获得这些信息。 使用这个flag编译的结果是: (1)你的代码将能够更加容易的读懂(反向工程) (2)生成的class文件会稍许大一些(不重要的) (3)移除不被使用的本地变量的优化功能将会失效。 换句话说,你在使用这个flag的时候不会遇到任何困难。 如果不加上debug信息来编译的话,Spring AOP将会尝试推断参数的绑定。 (例如,要是只有一个变量被绑定到切入点表达式(pointcut expression)、通知方法(advice method)将会接受这个参数, 这是显而易见的)。 如果变量的绑定不明确,将会抛出一个 AmbiguousBindingException 异常。 如果以上所有策略都失败了,将会抛出一个 IllegalArgumentException 异常 8、常用切入点表达式 execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?) 除了返回类型模式(上面代码片断中的ret-type-pattern),名字模式和参数模式以外,所有的部分都是可选的。 返回类型模式决定了方法的返回类型必须依次匹配一个连接点。 你会使用的最频繁的返回类型模式是 *,它代表了匹配任意的返回类型。 一个全称限定的类型名将只会匹配返回给定类型的方法。名字模式匹配的是方法名。 你可以使用 * 通配符作为所有或者部分命名模式。 参数模式稍微有点复杂:() 匹配了一个不接受任何参数的方法,而 (..) 匹配了一个接受任意数量参数的方法(零或者更多)。 模式 (*) 匹配了一个接受一个任何类型的参数的方法。 模式 (*,String) 匹配了一个接受两个参数的方法,第一个可以是任意类型,第二个则必须是String类型。 下面给出一些常见切入点表达式的例子。 任意公共方法的执行: execution(public * *(..)) 任何一个以"set"开始的方法的执行: execution(* set*(..)) AccountService 接口的任意方法的执行: execution(* com.xyz.service.AccountService.*(..)) 定义在service包里的任意方法的执行: execution(* com.xyz.service.*.*(..)) 定义在service包或者子包里的任意方法的执行: execution(* com.xyz.service..*.*(..)) 在service包里的任意连接点(在Spring AOP中只是方法执行): within(com.xyz.service.*) 在service包或者子包里的任意连接点(在Spring AOP中只是方法执行): within(com.xyz.service..*) 实现了 AccountService 接口的代理对象的任意连接点(在Spring AOP中只是方法执行): this(com.xyz.service.AccountService) 'this'在binding form中用的更多:- 请常见以下讨论通知的章节中关于如何使得代理对象可以在通知体内访问到的部分。 实现了 AccountService 接口的目标对象的任意连接点(在Spring AOP中只是方法执行): target(com.xyz.service.AccountService) 任何一个只接受一个参数,且在运行时传入的参数实现了 Serializable 接口的连接点 (在Spring AOP中只是方法执行) args(java.io.Serializable) 请注意在例子中给出的切入点不同于 execution(* *(java.io.Serializable)): args只有在动态运行时候传入参数是可序列化的(Serializable)才匹配,而execution 在传入参数的签名声明的类型实现了 Serializable 接口时候匹配。 有一个 @Transactional 注解的目标对象中的任意连接点(在Spring AOP中只是方法执行) @target(org.springframework.transaction.annotation .Transactional) 任何一个目标对象声明的类型有一个 @Transactional 注解的连接点(在Spring AOP中只是方法执行) @within(org.springframework.transaction.annotation .Transactional) 任何一个执行的方法有一个 @Transactional annotation的连接点(在Spring AOP中只是方法执行) @annotation(org.springframework.transaction.annota tion.Transactional) 任何一个接受一个参数,并且传入的参数在运行时的类型实现了 @Classified annotation的连接点(在Spring AOP中只是方法执行) @args(com.xyz.security.Classified) 9、参考并抄袭自 http://static.springsource.org/spring/docs/2.5.x/r eference/aop.html http://hi.baidu.com/wangyongjin87/blog/item/c9cf2c ec4e19de232df534cb.html