SpringAOP知识点梳理

这一生的挚爱 提交于 2020-02-25 22:08:15

在前段时间面试时,曾多次被问到“对AOP你了解多少”这样的问题。以前工作中虽然有用过 Spring AOP,但由于没有系统得自学过,所以关于Spring AOP 这块,可以说是基础甚差。即使临时抱佛脚,凭借网上面试宝典中前人的总结,在面对面试官时,也仅仅说出“AOP是面向切面编程”、“运用了代理模式”云云。多次碰壁后,我意识到自己过于自信,没有系统概念,就无法在面试官前多说出一句有意义的回答。

最近我把《Spring实战第四版》关于AOP那章看了一遍,除了最后的“注入AspectJ切面”外,其他知识点,都简单地做了梳理。AspectJ切面,由于涉及到AspectJ 自有AOP语法,我暂时不准备补习,所以不做梳理。

我认为,关于 Spring AOP,可以从这几个问题开始:

  1. 什么是 AOP?
  2. AOP 要解决的问题是什么?
  3. 关于 AOP 的术语?
  4. Spring AOP 的实现方式? 

 一、什么是AOP?

AOP,全称:Aspect Oriented Programming,意为面向切面编程,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。(此概念来自百度百科)

AOP框架不只是定义在Spring意义下,还有很多其他的AOP框架。不同的 AOP框架可能在连接点模型上有强弱之分,例如:有些通知可以应用在字段修饰符级别,另一些则只支持与方法调用相关的连接点;除此之外,织入切面的方式和时机也有所不同。但是,创建切点来定义切面所织入的连接点是 AOP 框架的基本功能。

常见的 AOP 框架是 Spring AOP和 AspectJ。AspectJ扩展了Java语言,定义了AOP语法,有专门的编译器来生成遵守Java字节编码规范的Class文件。Spring和AspectJ项目之间大量协作,且Spring对AOP的支持很多方面借鉴了 AspectJ。

二、AOP解决的问题是什么?

假设有这样一个场景,我们需要为一个项目的接口记录日志。在不使用切面的情况下,我们可能就在每个接口中一个个地加入记录日志的逻辑。这就有两个缺点:一是代码逻辑混乱,项目自有项目的主要功能,记录日志这样的功能,最好与主要业务逻辑分隔开;二是修改难度大,比如我们要在记录日志的方法中增加或去掉一个参数,甚至有一天我们不再需要记录日志,需要改动的代码范围可能会很大。

不只是记录日志这个功能,还有安全控制、事务管理等等其他的与主业务关注点不一致的功能。

那么 AOP 解决的问题就是,将业务关注点分离,实现非关注点功能统一管理,达可配置、可插拔的程序结构。

三、AOP术语

3.1 通知

(有地方也叫做增强)

通知定义了一个切面要做什么和什么时候做。

Spring 切面的5种类型的通知:

  • 前置通知(Before):在目标方法被调用之前调用通知功能;
  • 后置通知(After):在目标方法完成之后调用通知,此时不关心方法的输出是什么;
  • 返回通知(After-returning):在目标方法成功执行之后调用通知;
  • 异常通知(After-throwing):在目标方法抛出异常后调用通知;
  • 环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为。

3.2 连接点

连接点是在应用代码中能插入切面的一个点,这个点可以是调用方法时、抛出异常时、甚至修改一个字段时。

3.3 切点

前面通知定义了切面要做什么和什么时候做,切点则定义了在哪里执行。切点是一个或多个连接点的组合,通常使用明确的类或方法名称,或正则表达式匹配的类或方法名称来指定这些切点。

3.4 切面

切面时切点和通知的组合,切点和通知的组合共同定义了切面的内容:要做什么,在何时和何处执行。

3.5 引入

引入允许我们向现有的类添加新的方法或属性。我们正是通过切面实现了这个功能。

3.6 织入

与引入不同,引入添加的是方法或属性,织入添加的是对象。引入是把切面应用到方法上,织入是把切面应用到对象上。

