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
上班做地铁去单位
到单位去开会
下班坐地铁回家
 
在家休息睡觉

Leave a Comment.