在Spring 2.0以后,Spring AOP已经封装的比较完善,用@AspectJ注解或XML的方式就可以完成AOP功能,不了解AOP原理的话,是不大看的出AOP是基于动态代理的。本文的主要内容是Spring AOP的前世(Spring 1.2),以便能更好的理解动态代理转换到Spring AOP的过程。
有三篇文章,循序渐进的顺序为:1.代理模式学习笔记,2.本文(Spring AOP低级别支持),3.Spring相关文档翻译-chapter9(AOP部分)(Spring AOP高级别支持)
动态代理可以参考文章1。动态代理是由JDK自己为基类生成代理,在本章中Spring将此功能封装进了ProxyFactoryBean中。
一点基础知识
1.aop技术基于动态代理模式。动态代理模式实现由两种方式:基于JDK和基于CGLIB。我们知道JDK动态代理生成,需要被代理类实现一个或多个接口;如果要为一个没有实现接口的被代理类生成动态代理,就需要使用CGLIB。这也是Spring AOP自动代理所使用的规则。
2.低级别的Spring AOP支持,生成动态代理的主要类是ProxyFactoryBean。该类调用getObject()生成动态代理,生成方式取决于被代理类是否实现了接口。
3.ProxyFactoryBean定义时,有几个主要属性:
1)proxyInterfaces:指定被代理类实现的接口。不设置此属性,创建CGLIB代理。
2)target:指定被代理类
3)InterceptorNames:指定方法调用时,需要切入的消息或拦截器队列。此队列有先后顺序。
4)proxyTargetClass:默认为false。创建动态代理的方式
5)exposeProxy:默认为false。如果true,将代理暴露给本地线程,target可以通过AopContext.currentProxy()方法获取获取自己的代理。
6)singleton:默认为true。创建的消息为单例。如果要创建消息带有状态,需要设置为false。
关于上面几个属性的设置说明:
1)关于这几个参数,可以类比JDK生成动态代理的语句:Proxy.newProxyInstance(ClassLoarder loader,Class<?>[] interfaces,InvocationHandler h)。loader参数是我们web程序的类加载器,相当于上面的target(ClassLoarder loader = target.getClass().getClassLoader(););interfaces参数相当于上面的proxyInterfaces;h与InterceptorNames指定的类功能一致:在方法调用前后添加一些功能。
2)如果需要被代理的target没有实现任何接口,那么spring会为其创建一个基于CGLIB的动态代理。这是spring 2.0以后新增的功能。因为指定了一个实现类的实例target,那么根据此实例,可以获取其实现了的接口列表:target.getClass().getInterfaces()。那么指定proxyInterfaces属性就显得有些多余,spring也是如此做的:在proxyInterfaces属性中指明target所实现的全部接口,与省略proxyInterfaces属性,效果相同,都会生成一个覆盖了全部接口的代理。所以,如果target实现了4个接口,而你的proxyInterfaces属性只指明了少于4个的接口,spring会认为是开发者故意为之,只会生成实现了指定接口的代理。
3)如果target有实现的接口,那么生成proxy的类型,就由ProxyFactoryBean的配置决定:只要proxyTargetClass设置为true,不论proxyInterfaces属性是否指定,生成CGLIB代理。proxyTargetClass属性默认为false。
4)如果没有特殊要求,proxyInterfaces属性、proxyTargetClass属性都可以省略。交给spring自己决定如何创建代理。
ProxyFactoryBean使用
before advice
package com.business.born;
public interface IHello {
void sayHello(String name);
void danceHey(String type);
}
然后,该接口的实现类:
package com.business.born;
public class HelloLiu implements IHello {
@Override
public void sayHello(String name) {
System.out.println("你好,"+name);
}
@Override
public void danceHey(String type) {
System.out.println("邀请你跳舞,"+type);
}
}
package com.business.aop;
import java.lang.reflect.Method;
import org.springframework.aop.MethodBeforeAdvice;
public class DoBeforAdvice implements MethodBeforeAdvice {
@Override
public void before(Method method, Object[] args, Object target)
throws Throwable {
System.out.println("------在方法前做事 start");
System.out.println("调用方法:"+method.getName());
System.out.print("方法参数:");
for(Object arg:args){
System.out.print(arg);
}
System.out.println();
System.out.println("方法所在类:"+target.getClass().getName());
System.out.println("######在方法前做事 end");
}
}
接着,在Spring.xml中配置如下:
<?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:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="iHello" class="com.business.born.HelloLiu"/>
<bean id="doBeforeAdvice" class="com.business.aop.DoBeforAdvice"/>
<bean id="helloProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
<!-- HelloLiu实现类实现了接口,spring可以自动检测,用JDK创建动态代理。所以proxyInterfaces属性也不用设置 -->
<!-- <property name="proxyInterfaces"> -->
<!-- <value>com.business.born.IHello</value> -->
<!-- </property> -->
<property name="target">
<ref bean="iHello"/>
</property>
<property name="interceptorNames">
<list>
<value>doBeforeAdvice</value>
</list>
</property>
</bean>
</beans>
最后,junit测试:
package com.business.junit;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.business.born.IHello;
public class AopTest {
@Test
public void helloBeforeAdivceTest(){
ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
IHello iHello = (IHello) ac.getBean("helloProxy");
iHello.sayHello("2015元旦");
iHello.danceHey("华尔兹");
}
}
输出结果如下,不解释:
***方法调用前,输出调用方法的信息:
调用类:com.business.born.HelloLiu
调用方法:sayHello
方法参数:
2015元旦
准备调用方法...
你好,2015元旦
***方法调用后,输出返回值和方法执行时间:
返回值:null
执行耗时:0ms
***方法调用前,输出调用方法的信息:
调用类:com.business.born.HelloLiu
调用方法:danceHey
方法参数:
华尔兹
准备调用方法...
邀请你跳舞,华尔兹
***方法调用后,输出返回值和方法执行时间:
返回值:null
执行耗时:0ms
上面的例子是before advice。配置一个advice需要注意的点有:
1)自定义不同的advice,需要实现不同的接口。(至于是哪些接口,在下面的例子中)。
2)advice在spring配置文件中,需要以bean的形式定义,就想一个普通类的bean定义一样。
3)定义一个代理bean,改bean的名称(如helloProxy)就是我们在客户端中引用的代理名称,class属性为ProxyFactoryBean。该类中的属性设置,可以关联被代理类和advice,以及一些参数设置。
4)注意,此时虽然我们有一个名称为iHello的bean,但实际上我们在测试类中并不是调用此bean,而是调用spring为我们生成的代理bean(helloProxy),以此方式来控制我们队iHello bean的访问。
5)before advice这几步的定义,同样适用于around、after-returning、throw advice。这几个类的下面例子只贴了advice类代码,其他的测试配置步骤一样,你可以看出实现不同advice需要实现的不同接口。
around advice
package com.business.aop;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
public class DoAroundAdvice implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation paramMethodInvocation)
throws Throwable {
Object result = null;
System.out.println("***方法调用前,输出调用方法的信息:");
Object[] args = paramMethodInvocation.getArguments();
System.out.println("调用类:"+paramMethodInvocation.getThis().getClass().getName());
System.out.println("调用方法:"+paramMethodInvocation.getMethod().getName());
System.out.println("方法参数:");
for(Object arg:args){
System.out.print(arg+" ");
}
System.out.println("\n准备调用方法...");
Long beforTimes = System.currentTimeMillis();
result = paramMethodInvocation.proceed();
System.out.println("***方法调用后,输出返回值和方法执行时间:");
System.out.println("返回值:"+result);
Long time = System.currentTimeMillis() - beforTimes;
System.out.println("执行耗时:"+time+"ms");
return result;
}
}
After returning advice
package com.business.aop;
import java.lang.reflect.Method;
import org.springframework.aop.AfterReturningAdvice;
public class DoAfterAdvice implements AfterReturningAdvice {
@Override
public void afterReturning(Object returnValue, Method method,
Object[] args, Object target) throws Throwable {
System.out.println("------after returning ..start");
System.out.println("调用类:"+target.getClass().getName());
System.out.println("调用方法:"+method.getName());
System.out.print("方法传入参数:");
for(Object arg:args){
System.out.print(arg+" ");
}
System.out.println("\n返回值:"+returnValue);
System.out.println("######after returning ..end");
}
}
throwing advice
package com.business.aop;
import java.lang.reflect.Method;
import java.rmi.RemoteException;
import javax.servlet.ServletException;
import org.springframework.aop.ThrowsAdvice;
public class DoThrowAdvice implements ThrowsAdvice {
/**
* ThrowAdvice接口只是一个tag接口,接口中并为包含任何方法。
* afterThrowing()方法必须自己添加,只要方法签名参数不同,就可以添加任意多的同名方法。
* @param re
*/
public void afterThrowing(RemoteException re){
System.out.println("Java RMI 调用异常!");
}
public void afterThrowing(ServletException se){
System.out.println("servlet调用,发生异常!");
}
/**
* 前三个参数可以省略,取决于开发者是否需要获取异常抛出点的信息。
* @param m
* @param args
* @param target
* @param e
*/
public void afterThrowing(Method m,Object[] args,Object target,Exception e){
System.out.println("DoThrowAdvice调用:");
System.out.println("调用类:"+target.getClass().getName());
System.out.println("调用方法:"+m.getName());
System.out.print("方法参数:");
for(Object arg:args){
System.out.print(arg + " ");
}
System.out.println("\n抛出异常:"+e.getMessage());
}
}
Introduction advice
例子一:资源访问初始化
Introduction advice与上面的几种advice有些不同。它的功能指定某些类,让这些类实现一个共同的接口,也就是引入一个新类,已扩充原类的功能。package com.business.init;
import java.util.List;
/**
* 原功能接口
*/
public interface IResourceAccess {
List getUserInfo();
boolean isUserRegist(String id);
}
package com.business.init;
import java.util.List;
/**
* 原接口功能实现类
*/
public class ResourceAccessImpl implements IResourceAccess {
@Override
public List getUserInfo() {
System.out.println("访问用户资源..");
return null;
}
@Override
public boolean isUserRegist(String id) {
System.out.println("通过用户id,判断此用户信息是否注册..");
return false;
}
}
package com.business.init;
/**
* 扩展功能接口
*/
public interface IinitResource {
void initResouce();
boolean inited();
}
package com.business.init;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.support.DelegatingIntroductionInterceptor;
/**
* 扩展功能接口的实现类
* 此类继承DelegatingIntroductionInterceptor类,可以通过invoke方法将原功能接口的方法暴露出来
* 在invoke的前后,你可以像around-advice一样,做一些操作。
* 如果你并不需要获取原方法的调用信息,invoke方法无需重写
*/
public class InitResourceImpl extends DelegatingIntroductionInterceptor implements IinitResource {
<span style="white-space:pre"> </span>private static final long serialVersionUID = 3603308890579750856L;
<span style="white-space:pre"> </span>
<span style="white-space:pre"> </span>private boolean inited = false;
<span style="white-space:pre"> </span>@Override
<span style="white-space:pre"> </span>public void initResouce() {
<span style="white-space:pre"> </span>System.out.println("初始化资源..");
<span style="white-space:pre"> </span>this.inited = true;
<span style="white-space:pre"> </span>}
<span style="white-space:pre"> </span>@Override
<span style="white-space:pre"> </span>public boolean inited() {
<span style="white-space:pre"> </span>return inited;
<span style="white-space:pre"> </span>}
<span style="white-space:pre"> </span>
<span style="white-space:pre"> </span>@Override
<span style="white-space:pre"> </span>public Object invoke(MethodInvocation mi) throws Throwable{
<span style="white-space:pre"> </span>//you can do something here ..
<span style="white-space:pre"> </span>//if you don't need do something here,Override invoke(MethodInvocation mi) method is not necessary.
<span style="white-space:pre"> </span>return super.invoke(mi);
<span style="white-space:pre"> </span>}
}
package com.business.init;
import org.springframework.aop.support.DefaultIntroductionAdvisor;
/**
* 创建Introduction-advice
*/
public class InitResourceAdvisor extends DefaultIntroductionAdvisor {
/**
* 构造方法:为扩展功能接口创建AOP可引用的Advisor
*/
public InitResourceAdvisor(){
super(new InitResourceImpl(),IinitResource.class);
}
}
接下来是此Introduction advice在spring XML中的定义配置:
<span style="white-space:pre"> </span><!-- 初始化资源,Introduction-advice测试定义 -->
<!-- 1.定义原接口功能实现类bean -->
<bean id="resourceAccess" class="com.business.init.ResourceAccessImpl"/>
<!-- 2.定义Introduction-advice bean,bean中包含“扩展功能接口”实现类的引用-->
<bean id="initAdvisor" class="com.business.init.InitResourceAdvisor"/>
<!-- 3.为ProxyFactoryBean生成的动态代理定义一个引用 -->
<bean id="resourceAccessProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="proxyInterfaces">
<value>com.business.init.IResourceAccess</value>
</property>
<property name="target" ref="resourceAccess"/>
<property name="interceptorNames">
<list>
<value>initAdvisor</value>
</list>
</property>
</bean>
接下来使用此Introduction advice来测试:
<span style="white-space:pre"> </span>@Test
public void introductionAdvisorInitResourceTest(){
ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
//代理类原功能的引用
IResourceAccess ira = (IResourceAccess)ac.getBean("resourceAccessProxy");
ira.isUserRegist("132213");
ira.getUserInfo();
System.out.println("----");
//代理类扩展功能的引用
IinitResource iir = (IinitResource)ira;
//调用扩展功能,如果数据没有初始化,先初始化资源,再访问
if(!iir.inited()){
iir.initResouce();
}
//初始化完成,继续业务逻辑的访问
ira.isUserRegist("132213");
ira.getUserInfo();
}
测试结果如下:
通过用户id,判断此用户信息是否注册..
访问用户资源..
----
初始化资源..
通过用户id,判断此用户信息是否注册..
访问用户资源..
这里通过resourceAccessProxy bean获得的一个对象,此对象同时实现了原接口和扩展接口,要调用不同接口中的方法时,将此对象向上转型为对应的接口在调用即可。
例子二:Spring的例子为方法加锁
package com.business.lock;
public interface Lockable {
void lock();
void unlock();
boolean locked();
}
2)扩展接口实现
package com.business.lock;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.support.DelegatingIntroductionInterceptor;
public class LockMixin extends DelegatingIntroductionInterceptor implements Lockable {
private boolean locked;
@Override
public void lock() {
this.locked = true;
}
@Override
public void unlock() {
this.locked = false;
}
@Override
public boolean locked() {
return this.locked;
}
@Override
public Object invoke(MethodInvocation mi) throws Throwable{
if(locked() && mi.getMethod().getName().indexOf("say")>-1){<span style="white-space:pre"> </span>//我这里修改后,不是锁定set方法,而是锁定包含say字符串的方法
throw new MethodLockException();
}
return super.invoke(mi);
}
}
3)Introduction advice
package com.business.lock;
import org.springframework.aop.support.DefaultIntroductionAdvisor;
public class LockMixinAdvisor extends DefaultIntroductionAdvisor {
private static final long serialVersionUID = -2046296275775575276L;
public LockMixinAdvisor() {
super(new LockMixin(),Lockable.class);
}
}
4)自定义异常
package com.business.lock;
public class MethodLockException extends Exception {
public MethodLockException(){
super("调用方法被锁定。");
}
public MethodLockException(String msg){
super(msg);
}
}
5)测试代码
<span style="white-space:pre"> </span>@Test
public void introductionAdvisorLockableTest(){
ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
/*
* ac.getBean("lockProxy")获取到的代理实例包含两个接口的功能:IHello和Lockable
* 但是调用每个接口中的方法时,需向上转型到指定接口,再调用各自接口中的方法。
* 扩展接口实现类继承的DelegatingIntroductionInterceptor中可以暴露原接口中的方法,
* 如果扩展功能涉及到原功能(原方法),可以通过重新invoke方法来实现。
* 原接口与扩展接口的关联(暴露invoke()方法),是由ProxyFactoryBean来实现的。
*/
IHello helloProxy = (IHello)ac.getBean("lockProxy");
Lockable lockProxy = (Lockable)helloProxy;
helloProxy.sayHello("阿毛");
helloProxy.danceHey("交谊舞");
lockProxy.lock();
helloProxy.sayHello("阿毛阿毛");
}
关于Introduction advice的思考
使用auto-proxy功能
用于自动为bean生成代理的类有:BeanNameAutoProxyCreator和DefaultAdvisorAutoProxyCreator,前者根据bean名称来自动生成代理,多个bean名以逗号分隔;后者根据spring上下文中定义的advisor,来自动为bean生成代理。<span style="white-space:pre"> </span><!-- 1.定义原接口功能实现类bean -->
<bean id="resourceAccess" class="com.business.init.ResourceAccessImpl"/>
<!-- 2.定义Introduction-advice bean,bean中包含“扩展功能接口”实现类的引用-->
<bean id="initAdvisor" class="com.business.init.InitResourceAdvisor"/>
<!-- 通过bean名称,为多个bean自动生成代理 -->
<bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
<property name="beanNames">
<value>resourceAccess,iHello</value>
</property>
<property name="interceptorNames">
<list>
<value>initAdvisor</value>
</list>
</property>
</bean>
无论BeanNameAutoProxyCreator,还是DefaultAdvisorAutoProxyCreator,在定义bean时,无需指定id属性。当在客户端中要访问代理类时,直接获取原类的bean id即可,因为此时的bean,已经是代理类(这样是有好处的,可以隐藏被代理类,不被访问)。比如,上面我们要获得ResourceAccessImpl类的代理,我们只需要在测试类中用下面语句:
//代理类原功能的引用,与上面测试的不同在于bean名称的改变,看似调用的是本来的实现,其实这是此类的代理引用
IResourceAccess ira = (IResourceAccess)ac.getBean("resourceAccess");