面向方面编程(Aspect Oriented Programming)
1. 概念
1.1 背景
- 面向过程编程问题
面向过程是一种自顶向下的编程方法,适用于小型软件系统,在大型应用系统中,自顶向下逐步求精的方法,在系统的进化和维护,以及软件重用性方面都存在其不足之处 - 面向对象编程问题
由于其良好的封装性,层次化性以及继承性,且对象模型很好映射到实际领域。但是在设计阶段,由于以类为单位组织建模,不能全面反映软件系统的需求;在编码阶段,将数据和方法封装到类中增强了数据的安全性和模块化,但减少了代码重用可能性;维护阶段,类中夹杂各种特定于应用的代码,难以理解和维护
1.2 AOP
- 将传统的按功能或按对象划分程序模块的方法转化为按系统特征划分程序模块,这是AOP基本思想,OOP引入封装、继承和多态性等概念来建立一种对象层次结构,允许定义从上到下的关系,AOP则定义从左到右的关系,为分散的对象引入公共行为。
- 核心名词
- aspect方面:切面是横切性关注点的抽象,切入点和通知结合起来就是aspect
- joinpoint连接点是指那些被拦截到的点,在spring中,这些点指的是方法
- pointcut切入点:指对joinpoint进行拦截的定义,本质上是一个捕获连接点的结构,
其表述方式有:1、直接指定Joinpoint所在方法名称,2、正则表达式,3、使用特定的Pointcut表述语言,4、pointcut运算:可以涵盖逻辑运算语法,如spring的配置文件中使用and,or来作为逻辑运算符
- advice通知 :是指拦截到joinpoint之后所要做的事情就是通知,分为前置通知,后置通知,异常通知,最终通知,环绕通知
- target目标对象:代理的目标对象
- weawe织入:指将aspect应用到target对象并导致proxy对象创建的过程称为织入
- introduction引入:在不修改类代码的前提下,introduction可以在运行期为类动态地添加一些方法或field,本质也是一种advice,但其不是根据横切逻辑在Joinpoint处的执行时机来区分的,而是根据它可以完成的功能而区别于其他的advice类型
- aop技术:AspectJ,AspectWerkz,JBoss AOP,Spring Aop
- 语法方面: aspectJ对java进行了扩展,其它则没有改变
- 声明方面: aspectJ使用代码声明,其它则使用xml或注解
- 性能方面: AspectJ通过编译对目标二进制类的增加,其它则基于运行时拦截技术
1.3 发展历史
- 第一代:静态aop时代
以AspectJ为代表,相应的横切关注点以Aspec形式实现后,会通过特定的编辑器,将实现后的Aspect编译并织入到系统的静态类中,以EJB所提供的声明性事务等AOP实现为代表。Aspect直接以Java字节码的形式编译到java类中,由java虚拟机加载class文件得以运行。 - 第二代:动态aop时代
aop的织入过程在系统运行开始之后进行,而不是预先编译到系统类中,而且织入信息大都采用外部xml文件格式保存,可以在调整以入及织入逻辑单元的同时,不必变更系统其它模块。采用对系统字节码进行操作的方式来完成 aspect到系统织入,带到一定性能消耗,而jvm的提升,对反射以及字节码操作技术的更好支持,少量的性能损失可以容忍。
1.4 java平台AOP机制
- 动态代理机制,可以在运行期间,为相应的接口动态生成对应的代理对象,将横切关注点逻辑封装到动态代理的InvocationHandler中,以动态代理类为载体来实现横切逻辑,但这种机制只针对接口有效,性能稍逊静态aop
- 动态字节码增加,使用asm或cglib,动态构建字节码的class文件,通过动态字节码增强技术,对目标对象扩展继承生成相应的子类,而实现MethodInterceptor接口将横切逻辑加到这些子类中,通过Enhancer类(参数父类类型和callback实现类)创建代理类,从而达到将横切逻辑织入系统的目的。缺点在于final的声明则无法对其进行子类化的扩展。
- java代码生成(静态代理),早期EJB容器根据部署描述符文件提供的织入信息,会为相应的功能模块类生成对应的java代码,然后通过部署工具编译生成相应java类,然后在ejb容器下工作.主要由三个角色构成,subject接口,subject实例,subject代理,客户端使用代理来调用对象,使用代理对方法拦截进行部分业务处理。subject代理和subject实例都实现了subject接口,代理类持有对实例类的引用,在调用实例类业务方法的时候添加横切逻辑,缺点是虽然横切逻辑相同,但对应的目标对象类型(subject接口)是不一样的都需要编写对应代理类,加入相同的横切逻辑,代码过于臃肿。
- 自定义类加载器,通过自定义类加载器的方式完成横切逻辑到系统的织入,自定义类加载器通过读取外部文件规定的必要信息,在加载 class文件期间就可以将横切逻辑添加到系统模块类的现有逻辑中。jboss aop采用这种方式实现,某些应用服务器会控制整个类的加载体系。
2 AspectJ
AspectJ既是语言规范,又是语言实现,AspectJ也许是已知最好的,并且应用最广泛的AOP实现,
- 连接点:可用的连接点有
- 方法的调用或执行
- 构造器的调用和执行
- 对属性的读写访问
- 异常处理的执行
- 对象和类的初始化执行
备注:不能在像if条件检查或for循环这样细粒度语言构造上的连接点
- 方面
方面把切入点和通知包在一起。AspectJ允许在类中声明切入点,但在类中只能声明static的切入点,且不允许通知。
3 Spring AOP
3.1 概念表述
- spring aop采用动态代理机制和字节码生成技术,都是在运行期间为目标对象生成一个代理对象,而将横切逻辑织入到这个代理对象中,系统最终使用的是织入了横切逻辑的代理对象,而不是真正的目标对象。
- spring采用的代理模式有静态代理、动态代理、CGLig代理。spring在发现目标对象实现了相应的Interface时,使用动态代理机制生成代理对象,如何无任何接口则使用Cglib动态字节码生成,但proxyTargetClass属性和optimize属性强制ProxyFactory采用基于类的代理
- Spring aop使用ProxyFactory作为织入器,使用AnnotationAwareAspectJAutoProxyCreator来配置信息,搜集IOC容器中注册的Aspect,并应用到Pointcut定义的各个目标对象上。如果@Pointcut包含不支持的标志符的Pointcut表达式,将抛出IllegalArgumentException而拒绝,Spring aop只是借用一下AspectJ的Pointcut表述语言,而底层的Joinpoint类型匹配却依然是Spring原核心。
3.2 关键点实现
- Joinpoint:在spring仅提供方法执行类型的连接点,对于属性,构造方法级别的Joinpoint,会破坏对象的封装,也是极特殊的应求需求
- pointCut接口有两个重要的方法ClassFilter(目标类以及它们的实例)和methodFilte(方法拦截),分别用于匹配被执行织入操作的对象以及相应的方法
public interface MethodMathcer{
boolean mathces(Method method,Class target)
boolean isRuntime();
boolean mathces(Method method,Class target,Object[] args)
MethodMathcer TRUE = TrueMethodMatcher.INSTANCE;
}
关于重载拦截:在对对象具体方法拦截的时候,可以忽略每次方法执行的时候调用者传入的参数,也可以每次都检查这些方法调用的参数。在isRuntion()返回false时,表示不会考虑具体jointPoint参数,这种methodFilter称之为StaticMethodMather,框架内部缓存匹配结果以提高性能。如果isRuntiome()返回true时,表明该MethodMatcher将会每次都对方法调用的参数进行匹配检查,无法对匹配结果进行缓存,称之为DynamicMethodMatcher。
+Pointcut
-MethodMatcher
-StaticMethodMatcher
-StaticMethodMatherPointcut
-NameMatchMethodPointcut #简单实现,对一组方法名称及*匹配拦截,不对重载方法名进行匹配
-AbstractRegexpMethodPointcut
-JdkRegexpMethodPointcut:jdk正则表达式
-Perl5RegexpMethodPointcut:Perl5匹配
-DynamicMethodMatcher
-DynamicMethodMathcerPointcut
-AnnotationMatchingPointCut:类级别的注解
-ComposablePointcut:提供可以进行逻辑运算的pointcut实现,并与交的运算
-ControlFlowPontcut:匹配程序的调用流程,不对方法执行的JoinPoint处的单一特征匹配,需要在运行期间检查程序的调用栈
-
advice
- per-class类型的Advice:该类型的Advice的实例可以在目标对象类的所有实例之间共享。通常只是提供方法拦截的功能,不会为目标对象类保存任何状态或者添加新的特性。
- before advice:实现的横切逻辑奖在相应的Joinpoint之前执行,通常不会打断程序的执行流程。
- throwsAdvice:通常用于对系统中特定的异常情况进行监控,以统一的方式对所发生的异常进行处理。
- afterReturningAdvice:可以访问当前Joinpoint的方法返回值,方法,方法参数以及所在的目标对象。只有方法正常返回的情况下,afterReturningAdvice才会执行。
- around advice:spring中没有直接定义对应around advice的实现接口,而是直接使用methodInterceptor来完成around advice功能。其proceed()方法,可以让程序执行继续沿着调用链传播。在process()方法前后添加横切逻辑。
- per-instance类型的advice:per-instance类型的advice不会在目标类所有对象实例之间共享,而是会为不同的实例对象保存它们各自的状态以及相关逻辑。
- Introduction可以在不改动目标类定义的情况下,为目标类添加新的属性以及行为。在spring中,为目标对象添加新的属性和行为必须声明相应的接口以及相应的实现,这样,再通过特定的拦截器将新的接口定义以及实现类中的逻辑附加到目标对象之上。之后,目标对象就拥有了新的状态和行为。
- DelegatingIntroductionInterceptor:此类会使用它所持有的同一个“delegate"接口实例 ,供同一目标类的所有实例共享使用。
- DelegatePerTargetObjectIntroductionInterceptor:此对象会在内部持有一个目标对象与相应Introduction逻辑实现类之间的映射关系。当每个目标对象上的新定义的接口方法被调用的时候,此类会拦截这些调用,然后以目标对象实例作为键,到它持有的那个映射关系中取得对应当前目标对象实例的Introduction实现类实例。
- per-class类型的Advice:该类型的Advice的实例可以在目标对象类的所有实例之间共享。通常只是提供方法拦截的功能,不会为目标对象类保存任何状态或者添加新的特性。
-
aspect
Advisor代表Spring中的Aspect,但是,Advisor通常只持有一个Pointcut和一个Advice,而理论上可以定义多个Pointcut和多个advice.
-PointcutAdvisor#此类中真正的定义一个Pointcut和一个Advice的Advisor
-AbstractPointcutAdvisor
-AbstractGenericPointcutAdvisor
-NameMatchmethodPointcutAdvisor#内部持有NameMathcMethodPointcut类型的Pointcut实例,来设置方法名拦截。
-RegexpMethodPointcutAdvisor#只能通过正则表达式为其设置相应的Pointcut
-DefaultPointcutAdvisor#通用的PointcutAdvisor实现,通过设置相应的Pointcut和Advice来使用。
-DefaultBeanFactoryPointcutAdvisor#其自身绑定到BeanFactory,可以通过容器中的Advice注册的beanName来关联对应的Advice,只有当对应的Pointcut匹配成功后,才去实例化对应的Advice,减少了窗口启动初期Advisor和Advice之间的耦合性。对应Advice的配置属性名称为"advicebeanName"
-IntroductionAdvisor#只能应用于类级别的拦截,只能使用Introduction型的Advice.
-
Ordered
当系统实现中存在多个Advisor,当其中的某些Advisor的Pointcut匹配了同一个Joinpoint的时候,就会在这同一个Joinpoint处执行多个Advice的横切逻辑。如果同一个Joinpoint处执行的Advice逻辑存在优先顺序依赖的话,实现了Ordered接口,直接配置顺序呈来指定执行顺序,Order值越低,优先级越高。 -
TargetSource
- 作用:包装目标对象的容器,当每个针对目标对象的方法调用经历层层拦截而到达调用链的终点时候,就该调用目标对象上定义的方法。通过“插足于”调用链与实际目标对象之间的某个TargetSource来取得具体目标对象,然后再调用从TargetSource中取得的目标对象上的相应方法。
- 特性:每次的方法调用都会触发TargetSource的getTarget()方法,从相应的实现类中取得具体的目标对象。
- SingletonTargetSource:内部只持有一个目标对象,当每次方法调用到时间,SingletonTargetSource都会返回这同一个目标。
- PrototypeTargetSource:此类型targetSource都会返回一个新的目标对象实例调用
- HotSwappableTargetSourcev:此类可以在应用程序运行的时候,根据某种特定条件,动态替换目标对象类的具体实现。比如base dao接口有多个数据源的实现类,在程序启动之后,默认的实现类出现了问题,可以根据条件切换到别一个实现上。典型应用场景:对双数据源互换的实现改进,使用ThrowsAdvice对数据库相关异常进行捕捉,在捕捉到必要的切换信息后,调用HotSwappableTargetSouce的swap方法使用新的数据源替换旧的数据源。
- CommonsPoolTargetSource:使用现有的Jakarta commons pool提供对象池支池,返回有限数据上限的目标对象实例,好像获取连接池中的Connection一样
- ThreadLocalTargetSource:为不同的线程调用提供不同的目标对象,保证各自线程上对各目标对象的调用。
3.3 开启实践
- 基于annonation的spring aop拦截,使用@AspectJ标签
- 在配置文件中添加
<aop:aspectj-autoproxy/>
注解 - 创建一个Java文件,使用
@Aspect
注解修饰该类 - 创建一个方法,使用
@Before
、@After
、@Around
等进行修饰,在注解中写上切入点的表达式
- 在配置文件中添加
- 基于配置文件的配置
- 创建一个Java文件,并指定一个用于执行拦截的方法,该方法可以有0个或多个参数
- 在Spring配置文件中注册该Java类为一个Bean
- 使用
<aop:config/>
、<aop:aspect/>
等标签进行配置