在目标对象的生命周期里,以下几个时期可以进行织入:

  • 编译期:切面在目标类编译时被织入。AspectJ 采用这种方式织入切面。
  • 类加载期:切面在目标类加载到JVM时被织入。这种需要特殊的类加载器,AspectJ5的加载时织入支持这种方式织入切面。
  • 运行期:切面在应用运行的某个时刻被织入。一般在织入切面时,AOP容器会为目标对象动态地创建一个代理对象。Spring AOP 就是以这种方式织入切面。

四、Spring AOP

4.1 Spring AOP概述

4.1.1 Spring 提供了4种类型的 AOP 支持

(1)基于代理的经典Spring AOP
        Spring经典的AOP看起来显得过于复杂和笨重,现在Spring提供了更简洁和干净的方式,所以这种方式不再推荐使用。

(2)纯POJO切面

        通过XML配置,借助Spring的aop命名空间,可以将纯POJO转换为切面。这些POJO只需提供满足切点条件所需调用的方法。

(3)@AspectJ注解驱动的切面

        Spring基于注解驱动的AOP,借鉴了AspectJ的切面,本质上依然是基于代理的AOP,但编程模型几乎与AspectJ注解切面完全一致。好处是不需要使用XML配置来完成。

(4)注入式AspectJ切面(适用于Spring各版本)

        如果AOP需求超过了简单的方法调用(如构造器或属性拦截),那么需要考虑使用AspectJ来实现切面。

4.1.2 SpringAOP的三个特点

(1)Spring通知是Java编写
        Spring通知采用标准Java类编写,定义通知所用的切点通常使用注解或XML配置来编写。这对Java开发者来说很熟悉更容易上手。

        而AspectJ上面有介绍,它有独有的AOP语法,虽然可以获得更强大和细粒度的控制和更丰富的AOP工具集,但是需要开发者额外学习新的工具和语法。

(2)Spring在运行时通知对象

        Spring 的切面由包裹了目标对象的代理类实现:代理类封装了目标类,并拦截被通知方法的调用,再把调用转发给真正的目标bean。当代理拦截到方法调用时,在调用目标bean方法之前,会执行切面逻辑。

        直到应用需要被代理的bean时,Spring才创建代理对象。如果使用的是ApplicationContext的话,在ApplicationContext从BeanFactory中加载所有bean时,Spring才会创建被代理的对象。

(3)Spring只支持方法级别的连接点

        Spring基于动态代理,所以Spring只支持方法连接点。而其他的一些AOP框架不同,例如AspectJ和JBoss,除了方法切点,它们还支持字段和构造器接入点。Spring则缺少对字段连接点的支持,无法让我们创建细粒度的通知;且不支持构造器连接点,无法在bean创建时应用通知。

        方法拦截可以满足绝大部分的需求,如果还需要方法拦截之外的连接点拦截功能,可以利用AspectJ来补充SpringAOP的功能。

4.2 注解方式实现

4.2.1 注解说明

4.2.1.1 定义切点

@Pointcut

4.2.1.2 切点表达式

4.2.1.3 定义通知

(1)@Before 前置通知

        通知方法会在目标方法调用之前执行。

2@After 后置通知

        通知方法会在目标方法返回或抛出异常后调用。

(3)@AfterReturning 返回通知

        通知方法会在目标方法返回后调用。

(4)@AfterThrowing 异常通知

        通知方法会在目标方法抛出异常后调用。

