Spring AOP思想的理解和简单实现

来源:互联网 发布:凯立德地图修改端口 编辑:程序博客网 时间:2024/05/22 10:23
Spring之Aop的简单实现

所谓Aop,即Aspect Oriented Programming,面向方面编程。这个概念听起来可能有点抽象,所以在这里我们先引入Aop中的一些术语并结合它们来对Aop思想做一个整体的解释:

1.Aspect(切面):横切性关注点的抽象即为切面。记得有这么个俗语,意思就是一根筷子容易折断,而一捆筷子就不容易折断了,说的是团结的力量。那么,现在,大家想一下,如果我们手里拿着一把刀,要斩断一捆筷子(由十根筷子组成),我们要怎么办呢?答案是明显的,就是横着砍下去!我想应该没有人会选择竖着砍下去的,呵呵。那么,在砍的那个过程中,我们要关注的地方有哪些呢?或者说我们要砍断的筷子有哪几根呢?答案还是那么明显,当然是要把十根筷子都砍断咯。那么,对于这捆筷子中的每一根都可以理解为是一个横切性关注点。由于个人的想象力有限,所以举的例子不免有些牵强。好了,我们继续来解释切面的概念,那么在编程中,横切性关注点是什么呢?实际上,简单的来说,就是程序运行时我们要对哪些方法进行拦截,拦截后要做些什么事(例如可以对部分函数的调用进行日志记录),这些都算是横切性关注点。上面说了切面是横切性关注点的抽象,这里,我们可以结合面向对象的概念来理解。大家都知道的是,类是物体特征的抽象,所以结合这个来理解切面的意思应该会容易一点。
2.Joinpoint(连接点):顾名思义,连接点的作用就是可以在上面接一点东西。实际上,Aop中的连接点的意思也差不多,就是那些被我们拦截到的点(Spring中,这些点指的就是方法,因为Spring只支持方法类型的拦截点),那么我们拦截一个方法的目的是什么呢?当然是为了附带做一些事(即执行一些代码)啦,或者说是接入一些执行代码,所以被拦截的方法我们可以称之为接入点。
3.Pointcut(切点):切点用于指定或定义希望在程序流中拦截的Joinpoint。切点还包含一个通知(所谓通知,即拦截到Joinpoint后我们所要附带做的事,如方法的调用等等),该通知在连接点被拦截时发生,因此如果在一个调用的特定方法上定义一个切点,那么在调用该方法时,Aop框架将截获该切点,并执行切点所关联的通知。
4.Advice(通知):所谓通知,就是拦截到Joinpoint后我们所要做的事情(通常就是执行一些代码)。通知可分为前置通知、后置通知、异常通知、最终通知、环绕通知(对于各种通知的解释,这里不再做详细介绍了)。
5.Target Object(目标对象):包含连接点的对象,也称之为被通知或被代理对象。因为Aop是需要通过代理机制来实现的。对于目标对象的方法调用,实际上是通过它的代理对象来进行的,因此可以在代理对象中对调用操作进行拦截,然后执行切点的相关通知。
6.Aop Proxy(Aop代理):Aop框架为目标对象创建的代理对象,包含了通知。在Spring中,AOP代理可以是JDK动态代理或CGLIB代理。
7.Weave(织入):将Aspect运用到Target对象并导致proxy对象创建的这个过程。
8.Introduction(引入):添加方法或属性到被通知的类。在不修改任何源代码的情况下,Introduction可以在程序运行期间动态地为类添加一些方法和属性。

接着,我们结合上面的提到的Aop的这些术语来对Aop做一番解释。我们不妨引入一个需求(或者可称之为Aop思想的动机,即为什么要提出Aop思想):监控部分重要函数的执行时间,并以短信的方式通知相关人员。那么这个需求该怎么实现呢?或许会有朋友说,这个太简单了,不就是直接在方法体内添加发送短信的代码嘛。但是,如果要监控的函数很多呢?逐个地方手动地进行添加?如果需求突然变更了,要求将发短信改为发邮件,那该怎么办呢?如果要监控的目标函数也要发生改变那又该怎么办呢?所以,将代码写死绝对不会是一个程序设计的好思想,也不会是一个合格的编程人员应该做的。那么,Aop思想将可以用来很好地解决这个问题,它对于该需求的实现过程是这样的:
(1)将要监控的函数定义为切点,也就是说把要监控的函数当作接入点,并且定义到配置文件中去,这样就我们可以动态地修改切点了。
(2)为包含接入点的目标对象定义Aop代理(实际上可以只定义一个Aop代理来作为多个目标对象的代理)
(3)将发送短信的代码封装为一个类的方法(我们称之为通知方法),并且抽取该类的接口,然后我们在实际编码中使用的是接口,并把具体的通知类定义到配置文件中去,这样有便于我们以后做扩展。
(4)实现Aop代理的invoke方法,并在invoke中调用接入点方法,且在调用之后接着调用通知的方法(也就是发短信或者发邮件的方法)。
好了,对于刚才我们引入的需求,运用Aop思想,其大致的实现就是上面几步。那么,当需求变更为“发送邮件”时,我们只需要改变一下配置文件中的通知类就可以了;当要监控的函数需要改变时,我们也只需要改一下配置文件中对于切点的配置就可以了。
下面,我们来看一下Aop思想的一个简单实现的例子(该例子利用Aop思想完成一件事就是当我们调用UserDao对象的saveUser方法时,系统会进行日志记录):
1. 编写IOC容器要用到的配置文件(用来配置bean对象和切点)——beans.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans">
<bean id="UserDao" class="AopTest.UserDao"></bean>
<bean id="LogTool" class="AopTest.LogTool"></bean>
<aop id="logging" ref="LogTool" pointCut="UserDao.*" method="before"></aop>
</beans>

