上一章 详解SSJ(Spring3.x mvc + Spring3.x Core + JPA2.x)轻量级集成开发—第4章 剖析Annotations特性
下一章 详解SSJ(Spring3.x mvc + Spring3.x Core + JPA2.x)轻量级集成开发—第6章 剖析Spring3.x AOP特性02
目录
一、AOP简介;
二、为什么需要使用AOP;
三、设计模式之代理模式;
四、JDK动态代理实现;
五、Spring AOP简介;
六、Spring AOP术语;
七、基于Schema配置文件形式的Spring AOP;
前言
经过前面几个章节的学习,笔者已经详细的为大家讲解了关于Spring的内核技术(IOC)。那么从本章开始,笔者将会为大家讲解关于Spring AOP的一些技术。由于AOP的内容涉及范围较广,所以笔者仍然打算分成多章进行逐步讲解,希望大家能够耐心进行阅读。
笔者收到较多私信,很多朋友都是询问关于后期章节的内容安排,或者直接帖一大堆调试代码让笔者解释原因。对此笔者只能表示无奈,由于时间有限(高质量的博文需要笔者花费大量的时间去构思),加之年前项目周期较紧,所以笔者将不能及时回复,希望大家给予谅解。在此衷心的祝愿大家,新年快乐,奖金丰收。
一、AOP简介与实现
AOP(Aspect Oriented Programming,面向切面编程)简单来说就是在不改变源代码的前提下,通过前后横切的方式,动态添加新功能,这就是AOP。笔者经常听到很多朋友喜欢拿AOP与OOP(Object Oriented Programming,面向对象编程)进行比较,而且还分析得津津有味(曾出现在某些高校的学术论文上)。在此笔者希望大家能够明白AOP仅仅只是作为OOP的一种特性补充,如果脱离OOP,AOP将一无是处。
OOP延伸自POP(Procedure-Oriented Programming,面向过程编程),这才是一种创新的软件设计思想。换句话来说AOP其实是基于OOP的,算是一种改进,一种补充,但绝对算不上是一次实质性的飞跃。OOP是针对领域模型中的组件或者组件单元进行封装或抽象,其结构是按照顺序结构从上至下逐一执行。并且组件之间存在着相互关联,相互依赖,以高耦合的方式展现领域对象的职责,但这同时也暴露了OOP思想的缺陷(OOP本身就是基于依赖式设计)。而OOA的特性是模块横切点,与OOP不同的是,OOA更加关注内聚性。也就是说OOA是针对业务逻辑或控制逻辑中存在的通用模块进行抽取,所抽取出来的单元模块我们称之为横切点。并且在许多情况下,这些横切点并不属于业务逻辑或业务控制成员,AOP要做的事情就是对其进行逐步分离,然后让这些横切点以可插拔式的设计与其进行耦合。从这一点来看AOP和OOP的思想是截然不同的,所以从理论上来说你完全可以将OOA当做是对OOP的一种解耦优化。
二、为什么需要使用AOP
随着Object Oriented思想的大行其道。上至架构人员,下至是开发人员,心里边每天都在不停的思考着,如何让我的设计,我的代码更好的解耦,或许这便是软件工程永恒的话题。但是笔者同时也要告诉你,世界上没有最好的解耦方式。只要我们能够尽可能做到面向接口编程,遵循类型单一原则设计,善用设计模式,这样我们的设计才更具复用性、维护性、扩展性及伸缩性。
笔者上述章节已经提到过,AOP可以当做是OOP的一种解耦优化,那么接下来咱们就来看看AOP到底是以何种形式对OOP进行解耦的。首先来看基于OOP的设计,假设笔者有2个控制层(实际情况可能会更多),都需要请求同一业务操作响应处理。但是在请求之前,控制逻辑有必要对其进行一系列的处理操作,这些处理操作可能包含:权限检查、粗粒度日志记录等。如果按照OOP的设计思想咱们应该怎么做呢?很可能你的第一反应会首先编写一个通用的控制逻辑接口,然后指定其派生类实现重写所需的一系列处理操作。最后控制层在调用具体的业务操作之前(或者业务操作自身在执行之前),务必先要调用封装好的通用控制逻辑。如果你也是这么设计的,那么恭喜你,OOA(Object-Oriented Analysis,面向对象分析)和OOD(Object-Oriented Design,面向对象设计)你确实掌握的不错。
基于OOP的类图设计:
当你为上述设计沾沾自喜的时候,请花一分钟时间思考一下。控制层组件与通用控制逻辑完全是处于高耦合状态,先不论控制逻辑接口是否还具备有效性,只要控制逻辑稍稍发生些许变化,便会直接影响到所有控制层,这便基于依赖式设计的弊端。有办法可以解决吗?千万别告诉笔者Object Oriented只能这样,如果你真的是这么想的,那么你就可以考虑结合AOP的方式重构上述设计。
如果使用AOP的设计方式咱们应该怎么做呢?笔者前面提到过,AOP的特性是模块横切点,在这里这个横切点其实就是通用的控制逻辑,现在要做的事情就是需要将其抽取出来,使之与我们的控制层组件分离,这样既提升了内聚性,同时也保证了设计的低耦合。
基于OOA思想重构上述设计图:
通过基于上述AOP的设计重构,咱们完全实现了相关处理与控制层组件之间的分离。在这里笔者要给大家提示一下,如果想在程序中完全解除组件耦合是做不到的,且是不实际的。因为Object Oriented本身就是基于依赖式设计,如果希望设计出来的东西没有任何耦合性,恐怕你只能退回到POP才能满足你的需求。
对于目前而言,基于AOP的技术实现可以应用在诸多领域,比如:粗粒度日志记录、性能统计、安全控制、事务处理或者异常处理上。目前优秀的AOP产品其实也挺多的,除了Spring以外,Struts的Interceptor(拦截器)其实也是基于AOP思想的实现。
三、设计模式之代理模式
其实AOP的概念并不是近几年才诞生的新东西,早在GOF提出23种设计模式的时候,我们已经可以看见AOP的雏形身影。不知大家是否记得熟悉的代理模式?你完全可以把它当做是AOP的特定实现,当然如果你本身并不了解代理模式,请仔细的阅读笔者接下来的讲义。
回顾第1章,笔者为大家讲解了23种设计模式之中的工厂模式系列(包括:简单工厂模式、工厂方法模式和抽象工厂模式),本章节笔者则会为大家讲解关于代理模式的使用方式。
代理模式(Proxy Pattern)我们又称之为静态代理,该模式的特征定义是为委托对象提供一种代理访问机制,以控制其它对象对其进行访问。使用代理模式最大的好处在于,实际的角色只需关注具体的业务既可,其余操作则全权委托给代理对象负责,并且代理操作既可以在业务执行之前执行,同样也可以在业务执行完成之后再执行。通过这样的设计,我们的代码将更具扩展性,代码结构也更加清晰。
代理模式类图示例:
通过上述代理模式示例图,我们可以发现代理对象与委托对象都需要实现同一目标接口,客户端访问的时候,首先会访问代理类,最后由代理类去访问委托对象。
在这里笔者设计的代理对象,仅仅只是负责相关业务执行前后的粗粒度的日志记录。
代理类型代码如下:
public class LoginServiceProxy implements LoginService { private LoginServiceImpl loginServiceImpl; private boolean login; public boolean login() throws Exception { // TODO Auto-generated method stub System.out.println("日志记录1..."); loginServiceImpl = new LoginServiceImpl(); login = loginServiceImpl.login(); System.out.println("日志记录2..."); return login; } }
代理模式并不复杂,相对而言笔者觉得它应该是23种设计模式之中最好理解的设计模式之一。如果你对该模式还是无法理解,笔者建议你参考其他相关书籍。
四、JDK动态代理实现
JDK动态代理与静态代理(代理模式)不同的是, 动态代理会在程序运行时,通过反射机制动态生成代理对象。并且使用动态代理后,咱们再也不必手动编写代理类。这样不仅能够简化开发,相对于静态代理而言,程序将具备更好的扩展性。
在java.lang.reflect包下,Proxy类型和InvocationHandler接口将用于生成动态代理类。其中Proxy用于创建代理类型或者代理实例,来看看Proxy的常用方法。
用于生成代理类型的静态getProxyClass方法:
public static Class<?> getProxyClass(ClassLoader loader, Class<?>... interfaces) throws IllegalArgumentException;
使用Proxy的静态getProxyClass()方法,我们可以得到一个代理类型。其中“loader”属性用于指定委托类型的类装载器,“interfaces”属性则用于指定委托类型需要实现的所有接口。
用于生成代理实例的静态newProxyInstance()方法:
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException;
使用Proxy的静态newProxyInstance()方法,我们可以得到一个代理实例。其中“loader”属性用于指定委托类型的类装载器,“interfaces”属性用于指定委托类型需要实现的所有接口,而属性“h”则用于指定InvocationHandler实现(如果想使用JDK动态代理,那么代理类务必需实现InvocationHandler接口)。
使用JDK动态代理生成代理对象:
public class LoginServiceProxy<T> implements InvocationHandler { /* 需要被代理的委托对象 */ private T obj; public LoginServiceProxy(T obj) { this.obj = obj; } public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // TODO Auto-generated method stub return method.invoke(obj, args); } public boolean loginProxy() { boolean login = false; try { /* 反射生成代理对象 */ LoginService proxy = (LoginService) Proxy.newProxyInstance(obj .getClass().getClassLoader(), obj.getClass() .getInterfaces(), this); System.out.println("日志记录1...."); login = proxy.login(); System.out.println("日志记录2...."); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } return login; } }
提示:
使用JDK动态代理后,代理类型的名称则会以“$Proxy”开后。我们通过使用Debug来看一下代理类型的名称:
五、Spring AOP简介
关于Spring AOP中牵扯的概念较多,但笔者并不打算像其他技术文章一样故弄玄虚的瞎扯,尽可能的为大家带来最直接的表述方式。上述章节笔者详细的为大家阐述了AOP的一些相关概念和特定实现方式,从本章开始笔者将会为大家讲解如何在Spring中使用AOP。当然在正式开始讲解之前,你完全没有必要把Spring AOP想的过于复杂化,因为在Spring中使用AOP是一件极其轻松的事情,我们除了需要把对象管理(依赖建立、依赖维护、依赖销毁)交由Spring的IOC容器去负责外,代理方式则全部委派给Spring的AOP去完成即可。
从大致上来说使用Spring AOP其实有3种方式,第一种是采用实现MethodBeforeAdvice、AfterReturningAdvice、MethodInterceptor等接口的方式(在Spring1.x版本中使用最多)。第二种是基于Schema风格的配置文件方式。最后一种便是基于Annotation的实现方式,至于具体使用哪一种就看你自己喜好或项目需要。本章笔者仅围绕配置文件和Annotation的方式进行讲解,如果大家想了解基于指定接口的方式实现Spring AOP,笔者推荐你参考Spring官方帮助文档。
Spring AOP底层实现方式采用了2种代理机制,分别为:JDK动态代理和CGLib动态代理。至于为什么需要使用这2种代理机制,很大程度上是因为JDK自身只支持基于接口的代理,而不支持类型的代理。对于开发人员而言,要做的事情仅仅只是使用Spring封装好的AOP实现即可,不必太过于关注底层实现细节。
六、Spring AOP术语
在学习Spring AOP的时候,笔者不得不为大家提及一些AOP相关的术语概念,因为理解这些概念或许能够使你更快的掌握Spring AOP。笔者在很多年前学习Spring AOP术语时,曾经被国内一些“高手”的所谓翻译弄得晕头转向。在此笔者不得不“佩服”国内某些IT人员,把通俗易懂的英文翻译得连鬼都看不懂,笔者实在不解,所以笔者将会以自己的理解对以下AOP术语进行概念性总结:
1、切面(Aspect ):用于执行代理业务的具体内容;
2、连接点(Join point ):用于执行切面的具体代码位置;
3、通知(Advice ):用于执行代理业务的具体内容实现;
4、切入点(Pointcut):定义了通知应该拦截的方法;
5、目标对象(Target object):委托对象;
6、AOP代理(Aop proxy):代理对象;
7、前置通知(Before advice):代理业务执行于被拦截的方法之前;
8、后置通知(After advice):代理业务执行于被拦截的方法之后;
9、环绕通知(Around advice):代理业务执行于被拦截的方法之前或之后;
10、异常通知(After throwing advice):代理业务执行于被拦截的方法抛出异常后;
11、返回时通知(After returning advice):代理业务执行于被拦截的方法返回之前;
七、基于Schema配置文件形式的Spring AOP
笔者在前面章节中曾经提到过,如果想使用Spring AOP其实是有3种方式的。但笔者仅仅只会围绕Annotation和基于Schema风格的配置文件方式进行讲解。咱们先来看看如何使用配置文件的方式实现Spring AOP,或许这并不复杂,你完全可以轻松掌握并使用。
由于笔者的项目是基于Maven3.x进行管理的,所以首先需要添加Spring AOP所需的依赖构件:
<!-- AspectJ构件依赖 --> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjrt</artifactId> <version>1.6.11</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.6.11</version> </dependency>
<dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>2.1</version> </dependency> <!-- spring构件依赖 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>3.1.0.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aop</artifactId> <version>3.1.0.RELEASE</version> </dependency> </dependencies>
当然如果你只是使用传统工程构建Spring应用的话,你仅需要下载Spring AOP的相关构件引入至项目中即可,这些构件包括:
1、aspectjrt-version.jar;
2、spring-aop-version.jar;
3、aopalliance-version.jar;
4、aspectjweaver.jar;
5、cglib-version.jar;
6、asm-all-version.jar;
当我们成功添加Spring AOP的所需构件后,还需导入AOP命名空间:
xmlns:aop="http://www.springframework.org/schema/aop" http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.1.xsd
成功导入AOP命名空间后,接下来我们还需要在配置文件中进行切面配置。配置切面我们使用的是<aop:aspect/>标签,该标签的作用就是将一个普通的POJO转换成切面类进行相应的代理服务。
使用<aop:aspect/>标签配置切面:
<aop:config> <aop:aspect id="logAspect" ref="logBean"/> </aop:config> <bean name="logBean" class="org.johngao.bean.LogBean" />
在上述配置文件中,<aop:config/>标签作为Spring AOP的切面根标签。该标签中包含有<aop:advisor/>、<aop:aspect/>、<aop:pointcut/>等3的子标签元素,其中<aop:config/>标签用于定义切面服务,<aop:advisor/>标签类似于自包含切面,<aop:pointcut/>标签则用于定义切入点。大家在使用这3个标签的时候需要注意,必须按照pointcut-->advisor-->aspect的顺序进行声明。配置文件中允许定义多个<aop:config/>标签,且该标签内部仍然允许定义多个子标签元素。
我们首先来看<aop:aspect/>标签的使用。笔者前面也曾提及过,该标签用于定义切面,其中属性“ref”用于引用POJO作为切面类。此外<aop:aspect/>标签中还包含有2个可选属性,分别为“id”和“order”。其中属性“id”定义了切面的标识名。而属性“order”定义了切面类的优先级,属性值越低,切面的执行优先级就越高。
使用属性“order”定义切面执行优先级:
<aop:config> <aop:aspect id="logAspect2" ref="logBean2" order="2"/> <aop:aspect id="logAspect1" ref="logBean1" order="1"/> </aop:config> <bean name="logBean1" class="org.johngao.bean.LogBean1" /> <bean name="logBean2" class="org.johngao.bean.LogBean2" />
上述配置文件中,笔者定义了2个切面。其中切面1的执行优先级为“1”,所以切面1必然是优先于切面2执行。
提示:
使用属性“order”定义切面类的执行优先级,最常用的使用场景是多个切面指定拦截同一切入点。
本章内容到此结束,由于时间仓库,本文或许有很多不尽人意的地方,希望各位能够理解和体谅。关于下一章的内容,笔者打算继续讲解Spring AOP相关的内容。