目录
1 什么是AOP?
- AOP(Aspect Oriented Programming)称为面向切面编程,在程序开发中主要用来解决一些系统层面上的问题,比如日志,事务,权限等待,Struts2的拦截器设计就是基于AOP的思想,是个比较经典的例子。
- 在不改变原有的逻辑的基础上,增加一些额外的功能。代理也是这个功能,读写分离也能用aop来做。
AOP可以说是OOP(Object Oriented Programming,面向对象编程)的补充和完善。OOP引入封装、继承、多态等概念来建立一种对象层次结构,用于模拟公共行为的一个集合。不过OOP允许开发者定义纵向的关系,但并不适合定义横向的关系,例如日志功能。日志代码往往横向地散布在所有对象层次中,而与它对应的对象的核心功能毫无关系对于其他类型的代码,如安全性、异常处理和透明的持续性也都是如此,这种散布在各处的无关的代码被称为横切(cross cutting),在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。
- AOP技术恰恰相反,它利用一种称为"横切"的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为"Aspect",即切面。所谓"切面",简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。
- 使用"横切"技术,AOP把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处基本相似,比如权限认证、日志、事物。AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。
要理解切面编程,就需要先理解什么是切面。用刀把一个西瓜分成两瓣,切开的切口就是切面;炒菜,锅与炉子共同来完成炒菜,锅与炉子就是切面。web层级设计中,web层->网关层->服务层->数据层,每一层之间也是一个切面。编程中,对象与对象之间,方法与方法之间,模块与模块之间都是一个个切面。
我们一般做活动的时候,一般对每一个接口都会做活动的有效性校验(是否开始、是否结束等等)、以及这个接口是不是需要用户登录。
按照正常的逻辑,我们可以这么做。
这有个问题就是,有多少接口,就要多少次代码copy。对于一个“懒人”,这是不可容忍的。好,提出一个公共方法,每个接口都来调用这个接口。这里有点切面的味道了
同样有个问题,我虽然不用每次都copy代码了,但是,每个接口总得要调用这个方法吧。于是就有了切面的概念,我将方法注入到接口调用的某个地方(切点)。
这样接口只需要关心具体的业务,而不需要关注其他非该接口关注的逻辑或处理。
红框处,就是面向切面编程。
2 AOP中的相关概念
(1)横切关注点:对哪些方法进行拦截,拦截后怎么处理,这些关注点称之为横切关注点
(2)Aspect(切面):通常是一个类,里面可以定义切入点和通知
(3)JointPoint(连接点):程序执行过程中明确的点,一般是方法的调用。被拦截到的点,因为Spring只支持方法类型的连接点,所以在Spring中连接点指的就是被拦截到的方法,实际上连接点还可以是字段或者构造器
(4)Advice(通知):AOP在特定的切入点上执行的增强处理,有before(前置),after(后置),afterReturning(最终),afterThrowing(异常),around(环绕)
(5)Pointcut(切入点):就是带有通知的连接点,在程序中主要体现为书写切入点表达式
(6)weave(织入):将切面应用到目标对象并导致代理对象创建的过程
(7)introduction(引入):在不修改代码的前提下,引入可以在运行期为类动态地添加一些方法或字段
(8)AOP代理(AOP Proxy):AOP框架创建的对象,代理就是目标对象的加强。Spring中的AOP代理可以使JDK动态代理,也可以是CGLIB代理,前者基于接口,后者基于子类
(9)目标对象(Target Object): 包含连接点的对象。也被称作被通知或被代理对象。POJO
通知方法:
- 前置通知:在我们执行目标方法之前运行(@Before)
- 后置通知:在我们目标方法运行结束之后 ,不管有没有异常(@After)
- 返回通知:在我们的目标方法正常返回值后运行(@AfterReturning)
- 异常通知:在我们的目标方法出现异常后运行(@AfterThrowing)
- 环绕通知:动态代理, 需要手动执行joinPoint.procced()(其实就是执行我们的目标方法执行之前相当于前置通知, 执行之后就相当于我们后置通知(@Around)
然后举一个容易理解的例子:
看完了上面的理论部分知识, 我相信还是会有不少朋友感觉到 AOP 的概念还是很模糊, 对 AOP 中的各种概念理解的还不是很透彻. 其实这很正常, 因为 AOP 中的概念是在是太多了, 我当时也是花了老大劲才梳理清楚的.
下面我以一个简单的例子来比喻一下 AOP 中 Aspect
, Joint point
, Pointcut
与 Advice
之间的关系.
让我们来假设一下, 从前有一个叫爪哇的小县城, 在一个月黑风高的晚上, 这个县城中发生了命案. 作案的凶手十分狡猾, 现场没有留下什么有价值的线索. 不过万幸的是, 刚从隔壁回来的老王恰好在这时候无意中发现了凶手行凶的过程, 但是由于天色已晚, 加上凶手蒙着面, 老王并没有看清凶手的面目, 只知道凶手是个男性, 身高约七尺五寸. 爪哇县的县令根据老王的描述, 对守门的士兵下命令说: 凡是发现有身高七尺五寸的男性, 都要抓过来审问. 士兵当然不敢违背县令的命令, 只好把进出城的所有符合条件的人都抓了起来.
来让我们看一下上面的一个小故事和 AOP 到底有什么对应关系.
首先我们知道, 在 Spring AOP 中 Joint point
指代的是所有方法的执行点, 而 point cut 是一个描述信息, 它修饰的是 Joint point
, 通过 point cut, 我们就可以确定哪些 Joint point
可以被织入 Advice
. 对应到我们在上面举的例子, 我们可以做一个简单的类比, Joint point
就相当于 爪哇的小县城里的百姓,pointcut
就相当于 老王所做的指控, 即凶手是个男性, 身高约七尺五寸, 而 Advice
则是施加在符合老王所描述的嫌疑人的动作: 抓过来审问.
为什么可以这样类比呢?
-
Joint point
: 爪哇的小县城里的百姓: 因为根据定义,Joint point
是所有可能被织入Advice
的候选的点, 在 Spring AOP中, 则可以认为所有方法执行点都是Joint point
. 而在我们上面的例子中, 命案发生在小县城中, 按理说在此县城中的所有人都有可能是嫌疑人. -
Pointcut
:男性, 身高约七尺五寸: 我们知道, 所有的方法(joint point) 都可以织入Advice
, 但是我们并不希望在所有方法上都织入Advice
, 而Pointcut
的作用就是提供一组规则来匹配joinpoint, 给满足规则的 joinpoint 添加Advice
. 同理, 对于县令来说, 他再昏庸, 也知道不能把县城中的所有百姓都抓起来审问, 而是根据凶手是个男性, 身高约七尺五寸, 把符合条件的人抓起来. 在这里 凶手是个男性, 身高约七尺五寸 就是一个修饰谓语, 它限定了凶手的范围, 满足此修饰规则的百姓都是嫌疑人, 都需要抓起来审问. -
Advice
:抓过来审问,Advice
是一个动作, 即一段 Java 代码, 这段 Java 代码是作用于 point cut 所限定的那些Joint point
上的. 同理, 对比到我们的例子中, 抓过来审问 这个动作就是对作用于那些满足 男性, 身高约七尺五寸 的爪哇的小县城里的百姓. -
Aspect
::Aspect
是 point cut 与Advice
的组合, 因此在这里我们就可以类比: “根据老王的线索, 凡是发现有身高七尺五寸的男性, 都要抓过来审问” 这一整个动作可以被认为是一个Aspect
.
最后是一个描述这些概念之间关系的图:
3 AOP使用场景
Authentication 权限
Caching 缓存
Context passing 内容传递
Error handling 错误处理
Lazy loading 懒加载
Debugging 调试
logging, tracing, profiling and monitoring 记录跟踪 优化 校准
Performance optimization 性能优化
Persistence 持久化
Resource pooling 资源池
Synchronization 同步
Transactions 事务
4 横切技术
“横切”是AOP的专有名词。它是一种蕴含强大力量的相对简单的设计和编程技术,尤其是用于建立松散耦合的、可扩展的企业系统时。横切技术可以使得AOP在一个给定的编程模型中穿越既定的职责部分(比如日志记录和性能优化)的操作。
如果不使用横切技术,软件开发是怎样的情形呢?在传统的程序中,由于横切行为的实现是分散的,开发人员很难对这些行为进行逻辑上的实现或更改。例如,用于日志记录的代码和主要用于其它职责的代码缠绕在一起。根据所解决的问题的复杂程度和作用域的不同,所引起的混乱可大可小。更改一个应用程序的日志记录策略可能涉及数百次编辑——即使可行,这也是个令人头疼的任务。
在AOP中,我们将这些具有公共逻辑的,与其他模块的核心逻辑纠缠在一起的行为称为“横切关注点(Crosscutting Concern)”,因为它跨越了给定编程模型中的典型职责界限。
4.1 横切关注点
一个关注点(concern)就是一个特定的目的,一块我们感兴趣的区域,一段我们需要的逻辑行为。从技术的角度来说,一个典型的软件系统包含一些核心的关注点和系统级的关注点。举个例子来说,一个信用卡处理系统的核心关注点是借贷/存入处理,而系统级的关注点则是日志、事务完整性、授权、安全及性能问题等,许多关注点——即横切关注点(crosscutting concerns)——会在多个模块中出现。如果使用现有的编程方法,横切关注点会横越多个模块,结果是使系统难以设计、理解、实现和演进。AOP能够比上述方法更好地分离系统关注点,从而提供模块化的横切关注点。
例如一个复杂的系统,它由许多关注点组合实现,如业务逻辑、性能,数据存储、日志和调度信息、授权、安全、线程、错误检查等,还有开发过程中的关注点,如易懂、易维护、易追查、易扩展等,图2.1演示了由不同模块实现的一批关注点组成一个系统。
通过对系统需求和实现的识别,我们可以将模块中的这些关注点分为:核心关注点和横切关注点。对于核心关注点而言,通常来说,实现这些关注点的模块是相互独立的,他们分别完成了系统需要的商业逻辑,这些逻辑与具体的业务需求有关。而对于日志、安全、持久化等关注点而言,他们却是商业逻辑模块所共同需要的,这些逻辑分布于核心关注点的各处。在AOP中,诸如这些模块,都称为横切关注点。应用AOP的横切技术,关键就是要实现对关注点的识别。
如果将整个模块比喻为一个圆柱体,那么关注点识别过程可以用三棱镜法则来形容,穿越三棱镜的光束(指需求),照射到圆柱体各处,获得不同颜色的光束,最后识别出不同的关注点。
上图识别出来的关注点中,Business Logic属于核心关注点,它会调用到Security,Logging,Persistence等横切关注点。
public class BusinessLogic
{
public void SomeOperation()
{
//验证安全性;Securtity关注点;
//执行前记录日志;Logging关注点;
DoSomething();
//保存逻辑运算后的数据;Persistence关注点;
//执行结束记录日志;Logging关注点;
}
}
AOP的目的,就是要将诸如Logging之类的横切关注点从BusinessLogic类中分离出来。利用AOP技术,可以对相关的横切关注点封装,形成单独的“aspect”。这就保证了横切关注点的复用。由于BusinessLogic类中不再包含横切关注点的逻辑代码,为达到调用横切关注点的目的,可以利用横切技术,截取BusinessLogic类中相关方法的消息,例如SomeOperation()方法,然后将这些“aspect”织入到该方法中。例如图2.3:
通过利用AOP技术,改变了整个系统的设计方式。在分析系统需求之初,利用AOP的思想,分离出核心关注点和横切关注点。在实现了诸如日志、事务管理、权限控制等横切关注点的通用逻辑后,开发人员就可以专注于核心关注点,将精力投入到解决企业的商业逻辑上来。同时,这些封装好了的横切关注点提供的功能,可以最大限度地复用于商业逻辑的各个部分,既不需要开发人员作特殊的编码,也不会因为修改横切关注点的功能而影响具体的业务功能。
为了建立松散耦合的、可扩展的企业系统,AOP应用到的横切技术,通常分为两种类型:动态横切和静态横切。
4.2 动态横切
动态横切是通过切入点和连接点在一个方面中创建行为的过程,连接点可以在执行时横向地应用于现有对象。动态横切通常用于帮助向对象层次中的各种方法添加日志记录或身份认证。在很多应用场景中,动态横切技术基本上代表了AOP。
动态横切技术的核心主要包括join point(连接点),point cut(切入点),advice(通知)和aspect(方面)。在前面,我已经概要地介绍了这些术语分别代表的含义。接下来,我将以一个具体的实例来进一步阐述它们在AOP动态横切中实现的意义。
考虑一个电子商务系统,需要对订单进行添加、删除等管理操作。毫无疑问,在实际的应用场景中,这些行为应与权限管理结合,只有获得授权的用户方能够实施这些行为。采用传统的设计方法,其伪代码如下:
public class OrderManager
{
private ArrayList m_Orders;
public OrderManager()
{
m_Orders = new ArrayList();
}
public void AddOrder(Order order)
{
if (permissions.Verify(Permission.ADMIN))
{
m_Orders.Add(order);
}
}
public void RemoveOrder(Order order)
{
if (permissions.Verify(Permission.ADMIN))
{
m_Orders.Remove(order);
}
}
}
同样的,在该电子商务系统中,还需要对商品进行管理,它采用了同样的授权机制:
public class ProductManager
{
private ArrayList m_Products;
public ProductManager()
{
m_Products = new ArrayList();
}
public void AddProduct(Product product)
{
if (permissions.Verify(Permission.ADMIN))
{
m_Products.Add(product);
}
}
public void RemoveProduct(Product product)
{
if (permissions.Verify(Permission.ADMIN))
{
m_Products.Remove(product);
}
}
}
如此以来,在整个电子商务系统中,核心业务包括订单管理和商品管理,它们都需要相同的权限管理,如图2.4所示:
毫无疑问,利用AOP技术,我们可以分离出系统的核心关注点和横切关注点,从横向的角度,截取业务管理行为的内部消息,以达到织入权限管理逻辑的目的。当执行AddOrder()等方法时,系统将验证用户的权限,调用横切关注点逻辑,因此该方法即为AOP的join point。对于电子商务系统而言,每个需要权限验证的方法都是一个单独的join point。由于权限验证将在每个方法执行前执行,所以对于这一系列join point,只需要定义一个point cut。当系统执行到join point处时,将根据定义去查找对应的point cut,然后执行这个横切关注点需要实现的逻辑,即advice。而point cut和advice,就组合成了一个权限管理aspect。
由于aspect是一个封装的对象,我们可以定义这样一个aspect:
private static aspect AuthorizationAspect{……}
然后在这个aspect中定义point cut,在point cut中,定义了需要截取上下文消息的方法,例如:
private pointcut authorizationExecution():
execution(public void OrderManager.AddOrder(Order)) ||
execution(public void OrderManager.DeleteOrder(Order)) ||
execution(public void ProductManager.AddProduct(Product)) ||
execution(public void ProductManager.DeleteProduct(Product));
由于权限验证是在订单管理方法执行之前完成,因此在before advice中,定义权限检查:
before(): authorizationExecution()
{
if !(permissions.Verify(Permission.ADMIN))
{
throw new UnauthorizedException();
}
}
通过定义了这样一个完整的aspect,当系统调用OrderManager或ProductManager的相关方法时,就触发了point cut,然后调用相应的advice逻辑。如此以来,OrderManager和ProductManager模块就与权限管理模块完全解除了依赖关系,同时也消除了传统设计中不可避免的权限判断的重复代码。这对于建立一个松散耦合、可扩展的系统软件是非常有利的。
4.3 静态横切
静态横切和动态横切的区别在于它不修改一个给定对象的执行行为。相反,它允许通过引入附加的方法字段和属性来修改对象的结构。此外,静态横切可以把扩展和实现附加到对象的基本结构中。在AOP实现中,通常将静态横切称为introduce或者mixin。
静态横切在AOP技术中,受到的关注相对较少。事实上,这一技术蕴含的潜力是巨大的。使用静态横切,架构师和设计者能用一种真正面向对象的方法有效地建立复杂系统的模型。静态横切允许您不用创建很深的层次结构,以一种本质上更优雅、更逼真于现实结构的方式,插入跨越整个系统的公共行为。尤其是当开发应用系统时,如果需要在不修改原有代码的前提下,引入第三方产品和API库,则静态横切技术将发挥巨大的作用。
举例来说,当前已经实现了一个邮件收发系统,其中类Mail完成了收发邮件的功能。但在产品交付后,发现该系统存在缺陷,在收发邮件时,未曾实现邮件地址的验证功能。现在,第三方产品已经提供了验证功能的接口IValidatable:
public interface IValidatable
{
bool ValidateAddress();
}
我们可以利用设计模式中的Adapter模式,来完成对第三方产品API的调用。我们可以定义一个新的类MailAdapter,该类实现了IValidatable接口,同时继承了Mail类:
public class MailAdapter:Mail,IValidatable
{
public bool ValidateAddress()
{
if(this.getToAddress() != null)
{
return true;
}
else
{
return false;
}
}
}
通过引入MailAdapter类,原来Mail对象完成的操作,将全部被MailAdapter对象取代。然而,此种实现方式虽然能解决引入新接口的问题,但类似下面的代码,却是无法编译通过的:
Mail mail = new Mail();
IValidatable validate = ((IValidatable)mail).ValidateAddress();
必须将第一行代码作如下修改:
Mail mail = new MailAdapter();
利用AOP的静态横切技术,可以将IValidatable接口织入到原有的Mail类中,这是一种非常形象的introduce功能,其实现仍然是在aspect中完成:
import com.acme.validate.Validatable;
public aspect MailValidateAspect
{
declare parents: Mail implements IValidatable;
public boolean Mail.validateAddress()
{
if(this.getToAddress() != null)
{
return true;
}
else
{
return false;
}
}
}
静态横切的方法,并没有引入类似MailAdapter的新类,而是通过定义的MailValidateAspect方面,利用横切技术为Mail类introduce了新的方法ValidateAddress(),从而实现了Mail的扩展。因此如下的代码完全可行。
Mail mail = new Mail();
IValidatable validate = ((IValidatable)mail).ValidateAddress();
5 Spring Aop
Spring中的AOP代理还是离不开Spring的IOC容器,代理的生成,管理及其依赖关系都是由IOC容器负责,Spring默认使用JDK动态代理,在需要代理类而不是代理接口的时候,Spring会自动切换为使用CGLIB代理,不过现在的项目都是面向接口编程,所以JDK动态代理相对来说用的还是多一些。
使用注解配置aop
引入约束 beans context aop 扫描包 开启注解式的aop <aop:aspectj-autoproxy proxy-target-class="true"></aop:aspectj-autoproxy>
在业务类上加上注解(@Component)
在属性上加@Autowired注解
在切面类上加注解 (@Component @Aspect)
在通知上加上相应的注解 @Before(value="")
@AfterReturning(returning="目标切点返回值的名字") 在通知方法参数只能为Object
@AfterThrowing(throwing="异常的名字") 在通知方法参数可以为Object也可为Exception
重用切入点表达式: 在切面中定义一个空的方法,在方法上加@Pointcut注解,在value属性中指定切入点表达式 @Pointcut(value=“execution(* com.cdsxt.dao.*.*(..))”) Public void pointcutExpression(){} @Before(value="pointcutExpression()")
AOP两种代理的区别:
AOP支持2种代理,jdk的动态代理和CGLIB实现机制。
jdk基于接口实现:jdk动态代理对实现了接口的类进行代理。
CGLIB基于继承:CGLIB代理可以对类代理,主要对指定的类生成一个子类,因为是继承,所以目标类最好不要使用final声明。
通常情况下,鼓励使用jdk代理,因为业务一般都会抽象出一个接口,而且不用引入新的东西。如果是遗留的系统,以前没有实现接口,那么只能使用CGLIB。
切面类:
package com.enjoy.cap10.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
//日志切面类
@Aspect
public class LogAspects {
@Pointcut("execution(public int com.enjoy.cap10.aop.Calculator.*(..))")
public void pointCut(){};
//@before代表在目标方法执行前切入, 并指定在哪个方法前切入
@Before("pointCut()")
public void logStart(){
System.out.println("除法运行....参数列表是:{}");
}
@After("pointCut()")
public void logEnd(){
System.out.println("除法结束......");
}
@AfterReturning("pointCut()")
public void logReturn(){
System.out.println("除法正常返回......运行结果是:{}");
}
@AfterThrowing("pointCut()")
public void logException(){
System.out.println("运行异常......异常信息是:{}");
}
@Around("pointCut()")
public Object Around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
System.out.println("@Arount:执行目标方法之前...");
Object obj = proceedingJoinPoint.proceed();//相当于开始调div地
System.out.println("@Arount:执行目标方法之后...");
return obj;
}
}
目标方法:
package com.enjoy.cap10.aop;
public class Calculator {
//业务逻辑方法
public int div(int i, int j){
System.out.println("--------");
return i/j;
}
}
配置类:
package com.enjoy.cap10.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import com.enjoy.cap10.aop.Calculator;
import com.enjoy.cap10.aop.LogAspects;
@Configuration
@EnableAspectJAutoProxy
public class Cap10MainConfig {
@Bean
public Calculator calculator(){
return new Calculator();
}
@Bean
public LogAspects logAspects(){
return new LogAspects();
}
}
测试类:
public class Cap10Test {
@Test
public void test01(){
AnnotationConfigApplicationContext app = new AnnotationConfigApplicationContext(Cap10MainConfig.class);
Calculator c = app.getBean(Calculator.class);
int result = c.div(4, 3);
System.out.println(result);
app.close();
}
}
结果:
@Arount:执行目标方法之前...
除法运行....参数列表是:{}
--------
@Arount:执行目标方法之后...
除法结束......
除法正常返回......运行结果是:{}
1