2.编写bean节点配置类,用来封装配置文件中所配置的bean对象的信息——BeanNode.java
package AopTest;

/**
* xml配置文件中配置的bean节点的映射对象
* @author Administrator
*
*/
public class BeanNode
{
private String id;
private String className;

public BeanNode()
{
super();
}

public BeanNode(String id, String className)
{
this.id=id;
this.className=className;
}

public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getClassName() {
return className;
}
public void setClassName(String className) {
this.className = className;
}


}

3.编写aop节点配置类——AopNode.java
package AopTest;

/**
* xml配置文件中配置的aop节点的映射对象
* @author Administrator
*
*/
public class AopNode
{
private String id;
private String ref;
private String pointCut;
private String method;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getRef() {
return ref;
}
public void setRef(String ref) {
this.ref = ref;
}
public String getPointCut() {
return pointCut;
}
public void setPointCut(String pointCut) {
this.pointCut = pointCut;
}
public String getMethod() {
return method;
}
public void setMethod(String method) {
this.method = method;
}


}

4.编写自定义IOC容器,用来管理配置文件中配置的bean对象——IocContainer.java
package AopTest;

import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.XPath;
import org.dom4j.io.SAXReader;

/**
* 自定义Ioc容器
* @author Administrator
*
*/
public class IocContainer
{
private static java.util.Map<String,BeanNode> beanMap=new java.util.HashMap<String, BeanNode>();
private static java.util.List<AopNode> aopNodes=new java.util.ArrayList<AopNode>();


/**
* 解析配置好的xml文件,读取bean节点对象以及aop节点对象,并添加到队列中
* @param fileName
*/
private void readXML(String fileName)
{
//创建一个文件xml文件读取器
SAXReader saxReader=new SAXReader();
Document document=null;

try
{
//取得类的类装载器,通过类装载器,取得类路径下的文件
java.net.URL xmlPath=this.getClass().getClassLoader().getResource(fileName);
//读取文件内容
System.out.println("xmlPath="+xmlPath);
document=saxReader.read(xmlPath);
//创建一个map对象
java.util.Map<String, String> nameSpaceMap=new java.util.HashMap<String,String>();
//加入命名空间
nameSpaceMap.put("nameSpace", "http://www.springframework.org/schema/beans");

//创建beans/bean查询路径
XPath xpath1=document.createXPath("nameSpace:beans/nameSpace:bean");
//设置命名空间
xpath1.setNamespaceURIs(nameSpaceMap);
//获取文档下所有bean节点
java.util.List<Element> beans=xpath1.selectNodes(document);
System.out.println("bean对象的个数为:"+beans.size());

for(int i=0; i<beans.size(); i++)
{
Element element=beans.get(i);
//获取bean节点对象的id属性
String id=element.attributeValue("id");
System.out.println("bean对象的id为:"+id);
//获取bean节点对象的className属性
String className=element.attributeValue("class");
//创建bean节点对象
BeanNode beanNode=new BeanNode(id, className);
beanMap.put(beanNode.getId(), beanNode);
}

//创建beans/aop查询路径
XPath xpath2=document.createXPath("nameSpace:beans/nameSpace:aop");
//设置命名空间
xpath2.setNamespaceURIs(nameSpaceMap);
//获取文档下所有aop节点
java.util.List<Element> aops=xpath2.selectNodes(document);

for(int i=0; i<aops.size(); i++)
{
Element element=aops.get(i);
//获取aop节点对象的id属性
String id=element.attributeValue("id");
//获取aop节点对象的ref属性
String ref=element.attributeValue("ref");
//获取aop节点对象的pointCut属性
String pointCut=element.attributeValue("pointCut");
//获取aop节点对象的method属性
String method=element.attributeValue("method");

//创建aop节点对象
AopNode aopNode=new AopNode();
aopNode.setId(id);
aopNode.setRef(ref);
aopNode.setPointCut(pointCut);
aopNode.setMethod(method);
aopNodes.add(aopNode);
}

}


catch(Exception e)
{
e.printStackTrace();
}
}

/**
* 获取指定对象的代理对象,可以通过该代理对象执行指定对象的所有方法
* @param id
* @return
* @throws Exception
*/
public  Object getProxyOfBean(String id) throws Exception
{
//解析beans.xml文件
readXML("AopTest/beans.xml");
BeanNode beanNode=beanMap.get(id);
if(null==beanNode)
{
throw new Exception("没有这个东西!id="+id);
}

//得到配置的bean对象,注意,此处调用默认无参构造器
//通过反射机制实例化指定的bean对象
Class c=Class.forName(beanNode.getClassName());
Object  bean=c.newInstance();
//向代理对象传入代理配置参数(即aop节点中配置的参数)
UserDaoProxy.aopNodes=aopNodes;
//获取指定bean对象的代理对象
Object proxy=UserDaoProxy.getProxy(bean);
return proxy;

}

public static BeanNode getBeanNodeById(String id)
{
return beanMap.get(id);
}

//获得指定id的bean对象
public static Object getBean(String id) throws Exception
{
BeanNode beanMapping=beanMap.get(id);
if(null==beanMapping)
{
throw new Exception("没有这个东西!id="+id);
}
//得到配置的bean对象,注意,此处调用默认无参构造器
Class c=Class.forName(beanMapping.getClassName());
Object  bean=c.newInstance();
return bean;
}
}
5.编写目标对象接口——IUserDao.java
package AopTest;

