Spring AOP动态代理初学
所谓AOP,就是相对于OOP(Object Oriented Programming)面向对象编程的说法,有些人喜欢叫面向切面编程,有些人喜欢叫做面向方面,事实上这两个都是指同一个东西,只是叫法不同。
我们传统的编程都是面向对象,就是说每个类都有它实际的意义。而面向切面略有不同,它在面向对象的基础上扩展了一下,它编程的时候不是先考虑的一个具体对象(比如用户类),而是先考虑的对象的行为或者功能。这个不是编程方法的不同,而是编程思维的转变。
动态代理是实现AOP的一种方式,Spring缺省使用J2SE 动态代理(dynamic proxies)来作为AOP的代理。这样任何接口都可以被代理。
Spring也支持使用CGLIB代理. 对于需要代理类而不是代理接口的时候CGLIB代理是很有必要的。 要想学好AOP,首先要理解代理模式的应用,可以参考以前写的一篇文章。
下面针对一个实际的用例来分别说明如何用JDK与Spring实现代理。
1. 背景——我自己,工作 or 休息
首先以自己为例子,平时不是工作就是休息,那么首先构造这样一个自己的接口(见代码1)。
代码1:
public interface IMe { public void go2work(String doWhat); public void rest(String doWhat); } |
写一个自己的接口实现(见代码2)。
代码2:
public class Me implements IMe{ public void go2work(String doWhat) { System.out.println("到单位去" + doWhat); } public void rest(String doWhat) { System.out.println("在家休息" + doWhat); } } |
可以看出我自己有两个方法,分别是工作和休息,那么休息就在家好了,工作的的时候必定要有一个去单位上班,和下班回家的过程,这个也就是需要在go2work这个方法的前后加上一些方法。我们的题目来了,怎么在go2work前后加方法,而又不影响rest方法呢?
2. JDK代理解决
我们可以利用JDK自带的InvocationHandler来实现动态代理(见代码3)。
代码3:
public class MeJDKProxy implements InvocationHandler { Object target; public Object bind(Object target) { // 截取了被绑定的、需要被代理的实例对象,交给类成员target this.target = target; // 返回一个新的"代理实例",也就是说外部调用了bind方法以后,获取到的就不是传进来的那个实例对象了,而是被"封装"过的代理实例 return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this); } public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Object result = null; // 如果方法名为go2work,就拦截处理前后,如果不是则跳过 if ("go2work".equals(method.getName())) { // 上班前 doBeforeWork(); // 拦截了真正的目标实例对象的方法以后,利用反射来自动调用相同方法 // this.target就是被代理的实例对象 // 这里的args数组就是方法调用时传递的实参数组 result = method.invoke(this.target, args); // 上班后 doAfterWork(); } else { result = method.invoke(this.target, args); } return result; } public void doBeforeWork() { System.out.println("上班做地铁去单位"); } public void doAfterWork() { System.out.println("下班坐地铁回家"); } } |
3. Spring AOP解决
以上的InvocationHandler动态代理接口就实现了解耦,这也是Spring AOP的实现原理,也是建立在这个的基础上的。 这时候我们就要利用开始抛出AOP的几个名词概念:Pointcut切入点、Advice通知、Advisor配置器。
1)Pointcut切入点
Pointcut其实是一个集合,它是由一个一个叫做Join Point(连接点)的东西组合而成的。那么什么叫做Join Point呢?
其实前面我们的送礼物示例中的go2work()方法、rest()方法等等这些需要被代理的方法就是一个一个Join Point,它们代表了符合被代理条件的方法。
2)Advice通知
doBeforeWork()和doAfterWork()方法就是Advice通知,它代表的意思就是需要自动嵌入进Join Point也就是被代理方法里的方法。它有5种类型,分别代表了5种嵌入Advice的约束,比如Join Point的周围、前面、后面、异常的时候等等,分别对应在Spring中的接口:
1、Interception Around(实现MethodInterceptor接口)
2、Before
3、After Returning
4、Throw
3)Advisor配置器
Advisor就是Pointcut和Advice的配置器,它是将Advice织入Pointcut位置的代码,也就是前面的MeJDKProxy类。
AOP的强大优势就在于它能实现代码的低耦合,也就是说写项目的时候,可以让项目的各个功能模块都不用互相依赖,逻辑关系可以独立出来。
以经典的日志管理举例:我们可以先完全不用理会什么日志管理功能,就直接写我们需要的其他模块,比如说数据库操作。
当所有模块都完成以后,我们可以后期再额外的写一个不依赖与其他模块的日志管理模块,然后用AOP来把这个模块跟数据库操作模块挂上钩,这样在数据操作的时候同时也就通过AOP的自动代理功能自动的记录了数据库操作日志。
而当我们又突然不需要日志管理了,那么可以直接把日志管理模块移出,然后改一下AOP模块里的几行代码就可以了,数据库操作模块完全不用改写任何代码。
这也就是为什么AOP叫做面向切面编程而不是面向对象编程,因为用AOP观念来写程序的时候,是先考虑的要先那些切面(可以暂时理解为功能模块来帮助理解),而不是先考虑的这个工程有那些类和对象。
我们可以利用Spring AOP提供的MethodInterceptor来实现round方式的动态代理(见代码4)。
代码4:
public class MeSpringProxy implements MethodInterceptor { public Object invoke(MethodInvocation arg0) throws Throwable { Object result = null; doBeforeWork(); result = arg0.proceed(); doAfterWork(); return result; } public void doBeforeWork() { System.out.println("上班做地铁去单位"); } public void doAfterWork() { System.out.println("下班坐地铁回家"); } } |
Spring配置文件见代码5。
代码5:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd"> <beans> <bean id="me" class="com.baidu.ecom.beidou.proxytest.Me"/> <bean id="meSpringProxy" class="com.baidu.ecom.beidou.proxytest.MeSpringProxy" /> <bean id="meAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor"> <property name="advice"> <ref bean="meSpringProxy" /> </property> <property name="patterns"> <list> <!-- 这里使用的是正则表达式,代表了符合这个正则表达式的方法才会被代理 --> <value>.*go2work.*</value> </list> </property> </bean> <!-- 牢记这个bean的路径和全名,以后经常要用到,也很好记,直译成中文就是代理工厂Bean --> <bean id="meProxy" class="org.springframework.aop.framework.ProxyFactoryBean"> <!-- 注入被代理的目标类的接口 --> <property name="proxyInterfaces"> <value>com.baidu.ecom.beidou.proxytest.IMe</value> </property> <!-- 注入被代理的目标类,也就是Pointcut,注意这里注入的值是ref,而不是value --> <property name="target"> <ref bean="me" /> </property> <!-- 这个就是自动代理类,也就是Advice,我们这里改成advisor,是因为要过滤指定的方法名,可以直接用meSpringProxy其实 --> <property name="interceptorNames"> <list> <value>meAdvisor</value> </list> </property> </bean> </beans> |
4. 单元测试
单侧用例分别测试JDK和Spring AOP的动态代理功能,见代码6。搭建环境需要的Jar包如下:
commons-lang.jar
commons-logging.jar
spring.jar
代码6:
public class UnitTest extends TestCase { public void testJDKProxy() { MeJDKProxy proxy = new MeJDKProxy(); // 调用同一个Gift动态代理实例,无需重写 IMe me1 = (IMe) proxy.bind(new Me()); me1.go2work("编程"); System.out.println(); IMe me2 = (IMe) proxy.bind(new Me()); me2.rest("看电视"); } public void testSpringProxy() { ApplicationContext appContext = new ClassPathXmlApplicationContext("applicationContext.xml"); // 注意,这里getBean获取的是GiftProxy这个bean,而不是Robot IMe me = (IMe) appContext.getBean("meProxy"); me.go2work("开会"); System.out.println(); me.rest("睡觉"); } } |
输出如下:
上班做地铁去单位 到单位去编程 下班坐地铁回家 在家休息看电视 2011-6-12 13:51:31 org.springframework.context.support.AbstractApplicationContext prepareRefresh 信息: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@f38798: display name [org.springframework.context.support.ClassPathXmlApplicationContext@f38798]; startup date [Sun Jun 12 13:51:31 CST 2011]; root of context hierarchy 2011-6-12 13:51:31 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions 信息: Loading XML bean definitions from class path resource [applicationContext.xml] 2011-6-12 13:51:31 org.springframework.context.support.AbstractApplicationContext obtainFreshBeanFactory 信息: Bean factory for application context [org.springframework.context.support.ClassPathXmlApplicationContext@f38798]: org.springframework.beans.factory.support.DefaultListableBeanFactory@5e3974 2011-6-12 13:51:31 org.springframework.beans.factory.support.DefaultListableBeanFactory preInstantiateSingletons 信息: Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@5e3974: defining beans [me,meSpringProxy,meAdvisor,meProxy]; root of factory hierarchy 上班做地铁去单位 到单位去开会 下班坐地铁回家 在家休息睡觉 |