Spring 学习其二:AOP
一、AOP
Spring 的两大特性,IOC 在上一章,本篇讨论另一大特性,AOP(面向切面)。
何为面向切面,
动态代理,可以绑定一个接口和一个它的实现,并且代理这个实现类,所以我们可以在代理里写进一些自己的操作,甚至可以不执行实现类的方法。
原来的代码:
这是接口:
public interface ProxyService { void HelloWorld(); }
这是它的实现类:
import static MyTools.PrintTools.*; public class ProxyServiceImpl implements ProxyService { @Override public void HelloWorld() { println("Hello World"); } }
然后是动态代理的实现过程:
public class ProxyJdkExample implements InvocationHandler{ private Object target = null; public Object bind(Object target) { this.target = target; return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this); } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { println("proxy method"); println("before proxy"); //Object obj = method.invoke(target, args); println("after proxy"); return null; } }
二、在 xml 里定义 AOP
现在,我们再来一个例子,这个例子会贯穿整篇完整。
有一个叫做演员接口,它有一个方法叫做表演:
public interface Performer { void Perform(); }
然后有一个实现他的类:
public class PerformerImpl implements Performer{ @Override public void Perform() { System.out.println("Performing...."); } }
去调用这个方法很容易,但是现在我希望,在表演前,能够让观众关掉手机。我们可以利用动态代理把关手机的过程放进去:
public class PerformProxy implements InvocationHandler { private Object target = null; public Object bind(Object target) { this.target = target; return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this); } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("close your phone"); Object obj = method.invoke(target, args); return obj; }
测试:
public class PerformaceTest { public static void main(String[] args) { PerformProxy performProxy = new PerformProxy(); Performer performerImpl = new PerformerImpl(); Performer performer =(Performer)performProxy.bind(performerImpl); performer.Perform(); } } /*output: close your phone... Performing....*/
现在回到 AOP,AOP 让我们能够更方便的去实现上述的功能。在上面的示例中,我们完成在表演前让观众关闭手机,但是还不够,我们希望能够在完成表演后观众可以鼓掌,如果表演失败,观众会要求退票。现在我们用 spring 来实现。
首先我们把上述添加的功能作为方法统一放到一个观众类里:
public class Audience { public void before() { System.out.println("close your phone...."); } public void after() { System.out.println("clapping ...."); } public void afterThrowning() { System.out.println("take my money back ...."); } }
在进行面向切面设计之前,先来简单介绍 AOP 的几个术语:
- 切面(Aspect):就是指 Audience ,它是一个方法的集合,可以把这些方法切入到别的地方;
- 通知(Advice):我们之前实现的关手机操作,是在表演开始前的,“表演开始前”就是一个通知,Spring 提供的通知有:前置通知(before),后置通知(after)返回通知(afterReturning),异常通知(afterThrowing),环绕通知(around),每个通知的具体位置(在方法进行到哪一步执行)会在后面示例中体现
- 切点(Pointcut):我们想要切入的点,上面的示例,切点就是 perform() 方法。
- 连接点(join point):连接点对应的事具体需要拦截的东西,比如通过切点的正则表达式去判断哪些方法是连接点。
- 织入(Weaving):织入是一个生成代理对象并将切面内容放入指定流程中的过程。
现在我们来通过 xml 来定义 AOP:
<bean id = "performAspect" class = "SpringTest.performance.aspect.Audience"/> <bean id = "performer" class = "SpringTest.performance.service.impl.PerformerImpl"> </bean> <aop:config> <aop:aspect ref = "performAspect"> <aop:pointcut expression="execution(* SpringTest.performance.service.impl.PerformerImpl.perform(..))" id="performPoint"/> <aop:before method="before" pointcut-ref = "performPoint"/> <aop:after method="after" pointcut-ref = "performPoint"/> <aop:after-throwing method="afterThrowning" pointcut-ref = "performPoint"/> </aop:aspect> </aop:config>
注意 performer 也必须定义,只有将它放入到 IOC 容器里,AOP 才能生效。
测试:
public class PerformaceTest { public static void main(String[] args) { ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("spring-cfg.xml"); Performer performer =(Performer)ctx.getBean("performer"); performer.perform(); } } /*output: close your phone.... Performing.... clapping .... */
如果,方法执行过程中抛出异常,就会调用 afterThrowning()(这里不演示)
接下来是环绕通知,环绕通知的功能很强,甚至能够实现后置和前置通知,为何?先看代码:
public void around(ProceedingJoinPoint jp) { System.out.println("keep quite..."); try { jp.proceed(); } catch (Throwable e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println("ending..."); }
这是环绕通知的实现方式,把它放在 Audience 类里即可,可以发现,它是带有一个参数的,这个参数可以反射连接点的方法,也就是说环绕通知可以自定义什么时候执行原方法,甚至不执行。
现在我们再 xml 里注册它:
<aop:around method="around" pointcut-ref = "performPoint"/>
测试后的输出为:
close your phone.... keep quite... Performing.... clapping .... ending...
从输出顺序可以看出环绕通知执行的方式,clapping 在 ending 前面,说明什么?
说明传递给 around 的参数已经和 after 先绑定在一起了。所有在执行 try 内代码的时候,clapping 就执行了。
那么 close your phone...为何在keep quit 之前?难道 before 就没有和切点方法绑定?为了得到解释,我们只能通过 debug 一步步看它怎么执行的。
这里直接给出,我看源码的结果:
首先这些通知是按照一定的顺序放置在一个拦截器数组数组里的,第一个被执行的是 Before 所以第一个输出 close your phone;
第二个执行的是 Around,一路执行到输出 keep quite。到了这里,我们会进入到 jp.proceed() 该方法会进入到拦截器数组.
拦截器数组第三个应该是 afterThrowning 但是不满足执行条件,所以执行第4个 after。然后输出 clapping...
jp.proceed() 执行完后,继续执行 Around 的输出 ending。
整个过程其实是一个递归过程,当执行 Around 时,应为 proceed() 的关系,进入了更深层次的递归,所以导致,ending 的输出在 clapping 后面。
三、给通知传递参数
表演者往往都有自己的粉丝,而粉丝只想给自己的偶像鼓掌怎么办?能不能再通知里加入一个参数能够判别是哪个表演者在表演?在动态代理里,代理方法 invoke 是能够获取到被代理方法的参数的,所以在 spring 里也是可行的。
首先,先在被代理的方法 perform 里加上演员名称的参数:
public interface Performer { void perform(String performer); }
public class PerformerImpl implements Performer{ @Override public void perform(String performer) { System.out.println(performer + " is Performing...."); } }
之后,修改切点的定义,添加参数进去:
<aop:pointcut expression="execution(* SpringTest.performance.service.impl.PerformerImpl.perform(..)) and args(performer)" id="performPoint"/>
在对应的通知方法里加上参数:
public class Audience { public void before(String performer) { System.out.println("close your phone...."); System.out.println("welcome " + performer); } public void after(String performer) { System.out.println("clapping for " + performer); } public void afterThrowning(String performer) { System.out.println("take my money back ...."); } public void around(ProceedingJoinPoint jp,String performer) { System.out.println(performer+" is singing"); System.out.println("ending..."); } }
需要注意的是 Around 通知的参数必须放在 jp 后面,放在前面会抛出异常。
四:引入
引入说的简单点就是引入其他接口的实现,比如 PerformImpl 实现的事 Perform 这个接口,通过引入可以让它实现另一个接口,并且能为它指定具体的实现方法。
现在,我们就为 Perform 添加一个接口,这个接口的方法用来判断票卖出了多少,如果少于100张,就取消表演:
public interface TicketCheck { boolean ticketIsEnough(int number); }
public class TickCheckImpl implements TicketCheck { @Override public boolean ticketIsEnough(int number) { return number > 100 ? true:false; } }
然后再 xml 里把 TicketChenk 引入到 Perform 里:
<aop:config> <aop:aspect ref = "performAspect"> <aop:pointcut expression="execution(* SpringTest.performance.service.impl.PerformerImpl.perform(..)) and args(performer)" id="performPoint"/> <aop:after method="after" pointcut-ref = "performPoint"/> <aop:after-throwing method="afterThrowning" pointcut-ref = "performPoint"/> <aop:before method="before" pointcut-ref = "performPoint"/> <aop:around method="around" pointcut-ref = "performPoint"/> <aop:declare-parents types-matching="SpringTest.performance.service.impl.PerformerImpl+"
implement-interface="SpringTest.performance.service.TicketCheck" default-impl = "SpringTest.performance.service.impl.TicketCheckImpl"/> </aop:aspect> </aop:config>
来分析下关于引入的定义:
<aop:declare-parents types-matching="SpringTest.performance.service.impl.PerformerImpl+" implement-interface="SpringTest.performance.service.TicketCheck" default-impl = "SpringTest.performance.service.impl.TicketCheckImpl"/>
types-matching:表示切点的类型,这里是 PerformerImpl,后面的 + 表示我们想要为它添加一个新的接口,接口的名称在 implement-interface 声明,然后默认的实现类在 default-impl 里声明,这样处理之后,我们能干吗?我们可以把 PerformerImpl 显性的转换为 TicketCheck :
public class PerformaceTest { public static void main(String[] args) { ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("spring-cfg.xml"); Performer performer =(Performer)ctx.getBean("performer"); TicketCheck ticketCheck = (TicketCheck) performer; if(ticketCheck.ticketIsEnough(101)) performer.perform("Luhan"); } }
五:注解实现 AOP
注解实现的解释说明和 xml 很接近,这里直接上代码做参照即可
Audience:
package SpringTest.performance.aspect; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.AfterThrowing; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.DeclareParents; import org.aspectj.lang.annotation.Pointcut; import org.springframework.stereotype.Component; import SpringTest.performance.service.TicketCheck; import SpringTest.performance.service.impl.TicketCheckImpl; @Aspect @Component(value="performAspect") public class Audience { @Pointcut("execution(* SpringTest.performance.service.impl.PerformerImpl.perform(..))" + " && args(performer)") public void performPoint(String performer) { } @DeclareParents(value="SpringTest.performance.service.impl.PerformerImpl+",defaultImpl = TicketCheckImpl.class) public TicketCheck ticketService; @Before("performPoint(performer)") public void before(String performer) { System.out.println("close your phone...."); System.out.println("welcome " + performer); } @After("performPoint(performer)") public void after(String performer) { System.out.println("clapping for " + performer); } @AfterThrowing("performPoint(performer)") public void afterThrowning(String performer) { System.out.println("take my money back ...."); } @Around("performPoint(performer)") public void around(ProceedingJoinPoint jp,String performer) { System.out.println(performer+" is singing"); System.out.println("ending..."); } }
performPoint() 方法只是为了给上面注解的 Pointcut 创建一个 id。
此处有很多需要注意得点,不然很容易报错:
- 在类上面添加 @Aspect 这样才能被当做切面处理
- 在类上面添加 @Component 这样才能被装入 IOC 容器
- 定义切点的时候注意格式:* 和 S 之间的空格必须存在
@Pointcut("execution(* SpringTest.performance.service.impl.PerformerImpl.perform(..))" + " && args(performer)")
- @Pointcut 下面的方法参数必须和注解里的参数保持一致
- 注意通知里的切点的格式,必须有双引号,参数必须和切点保持一致
以上有几点关于格式的错误,Spring 的报错很不详细的,所以必须在键入代码时,就注意格式问题。
完成之后,就可以把 xml 里关于 AOP 的声明都删除了:
为了注解的内容能被扫描到,我们还需要定义一个扫描器:
import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.EnableAspectJAutoProxy; import SpringTest.performance.aspect.Audience; import SpringTest.performance.service.impl.PerformerImpl; @Configuration @EnableAspectJAutoProxy @ComponentScan(basePackages= {"SptingTest.performance"}, basePackageClasses= {PerformerImpl.class,Audience.class}) public class PerformConfig { }
注意我用的注解,一个都不能少。。。
然后就可以运行测试了:
public class PerformaceTest { public static void main(String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(PerformConfig.class); Performer performer =(Performer) ctx.getBean("performer"); TicketCheck ticketCheck = (TicketCheck) performer; if(ticketCheck.ticketIsEnough(101)) performer.perform("Luhan"); } }
六、关于多重切面
Spring 可以针对一个切点,定义多个切面和多个通知,需要注意的是,想要控制好它的顺序,可在 @Aspect 注解下面跟随 @Order({数字}) 比如 @Order(1),表是它的顺位是第一个,但是记住,这里 1 表达的是,该切面是在所有切面的最外围,2 被包裹在 1 里面,3 又会被包裹在 2 里面。其输出顺序 1 的 before 在最前面调用,After 在后面调用。
至于更为复杂的 Around 通知的顺序,我会在后面针对 通知的执行顺序 源码说明里提到(挖个坑)。