读不在三更五鼓,功只怕一曝十寒。 —— 郭沫若
@[toc]
一、导言
随着软件世界不断复杂化,传统的OOP(面向对象)建模思路已经不足以很好的处理好开发时所面临的种种挑战,AOP(Aspect Orient Programming)应运而生,它和OOP建模方式并不冲突,它是OOP编程的一种有效补充。
OOP面向名词领域建模,使用类作为单位来模块化目标系统,而AOP面向动词领域建模,其模块化单位则是Aspect
:切面。
常见于处理一些具有==横切性质==的系统级服务,例如: 日志管理、事务管理、安全检查缓存、对象池管理等
。
本文首先介绍了AOP主要概念和常见实现原理,后就动态代理引入SpringAOP,并简单介绍了SpringAOP的原理和其支持的切面,最后做了一个总结。
二、AOP
2.1. 代理模式
说到AOP不得不说说代理模式
。代理模式顾名思义,不直接访问目标对象,而是通过一个代理对象来间接访问目标对象。客户端代码隔着一层代理对象访问目标对象,代理对象可以对访问过程做各种加工、控制。如下类图简洁明了的展示了代理模式的依赖关系、类结构。
ProxyPatternDemo类是客户端代码,Image是抽象接口,代理类ProxyImage和目标类ReadlImage都实现Image,客户端代码通过代理对象才能访问目标对象。
2.2. AOP核心概念
常见的AOP框架都使用代理的方式给JointPoint~下面会介绍~添加“行为增强”,例如日志增强、事务增强等,具体可以理解为使用代理模式==透明的==的封装了客户端代码对被代理对象的访问。
2.2.1. Aspect 切面
Aspect对标OOP中的class的含义,封装==横切点的逻辑==和横切点逻辑的==作用范围==。切面由切入点和通知组成,它既包含了横切逻辑的定义,也包括了切入点的定义。具体的日志、事务、线程池管理
等逻辑都在横切逻辑的Advice
(通知)~下面有介绍~里实现。
2.2.1.1. PointCut 切入点
PointCut
是用于==限定横切逻辑作用范围==的谓词限定式,其核心是切点表达式
。由“切点表达式匹配JointPpoint
”的概念是AOP的核心,SpringAOP 默认使用AspectJ切点表达式语言,在实际使用上有以下两种形式,前者可以将切入点表达式抽象出来,在SpringAOP中就是抽象成一个方法:
- 使用
@PointCut
注解声明。 - 直接在Advice方法的注解里声明。
2.2.1.2. Advice 通知
Advice封装具体的横切逻辑。不同的横切逻辑可以以around、before、after、return、afterThrow
等形式体现。Spring AOP以拦截器的方式实现,并维护了一个以连接点~jointPoint~为中心的拦截器链。
2.2.2. JoinPoint 连接点
JoinPoint是横切逻辑执行时所关联的点。在JointPoint的“前后左右等”方向上执行Advice逻辑。PointCut表达式指定了JointPoint位置,Advice的体现形式~after、before等~指定了以什么形式在JointPoint“附近”执行 。
2.2.3. Introduction 引入
可以简单的理解为被代理对象添加方法或字段。
2.2.4. Targect Objecj 目标对象
被代理对象。被AOP框架处理的对象
2.2.5 Weaving 织入
织入是创建Advice代理对象并将Aspect代理对象和业务逻辑对象连接起来的过程。
织入可以在编译时,类加载时和运行时完成。在编译时进行织入就是静态代理,而在运行时进行织入则是动态代理。
2.3. 常见AOP实现原理
常见的AOP实现都是基于代理模式的,可以分为:1.静态代理 2.动态代理
两种。
静态代理: 在java领域中,最常见的基于静态代理的实现是AspectJ
(官网链接),目前已经更新到AspectJ 9
了。AspectJ
支持编译时、编译后、类加载时织入。具体原理和使用方法暂且略过不表。
动态代理: 动态代理可以分为基于JDK动态代理
和CGLIB动态代理
。都是在内存中临时为目标对象生成一个代理对象,客户端代码==透明的==通过调用代理对象提供的方法来访问目标对象的方法。
而目前市面上最常见的基于动态代理
的AOP实现是SpringAOP
。
三、SpringAOP
SpringAOP
是Spring
框架的一个关键组件,其是基于Spring IOC容器而实现,与IOC容器无缝衔接。SpringAOP基于动态代理、纯java方式实现,不需要特定的编译过程,也不需要关心classLoader
类加载机制,本质上就是使用JDK动态代理或CGLIB来动态生成一个代理类。
3.1. SpringAOP代理原理浅谈
当被代理对象有对应接口~所有的要被代理的方法在这个接口里都有声明~时,SpringAOP使用JDK动态代理实现,当被代理对象没有对应的接口时,使用CGLIB实现。
CGLIB可以理解为继承被代理类而动态产生一个代理对象,这和传统的代理模式有一点区别~传统代理模式有共有接口~。由于是使用继承机制来实现AOP,所以SpringAOP不支持对final
方法的代理,更不支持对fianl
类的代理,因为子类无法对父类方法进行override
(重写)。
3.2. SpringAOP和AspectJ
SpringAOP目前只支持方法级别的AOP支持,我没有实现字段拦截,如果需要对字段访问进行AOP处理的话,可以考虑使用AspectJ语言。
SpringAOP不同于大多其他的AOP实现,SpringAOP目标不是提供最完整的AOP实现。它的亮点在于AOP和SpringIOC之间的紧密结合(虽然已经很好用了),它非侵入的实现了AOP,不依赖于AspectJ编译器和AspectJ织入器。在SpringAOP中使用普通的bean定义来定义Aspect,SpringAOP和AspectJ是互补的,不是非此即彼的。
下表是摘自这里的SpringAOP和AspectJ的关键区别:
Spring AOP | AspectJ |
---|---|
在纯 Java 中实现 | 使用 Java 编程语言的扩展实现 |
不需要单独的编译过程 | 除非设置 LTW,否则需要 AspectJ 编译器 (ajc) |
只能使用运行时织入 | 运行时织入不可用。支持编译时、编译后和加载时织入 |
功能不强-仅支持方法级编织 | 更强大 - 可以编织字段、方法、构造函数、静态初始值设定项、最终类/方法等......。 |
只能在由 Spring 容器管理的 bean 上实现 | 可以在所有域对象上实现 |
仅支持方法执行切入点 | 支持所有切入点 |
代理是由目标对象创建的, 并且切面应用在这些代理上 | 在执行应用程序之前 (在运行时) 前, 各方面直接在代码中进行织入 |
比 AspectJ 慢多了 | 更好的性能 |
易于学习和应用 | 相对于 Spring AOP 来说更复杂 |
3.3. SpringAOP Advice
3.3.1. Before Advice : 前置通知
在JointPoint前执行的Advice,不可阻止JointPoint处方法的执行~Around型可以~。使用@Before
定义,具体源码如下所示。
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Before {
String value();
// 结合SpringAOP的命名绑定机制(name binding)使用,支持的PCD有: target、this、args
String argNames() default "";
}
附: PCD(PointCut Designators 切点指示器),是切点表达式的重要组成部分。
3.3.2. After : 后置通知
不管被代理方法是正常结束还是异常结束,都会执行这个Advice。
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface After {
String value();
// 同上
String argNames() default "";
}
3.3.3. AfterReturning : 后置返回通知
在JointPoint后执行的Advice,当JointPoint处的方法正常执行结束后,会执行这个Advice,如果是异常执行话则不会。使用@AfterReturning
定义,具体源码如下所示。
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface AfterReturning {
String value() default "";
// 和value同一个作用
String pointcut() default "";
// 用于声明返回值
String returning() default "";
// 同上
String argNames() default "";
}
可以简单的看成带返回值的After
advice。
3.3.4. AfterThrowing : 后置异常通知
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface AfterThrowing {
String value() default "";
String pointcut() default "";
// 当throwing指定的异常发生时会调用这个Advice
String throwing() default "";
// 同上
String argNames() default "";
}
正常结束不会调用这个Advice,当指定异常发生时会调用这个Advice。
3.3.5. Around: 环绕通知
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Around {
String value();
// 同上
String argNames() default "";
}
// 常和ProceedingJoinPoint类结伴使用
==最强大的Advice==,几乎所有的其他类型的Advice都可以使用环绕通知来实现。但Spring官方建议选用“能实现所需行为的功能最小的通知类型”: 提供最简单的编程模式,减少了出错的可能性。
四、总结
一般来说,我们可以通过手动编写的形式来实现代理模式,但当需要被代理的对象、类==有很多==时,手动编写的方式就不太合适,AOP就是适合这种有很多横切点的且横切点可以抽象出统一逻辑的一种编程范型。
举一个在实际生产中很常见的例子:
假如某些业务逻辑接口的调用都需要在方法开始前和方法开始之后打日志,可以使用AOP切面的方式在所有的业务逻辑方法~JoinPoint~处定义两个Advice: before和after类型的。
五、参考链接
==一键三连==支持下吧