/**
* userDao接口
* @author Administrator
*
*/
public interface IUserDao
{
/**
* 根据用户名和密码,保存用户对象
* @param userName
* @param userPwd
*/
void saveUser(String userName, String userPwd);

/**
* 根据用户id删除用户对象
* @param id
*/
void deleteUser(int id);
}

6.编写目标对象类(实现目标对象接口类)——UserDao.java
package AopTest;

public class UserDao implements IUserDao
{
/**
* 根据用户名和密码,保存用户对象
* @param userName
* @param userPwd
*/
public void saveUser(String userName, String userPwd)
{
System.out.println("保存用户对象成功,用户名:"+userName+" 密码:"+userPwd);
}

/**
* 根据用户id删除用户对象
* @param id
*/
public void deleteUser(int id)
{
System.out.println("删除用户对象成功,用户编号:"+id);
}
}

7.编写日志工具类接口,用来记录某方法调用的信息——ILogTool.java
package AopTest;

/**
* 日志记录工具接口
* @author Administrator
*
*/
public interface ILogTool
{
/**
* 在方法执行前记录日志
* @param m: 正在执行的方法
* @param args: 方法中的参数
*/
void before(java.lang.reflect.Method m, Object[] args);

/**
* 在方法执行之后记录日志
* @param m: 正在执行的方法
* @param args:方法中的参数
*/
void after(java.lang.reflect.Method m, Object[] args);
}

8.编写日志工具实现类——LogTool.java
package AopTest;

/**
* 日志工具实现类
* @author Administrator
*
*/
public class LogTool implements ILogTool
{
/**
* 在方法执行前记录日志
* @param m: 正在执行的方法
* @param args: 方法中的参数
*/
public void before(java.lang.reflect.Method m, Object[] args)
{
//获取方法名字
String methodName=m.getName();
//获取传入方法中的参数值
String paramValue="";
for(int i=0; i<args.length; i++)
{
Object param=args[i];
paramValue=paramValue+param.toString()+",";
}
System.out.println("before execute "+methodName+" method, params:"+paramValue);
}

/**
* 在方法执行之后记录日志
* @param m: 正在执行的方法
* @param args:方法中的参数
*/
public void after(java.lang.reflect.Method m, Object[] args)
{
//获取方法名
String methodName=m.getName();
//获取传入方法中的参数值
String paramValue=null;
for(int i=0; i<args.length; i++)
{
Object param=args[i];
paramValue=" "+param.toString();
}
System.out.println("after execute "+methodName+" params:"+paramValue);
}
}

9.编写目标对象的代理类——UserDaoProxy.java
package AopTest;

import java.lang.reflect.Method;