(5)@Around 环绕通知

        通知方法会将目标方法封装(包裹起来

4.2.1.4 切面

@Aspect

4.2.2 简单切面案例

这个案例,通过极其简单的代码,展示了一个简单切面的实现方式。

4.2.2.1 定义切面

创建切面类,通过注解定义切点、通知类型等等

切点注解里包含一个切点表达式,这个表达式匹配到了一个具体的无参方法。下面定义的几个通知方法,都是围绕这个切点进行通知。例如silenceCellPhones() 方法,方法上注解了@Before,说明是一个前置通知。那么就会在切点表达式匹配到的那个方法被调用之前先执行,而后再执行切点方法。其他通知方法以此类推。

4.2.2.2 启动自动代理

要想使用切面,需要将Audience类的bean加入到Spring上下文中,并开启自动代理功能,可以通过JavaConfig,也可以通过XML配置,根据实际情况选择。

(1)JavaConfig方式

(2)XML配置方式

4.2.2.3 特点或不足

这个切面实现相对简单,但有三个注意点:

  1. 多个通知引用同一个切点,可考虑整合成一个通知,这尤其有利于多个通知需要处理同一个变量的情况;
  2. 是不需要处理通知参数;
  3. 这些类我们有源码,可以自行添加注解;对于那些无法修改源码的类,我们无法通过注解方式将其定义为切面。

对于前两点,可以分别通过环绕通知或有参切点表达式处理,这都是在注解方案内可实现的。对于第三点,可以通过后面的XML方式来解决。

4.2.3 环绕通知

这个通知方法,将前面几个通知方法整合到一起,效果很明显了。这个方式对于多个通知处理同一个变量尤其有效。

环绕通知方法,ProceedingJoinPoint 这个对象是必须要有的,因为你要在通知中通过它来调用被通知的方法。通知方法中可以做任何事情,当要将控制权交给被通知的方法时,它需要调用 ProceedingJoinPoint 的 proceed() 方法。

4.2.4 传递通知参数

先看一个有参数的切点方法所在的类:

然后看看切面类是怎么定义的:

切点表达式中的 arg(trackNumber) 限定符。它表明传递 playTrack() 方法的 int 类型参数也会传递到通知中去。参数的名称 trackNumber 也与切点方法签名中的参数相匹配。

这个参数会传递到通知方法中,这个通知方法是通过 @Before 注解和命名切点 trackPlayed(trackNumber) 定义的。切点中定义的参数与切点方法中的参数名称是一样的,这样就完成了从命名切点到通知方法的参数转移。

注意:execution表达式中,全路径方法的参数类型必须与实际方法的参数类型一致。CompactDisc 及 BlankDisc 的 playTrack 方法参数均为 Integer,那么execution 中的参数类型也必须为 Integer。如果不一致,即其中一个为 int ,另一个为 Integer,执行测试时就会发现,切面没起作用。

4.2.5 引入新方法

(1)关键注解及说明

    @DeclareParents

  • value 属性指定了哪种类型的 bean 要引入该接口。在本例中,也就是所有实现 CompactDisc 的类型。(标记符后面的“+”表示是 CompactDisc 的所有子类型,而不是 CompactDisc 本身。)
  • defaultImpl 属性指定了为引入功能提供实现的类。在这里,我们指定的是 DefaultEncoreable 提供实现。
  • @DeclareParents 注解所标注的静态属性指明了要引入接口。在这里,我们所引入的是 Encoreable 接口。

(2)Demo

前面介绍的那些内容,从某种角度,可以说是Spring AOP为已经存在的方法添加了新功能。不仅如此,我们还可以为已经存在的bean添加新的方法。

对于添加新功能和新方法这个事,我本来也是十分困惑。在我看来,很多代码都可以封装成方法,添加新功能的代码也是如此。那么两者到底有什么区别呢?我就用一个代码Demo来说明,正好展示一下这个注解的用法。

首先是一个目标功能接口,不用在意它的实现类的内容,只需要知道接口的这两个方法。我们要在这个接口引入一个新的接口。

定义一个要被引入的接口及其实现类:

定义一个切面,为CompactDisc类引入Encoreable接口的方法:

现在可以看效果了,先看看单元测试怎么设计的:

现在是把一个CompactDisc 对象,强转型为我们引入的Encoreable接口类型,然后调用引入接口对象的方法。现在看控制台结果:

其实在看到上面单元测试内容的时候我有点不确定,不确定这是不是@DeclareParents的正确用法,还有单元测试效果是不是应该期望的。于是我把EncoreableIntroducer类,即切面类中的@DeclareParents注解注释掉,再执行单元测试,看结果:

en指向的仍然是cd对象,cd强转不成Encoreable类型,也就是,没有那个注解,cd也调用不了Encoreable接口的方法。这需要细品。

这就是我对于@DeclareParents用法的个人理解了。

4.2.6 注解方式优缺点

优点:简单,只需少量Spring配置。

缺点:必须有源码才能为通知类添加注解。

4.3 XML方式实现

基于注解的方式优于基于XML配置的方式,但当我们无法为通知类添加注解时,就需要转战XML配置。

4.3.1 Spring AOP配置元素

AOP配置元素

用途

<aop:advisor>

定义 AOP 通知器

<aop:after>

定义 AOP 后置通知(不管被通知的方法是否执行成功)

<aop:after-returning>

定义 AOP 返回通知

<aop:after-throwing>

定义 AOP 异常通知

<aop:around>

定义 AOP 环绕通知

<aop:aspect>

定义一个切面

<aop:aspectj-autoproxy>

启用 @AspectJ 注解驱动的切面

<aop:before>

定义一个 AOP 前置通知

<aop:config>

顶级的AOP配置元素,大多数的<aop:*>元素必须包含在<aop:config>元素内

<aop:declare-parents>

以透明的方式为被通知的对象引入额外的接口

<aop:pointcut>

定义一个切点

4.3.2 简单切面案例

这次还用注解方式里的案例,但是把Audience类的注解去掉,就变成了一个极普通的POJO:

然后在XML中,通过Spring aop命名空间的一些元素,将这个普通的POJO转换为切面:

这仍然是一个通知方式比较分散的案例,分别使用了前置、前置、返回和异常通知。具体流程不赘述。

4.3.3 环绕通知

还是考虑到几个通知分开执行,造成信息无法共享的问题,我们将几个方法合二为一。这与注解方式案例里没有区别。

再看怎么在XML的aop命名空间内声明:

4.3.4 传递通知参数

还是之前的TrackCounter类,去掉里面的注解:

然后在XML配置中,声明TrackCounter bean和BlankDisc bean,并将 TrackCounter 转化为切面。

注意:expression 的属性值中,使用的是 and 关键字而不是 &&,因为在 XML 中,& 符号会被解析为实体的开始。

4.3.5 引入新方法

相对注解方式里的@DeclareParents,在Spring aop命名空间里也有<aop:declare-parents>元素,实现相同的功能。

样例一:

其中明显与注解方式里不同的是“types-matching”这个属性,它的属性功能正对应@DeclareParents注解里的value属性功能,表明新引进的接口目标类是谁。

样例二:

样例二与样例一的不同之处在于,样例一的default-impl变成了delegate-ref,delegate-ref的属性值是一个已经声明好的bean的id。这效果与样例一一致。

五、SpringAOP与代理模式

SpringAOP底层基于动态代理实现。所以我决定再简单介绍一下关于代理相关的内容。

5.1 代理模式简介

面向对象的设计模式有23中,代理模式是经典模式之一。

根据《设计模式:可复用面向对象软件的基础》书中介绍,代理模式的意图是:为其他对象提供一种代理以控制对这个对象的访问,原因是为了只有我们确实需要这个对象时才对它进行创建和初始化。

代理模式分为静态代理和动态代理,静态代理是我们自己创建代理类,而动态代理是程序自动帮我们生成代理。

5.2 静态代理

通过一个Demo来演示静态代理怎么实现。我们设定一个情景:房主有栋房子,想要通过中介代理租出去。

先定义一个Person接口,里面有个出租方法:

然后定义一个房主类,这是个被代理类,实现了Person接口:

定义一个中介代理类,实现了和被代理类一样的接口:

如此,中介代理了房主的房子,假如有租客来租房子,不必直接找到房主,通过中介即可。

查看测试类和测试结果:

这就是静态代理的用法,代理类实现和被代理类相同的接口,实现同样的方法,代理类在自己的同名方法中根据需求实现自己的逻辑。

5.3 动态代理

Java中动态代理有两种实现方式:JDK动态代理CGLIB动态代理

JDK动态代理是利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理;

CGLIB动态代理是利用ASM(开源的Java字节码编辑库,操作字节码)开源包,将代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。

二者有所区别:JDK动态代理只能实现有接口的类生成代理;CGLIB是针对类实现代理,对指定的类生成一个子类,并覆盖其中的方法,这种通过继承类的实现方式,不能代理final修饰的类。

下面通过Demo来分别演示两种动态代理。

5.3.1 JDK动态代理

依旧是熟悉不变的Person接口:

和熟悉不变的房主实现类:

不同的是,这次我们不再手动创建那个房屋中介的代理类,而是创建一个可以自动生成代理类实例的代理类处理器。获取代理类实例的主要方法是Proxy.newProxyInstance(),这个方法需要三个参数:

  • ClassLoader loader:指在程序运行时将代理类加载到JVM时的类加载器对象;这里可以与房主类或者说被代理类的类加载器一致;
  • Class[] interafaces:被代理类的所有接口信息,便于生成的代理类可以具有代理接口的所有方法;
  • InvocationHandler h:调用处理器,调用实现了InvocationHandler类的一个回调方法;我们主要需要实现这InvocationHandler接口的invoke方法;这个invoke方法有三个参数:

  • Object proxy:代理对象;
  • Method method:代理对象当前调用的方法;
  • Object[] args:方法参数。

这三个参数我们可以先不关注,而是关注invoke这个方法。当我们程序运行时,一旦调用Person接口的rentHouse()方法时,实际执行的正是invoke方法里的逻辑。下面看代理类处理器怎么定义:

或者我们不显式实现InvocationHandler接口,而是直接搞一个匿名内部类来实现InvocationHandler接口,推荐这种方式:

查看单元测试:

5.3.2 CGLIB动态代理

JDK实现动态代理需要实现类通过接口定义业务方法。但是如果代理类没有接口时,就需要CGLIB来实现动态代理了。

CGLIB采用了非常底层的字节码技术,其原理是:通过字节码技术为一个类创建子类,并在子类中采用拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。但由于是采用继承,所以不能对final修饰的类进行代理。

看看CGLIB代理类处理器怎么实现:

Enhancer类是CGLIB中的一个字节码增强器,可以方便对你想处理的类进行扩展。

看单元测试执行情况:

在写这个Demo时,我曾在CglibProxyHandler的intercept方法里,调用的是methodProxy.invoke()方法,而非invokeSupper()方法,最终导致了这个执行结果:

这异常的大致原因就是函数调用栈太深。具体源码,我也没看大明白,也希望见到这篇文章的大哥可以解答一二。

5.3.3 总结

  1. JDK动态代理底层使用反射机制,CGLIB代理使用字节码处理框架ASM,通过修改字节码生成子类。
  2. JDK动态管理创建代理对象,效率较高但执行效率较低;CGLIB创建效率较低,但执行效率较高。
  3. JDK动态代理机制是委托机制,动态实现接口类,在动态生成的实现类里委托handler去调用原始实现类方法;CGLIB则使用继承机制,被代理类和代理类是继承关系,所以代理类可以赋值给被代理类。

5.4 SpringAOP采用何种代理?

JDK动态代理和CGLIB动态代理都是实现SpringAOP的基础。

我们查看DefaultAopProxyFactory这个类源码,这个类是SpringAOP框架中的类:

根据源码可得知,SpringAOP具体采用哪种代理方式,是通过代码判断的。

——————————————————————————————————————————————————————————————————

前辈刚给提出意见,关于AOP这块,从事务管理延伸一下。立个Flag,希望后面有机会搞一个Demo,AOP实现事务管理。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!