上篇讲述了Spring的IOC原理和使用,本篇讲述Spring对AOP的支持。首先回顾一下Spring IOC容器,用一种通俗的方式理解Spring的IOC,也就是家里要安装灯泡,去网上买,我们只需要去下订单就(ApplicationContext.xml)可以了,无需关心工厂是如何加工的,你想要灯泡发红的光就直接在选择的时候选择红光,如果想要发黄色光的就直接选择发黄色光的灯牌,之后生成订单后会有派件人员直接派送到你的家门口,不需要你自己创建灯泡工厂去生产(new)灯泡。
那什么是Spring的AOP呢?
我们可以理解为你想要给灯安装一个灯罩,可以直接把灯罩起来,而这个灯罩相对于灯本身来说没有任何的关系,是独立存在的,你只要加上去就可以。对于这个灯罩来说,就是从AOP的角度去分析了。
那究竟什么是Spring的IOC容器呢?
在软件业,AOP是AspectOriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。也可以意为面向行为编程,是函数式编程的一种衍生范型。
我们以一个例子来说明什么是AOP。
例如我们在用户增删改查上添加一个安全性检查,这样无论是任何操作之前都要进行安全性检查。
UserManager.java接口代码如下所示。
public interface UserManager { public void addUser(String name,String password); public void delUser(int userId); public String findUserById(int userId); public void modifyUser(int userId,String username,String password); }
UserManagerImpl.java实现接口代码如下所示。
public class UserManagerImpl implements UserManager { @Override public void addUser(String name, String password) { checkSecurity(); System.out.println("------------UserManagerImpl.add()----"); } @Override public void delUser(int userId) { checkSecurity(); System.out.println("------------UserManagerImpl.del()----"); } @Override public String findUserById(int userId) { checkSecurity(); System.out.println("------------UserManagerImpl.findUserById()----"); return "张三"; } @Override public void modifyUser(int userId, String username, String password) { checkSecurity(); System.out.println("------------UserManagerImpl.ModifyUser()----"); } //检查安全性. //当安全性不需要时就要成百上千给去改动, //我们就可以用代理模式,让代理去做,让代理去控制目标,代理做某件事情. private void checkSecurity() { System.out.println("------------UserManagerImpl.checkSecurity()----"); } }
这样的话如果我们想要更改的话就要在这个类上对安全性检查进行更改,添加的话也需要修改这个类的代码进行添加,这不符合我们设计原则,开闭原则,对扩展开放,对修改关闭。那怎么办呢?我们从中可以发现,检查安全性来说对增删改查的操作没有影响,是一项独立存在的方法或独立存在的服务,这样我们可以把检查安全性这以服务抽取出来放到UserManagerImpl类的代理类中,这样去分开这个独立的服务。让代理去实现相同的接口,调用UserManagerImpl中的方法,把服务单独的放到代理类中。这样单独的管理这项服务。
使用静态代理解决问题
代理类代码如下所示。UserManagerImplProxy.java,加上代理类之后我们就可以去掉UsermanagerImpl.java中的安全性检查方法。
/** * 代理类和目标做的事情是一样的,所以实现相同的接口. * @author Summer * */ public class UserManagerImplProxy implements UserManager { //目标引用. private UserManager userManager; //通过构造方法传递过来. public UserManagerImplProxy(UserManager userManager) { this.userManager = userManager; } @Override public void addUser(String username, String password) { checkSecurity(); userManager.addUser(username, password); } @Override public void delUser(int userId) { // TODO Auto-generated method stub checkSecurity(); userManager.delUser(userId); } @Override public String findUserById(int userId) { checkSecurity(); return userManager.findUserById(userId); } @Override public void modifyUser(int userId, String username, String password) { checkSecurity(); userManager.modifyUser(userId, username, password); } private void checkSecurity() { System.out.println("------------UserManagerImpl.checkSecurity()----"); } }
但问题是要去修改的这个安全性检查的方法的话,就要去修改代理类了,如果很多方法,就要修改很多次,会引来同样的开闭问题。
我们应该剥离出来,让这个方法在运行的时候动态的加入进去,方法只写一次,从AOP的角度来说,这种遍布在系统中的独立的服务,称为横切性的关注点。纵向的是方法一调用方法二,方法二调用方法三,而这个服务是切入到各个方法上的,所以认为是横向的。我们可以使用动态代理来解决这个问题。
使用动态代理解决问题
动态代理,我们要实现一个系统的处理器InvocationHandler,它可以复用,不需要每个方法一个代理,并且是在运行时声明出来的,可以为各个接口服务。把横切性的问题拿出来放到一个单独的类中,上述例子中,我们删除我们自己手动写的代理类,新建SecurityHandler类,实现InvocationHandler这个接口。
代码如下所示。
import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; public class SecurityHandler implements InvocationHandler { private Object targetObject; public Object createProxyInstance(Object targetobject) { this.targetObject = targetobject; //根据目标生成代理. //代理是对接口来说的,是拿到目标的接口.拿到这个接口,实现这个接口. //targetobject这个目标类实现了哪个接口. //返回代理. return Proxy.newProxyInstance(targetobject.getClass().getClassLoader(), targetobject.getClass().getInterfaces(), this); } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //检查安全性. checkSecurity(); //当前方法,调用目标对象的方法. //调用目标方法. Object ret = method.invoke(targetObject, args); return ret; } //检查安全性. private void checkSecurity() { System.out.println("------------UserManagerImpl.checkSecurity()----"); } }
这个代理类是运行时创建的,是动态的,而编译时创建的是静态的。TargetObject目标,建立目标实例createProxyInstance(),根据目标生成代理Proxy.newProxyInstance(),在invoke()方法中设置允不允许调用真正的对象,在invoke()中进行检查安全性控制,在invoke()中统一做控制,就像Filter中的doFilter方法。
客户端调用,代码如下所示。
public class Client { /** * @param args */ public static void main(String[] args) { SecurityHandler hander = new SecurityHandler(); //返回的是一个代理类,不是真正的UserManager.而是指向代理类.在内存中. UserManager userManager = (UserManager)hander.createProxyInstance(new UserManagerImpl()); //使用的是代理. //共同的接口,不同的实现. userManager.addUser("张三", "123"); } }
接下来让我们走入Spring的AOP,他是如何实现的呢?如下图所示。
AOP中的基本概念和术语。
1、 切面(Aspect)
切面就是一个抽象出来的功能模块的实现,例如上述安全性检查,可以把这个功能从系统中抽象出来,用一个独立的模块描述,这个独立的模块就是切面,我们的SecutiryHinder类。
2、 连接点(JoinPoint)
连接点即程序运行过程中的某个阶段点,可以是方法调用、异常抛出等,在这个阶段点可以引入切面,从而给系统增加新的服务功能。
3、 通知(Advice)
通知即在某个连接点所采用的处理逻辑,即切面功能的实际实现。通知的类型包括Before、After、Around以及Throw四种,下篇文章将会展示通知的使用方法。
4、 切入点(PointCut)
接入点即一系列连接点的集合,它指明通知将在何时被触发。可以指定能类名,也可以是匹配类名和方法名的正则表达式。当匹配的类或者方法被调用的时候,就会在这个匹配的地方应用通知。
5、 目标对象(Target)
目标对象就是被通知的对象,在AOP中,目标对象可以专心实现自身的业务逻辑,通知功能可以在程序运行期间自动引入。
6、 代理(Proxy)
代理是目标对象中使用通知以后创建的对象,这个对象既拥有目标对象的全部功能,而且拥有通知提供的附加功能。
理解上述术语还是先用比较通俗的方式来解释,对于切面,我们在中学中学习过,切线,切面,切点,对于数学中学习的我们一定都不陌生。
如果仍然还是抽象的话,我们知道嫦娥一号与月球对接,嫦娥一号所要完成的任务或功能就是我们的切面(Aspect),带探究月球生命的使命;连接点(JoinPoint)就是总台发出命令,来触发嫦娥一号升空,这个发出的命令触发点就是JoinPoint;通知(Advice)就是具体的去执行总台的命令,包括集中类型是在总台发出命令之前(Before)、之后(After)、Around(大约多长时间)、或系统出现错误(Throw),去执行具体的操作。切入点(PointCut)就是总台发出取出月球特殊植物,嫦娥一号进行匹配特殊的植物,并通知是否找到成功或失败。那嫦娥一号这个对象本身就是我们的代理(Proxy),而真正的目标对象是总台(大概理解一下,不到之处,望原谅)。
我们这里的横切性关注点(Coreconcerns)是指安全性服务,对于安全性的检查可能分不到各个模块中。而切面(Aspect)是一个关注点的模块化,从软件角度来说是指应用程序在不同模块的某一个领域或方面。连接点(Joinpoint)程序执行过程中某个特殊的点,比如方法调用或处理异常的时候,上述我们再调用增删改查方法的时候。切入点(Pointcuts)是连接点的集合。通知(Advice)是实际的逻辑实现,即实际实现安全服务的那个方法。
通知有四种类型,前置通知(Beforeadvice):在切入点匹配方法之前使用。返回后通知(Afterreturning advice):在切入点匹配方法返回的时候执行。抛出后通知(Afterthrowing advice):在切入点匹配的方法执行时抛出异常的时候运行。后通知(After(finally)advice):无论切入点匹配方法是否正常结束,还是抛出异常结束的,在它结束(finally)后通知(afteradvice)。环绕通知(AroundAdvice):环绕通知既在切入点匹配的方法执行之前又再执行之后运行。并且可以决定这个方法在什么时候执行,如何执行,设置是是否执行。
晦涩难懂的东西还是用实例来说明,见下一篇SpringAOP实例。