/**
* 通用代理类的实现
* Aop要通过jdk中的“代理”机制来实现
* 实际上Aop思想的实现就是将包含接入点的目标对象交给其代理对象来管理,客户端通过目标对象的代理对象来调用目标对象提供的接入点方法,
* 这样我们可以在代理对象中调用接入点方法时做其他一些处理,这里我们称之为通知
* @author Administrator
*
*/
public class UserDaoProxy implements java.lang.reflect.InvocationHandler
{
//被切点对象的配置列表,里面是xml中读取的AopXmlMapping对象
static java.util.List<AopNode> aopNodes;
//被代理的对象(即目标对象)
static Object proxiedObj;

/**
* 获取指定对象的代理对象
* @param obj
* @return
*/
public static Object getProxy(Object obj)
{
return java.lang.reflect.Proxy.newProxyInstance(obj.getClass().getClassLoader(),
obj.getClass().getInterfaces(),
new UserDaoProxy(obj));
}

private UserDaoProxy(Object obj)
{
this.proxiedObj=obj;
}

/**
* 实现InvocationHandler接口的invoke方法
* 代理对象被调用时,实际上是执行了该方法
* 因此那些被代理执行的方法应该写在该方法体内
*/
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable
{
Object result=null;
try
{
ILogTool logTool=null;
boolean needAop=false;
for(int i=0; i<aopNodes.size(); i++)
{
//获取aop节点的配置信息
AopNode aopNode=aopNodes.get(i);
//获取切入点或称之为连接点(也就是要切入到哪个对象的哪个方法上去)
String pointCut=aopNode.getPointCut();
//获取包含通知方法的对象(我们在aop节点中配置的是LogTool对象)
String logToolName=aopNode.getRef();
//找到日志工具对象——包含通知方法的对象
logTool=(LogTool)IocContainer.getBean(logToolName);
int limit=pointCut.indexOf(".");
//获取目标对象的beanId
String destPointBeanId=pointCut.substring(0,limit);//匹配beanid部分

//1.获取连接点(Spring中只支持方法类型的连接点)
String methodRane=pointCut.substring(limit+1,pointCut.length());//匹配方法部分
System.out.println("destPointBeanId:  "+destPointBeanId);
//获取目标对象的节点配置信息
BeanNode beanNode=IocContainer.getBeanNodeById(destPointBeanId);
System.out.println("目标对象的类名为: "+beanNode.getClassName());
System.out.println("被代理执行的对象的类名为: "+this.proxiedObj.getClass().getName());
//找到了要切入的目标对象!
if(null!=beanNode&&beanNode.getClassName().equals(this.proxiedObj.getClass().getName())){
if(methodRane.equals("*"))//被代理的对象的所有方法都要接受切入!
{
//2.获取连接点方法的名字,然后执行该方法
if(aopNode.getMethod().equals("before"))
{
//方法前切入(即前置通知)
//这里的method实际上就是客户端调用的被代理对象的方法
logTool.before(method, args);//3.执行要切入的方法(即通知)
//调用切入的目标对象(也就是被代理的对象)的方法(即连接点方法)
//invoke方法的第一个参数为method方法的拥有者,第二个参数为传入method方法的参数对象
result = method.invoke(this.proxiedObj, args);//4.通过代理对象执行连接点方法
needAop=true;
break;
}
else
{
//方法后切入(即后置通知)
//调用切入的目标对象(也就是被代理的对象)的方法(即连接点方法)
result = method.invoke(proxy, args);
logTool.after(method, args);//调用后切入!
needAop=true;
break;
}

}
}
}
if(!needAop)
{
result = method.invoke(proxy, args);//调用目标对象(也就是被代理的对象)的方法
}

catch (Exception e)
{
e.printStackTrace();
throw new RuntimeException("invocation : " + e.getMessage());

return result;
}
}

10.编写测试类——Tester.java
package AopTest;

public class Tester
{
public static void main(String[] args)
{
try
{
IocContainer ic=new IocContainer();
//获取UserDao对象的代理对象
IUserDao userDao=(IUserDao)ic.getProxyOfBean("UserDao");
userDao.saveUser("zzq", "123456");
}
catch(Exception e)
{
e.printStackTrace();
}

}
}

11.测试结果如下:
xmlPath=file:/F:/myeclipse6_workspace/netjava_web_project/WebRoot/WEB-INF/classes/AopTest/beans.xml
bean对象的个数为:2
bean对象的id为:UserDao
bean对象的id为:LogTool
destPointBeanId:  UserDao
目标对象的类名为: AopTest.UserDao
被代理执行的对象的类名为: AopTest.UserDao
before execute saveUser method, params:zzq,123456,
保存用户对象成功,用户名:zzq 密码:123456
0 0