【Spring杂烩】初探Spring的AOP方案 | Spring AOP

初入学习Spring的AOP实现方案 - Spring AOP


aop的知识点,的确比较的…多;所以慢慢看吧

  • 前置知识
    • 什么是AOP
    • 它与OOP的差异?
    • AOP的术语
    • AOP的实现者
    • Spring 对AOP的支持
    • Spring AOP和注入式AspectJ切面在编程上的区别
  • Spring AOP的基础知识
    • Spring AOP面向切面编程
    • Spring AOP的实现原理|动态代理
  • 基于@Aspect注解驱动的Spring AOP基础
    • @Aspect的语法基础
    • 切点表达式函数
    • 函数入参中的通配符
    • 逻辑运算符
    • 增强注解类型
  • 基于@Aspect的AOP代码实现
    • 声明一个切面以及切点
    • 声明一个复合切点
    • 访问连接点信息
    • 绑定连接点xxx到增强方法中
  • 相关问题
    • Spring AOP和AspectJ之间的关系和区别
    • Filter、Intercepter、Spring AOP、ControllerAdvice的介绍和区别
    • Jdk动态代理和Cglib动态代理自定义实现

前置知识


什么是AOP?

AOP是什么?AOP又称面向切面编程,是OOP的有益延伸。我们知道,面向对象的特点是继承、多态和封装。而封装就要求将功能分散到不同的对象中去。也就是说,让不同的类设计不同的方法。这样代码就分散到一个个的类中去了,这样就可以降低代码的复杂程度,增强类的可重用性。

但是之后我们也会发现,在分散代码的同时,也增加了代码的重复性。什么意思呢?比如说,我们在两个类中,可能都需要在每个方法中做同一的方法,比如做日志,权限判断等等通用的一些方法。按OOP的思想,我们就必须在两个类的方法中都加入这些重复的逻辑判断。也许他们是完全相同的,但就是因为面向对象的设计让类与类之间无法联系,而不能将这些重复的代码统一起来。

但是你可能还会说,我们可以将这些重复代码抽象出来,封装到一个类的方法中呀,让其他方法都调用它。嗯,是的,没有错。但是这样做的形式并没有从根本上解决所有的方法最终还是要调用封装好的方法的问题。每个方法中都还是有一句重复的方法调用,这种形式的封装所带来的好处仅仅是把代码缩短了而已。

此时AOP的场景就出现了,他就可以将类与类方法都通用的一些代码逻辑抽离出来,而且完全不需要在原来的方法中体现出要去调用这些重复代码的特征。而是通过切面的方式,在编译时,运行时或者其他时候将这些重复代码在不修改原有代码的基础上织入进去。

AOP (面向切面编程)- 百度百科
什么是面向切面编程AOP?- @作者:欲眼熊猫


它与OOP的差异?

引用至《精通Spring 4.x企业应用开发实战》

编程语言的终极目标就是能以更自然,更灵活的方式模拟世界,从原始机器语言学习到过程语言再到面向对象语言,编程语言一步步的用更自然,更灵活的方式编写软件。AOP是软件开发思想发展到一定阶段的产物,但AOP的出现并不是要完全的替代OOP,而仅作为OOP的有益补充

所以我们知道了AOP面向切面编程的出现仅仅是作为OOP面向对象编程的有益补充,而Spring AOP的就是能够让我们通过OOP面向对象的思维去实现AOP面向切面的功能。


AOP的术语

要了解AOP技术的实现,尤其是了解Spring AOP,我们就必须来了解以下的一些AOP术语:

  • 连接点(Joinpoint)
    连接点是代码执行过程中的某个特殊位置,比如类开始初始化前,类初始化后,类的某个方法调用前,某个方法调用后,方法抛出异常后等。像这类具有边界性质的的特定点,我们就可以成为连接点。在Spring AOP中,因为底层是基于动态代理实现的,所以它只支持方法的连接点。连接点有两部分信息构造:代码执行点方位;比如某个方法是连接点,那么方位就是这个方法执行前或方法执行后,但是方位信息是在增强中存储的

  • 切点(PointCut)
    每个程序类都有很多个连接点,如一个拥有两个方法的类,这两个方法都是连接点,即连接点是程序类客观存在的事物。那它跟切点有什么关系呢?在为数众多的连接点中,我们怎么做到在特定的连接点中执行我们的代码织入呢?那么切点就是这么一个东西,它就是一种标识,这个标识就叫连接点的切入点。即当我们知道切入点时,我们就可以根据这些切入点(标识),找到它所对应的连接点进行切面编程。书中是用数据库记录代表连接点,用查询条件来代表切点进行描述的。所以切点就是一系列连接点的共性标识,可能是一对一关系,也可能是一对多关系。

  • 增强(Advice)
    增强在有些地方又叫通知,但我不喜欢通知这个称呼,因为它会误导我的理解;增强就是织入到目标类连接点的一段代码,我们就可以简单的理解成它是要插入进去的,准备增强扩展目标类的代码段。但它也不完全只是一段代码,它还包括了一个关于连接点的信息,那就是连接点的方位信息,即在我这段增强代码是在连接点的前还是后位置织入。所以从增强中,我们知道了要插入的代码,也知道了要插入连接点的前后方位,然后再通过与切点配合,找到要织入的连接点。我们就可以将扩展代码织入到连接点中准确的位置

  • 目标对象(Target)
    目标对象就很好理解了,他就是需要被切面编程,织入扩展代码的目标类。他不需要跟AOP代码有任何关联

  • 引介(Introduction)
    引介是一种特殊的增强,它为类添加了一些属性和方法。这样,即使一个业务类原本没有实现某个接口,通过AOP的的引介功能,也可以动态的为该业务类添加接口的实现逻辑,让该业务类成为这个接口的实现类。即普通的增强仅仅是在目标对象的原有方法或代码进行增强。但引介可以实现新的方法或属性

  • 织入(Weaving)
    织入就是将增强插入到目标对象的具体某个或某些连接点的行为或过程。AOP就像一台织布机,将目标对象、增强或引介天衣无缝的编织在一起。AOP织入大致有4种方式:编译时,后编译时,类加载时和运行时织入。Spring AOP采用的是运行时织入

  • 代理(Proxy)
    一个类被AOP织入增强后,就产生了一个结果类,它是融合了原目标对象和增强逻辑的代理类。根据不同的代理形式,代理类即可以是跟原类具有相同接口的类,也可能是原类的子类,所以可以采用与调用原类相同的方式调用代理类。

  • 切面(Aspect)
    切面是什么?切面就是由切点和增强组合成的一个新定义,它即包括横切逻辑的定义,也包括连接点的定义。而Spring AOP就是负责实施切面的框架,它将切面所定义的增强代码,织入到切面所指定的连接点中。如果使用Spring AOP的代码层面去理解的话。使用了@AspectJ注解的哪个类就是一个切面,我们可以在切面中声明@PointCut切点,还可以声明要增强的代码片以及要织入连接点的方位。

通过上面AOP的基础概念学习,我们将这些术语串联起来表述AOP切面编程到做的事情,那就是:

  • 定义切面(相当于定义了切点和增强)
  • 通过切点定位到要织入的连接点上,并通过增强提供的连接点方位信息得知要织入的方位
  • 将增强织入到目标对象连接点的特定方位位置,这个织入过程是可能是通过代理来实现的

AOP的实现者

Aop工具的设计目标是把横切的问题(如性能监控,事物管理)模块化。使用类似OOP的方式进行其切面的编程工作。位于AOP工具核心的是连接点模型,它提供了一种机制,可以定位到需要在哪里发生横切

  • AspectJ
    AspectJ是语言级的AOP实现,AspectJ扩展了Java语言,定义了AOP语法,能够在编译期提供横切代码的织入。所以它有一个专门的编译器用来生成遵守Java字节编码规范的Class文件。

  • AspectWerkz
    AspectWerkz是基于Java的简单,动态,轻量级AOP框架。它支持运行期或类加载期织入横切代码,所以它拥有一个特殊的类加载器。不过AspectJ早已经跟AspectWerkz项目合并了。以便整合两者的力量和技术创建统一的AOP平台。他们合作的第一个发布版本是AspectJ5,扩展了AspectJ语言,以基于注解的方式支持类似AspectJ的代码风格

  • JBoss AOP
    JBoss AOP于2004年作为JBoss应用程序服务器框架的扩展功能发布

  • Spring AOP
    Spring AOP使用纯Java实现,它不需要专门的编译过程,也不需要特殊的类加载器,它在运行期通过代理方式向目标类织入增强代码。Spring并不尝试提供最完整的AOP实现,相反,它侧重于提供一种和IoC容器整合的AOP实现,用以解决企业级开发中的常见问题。在Spring中可以无缝的将Spring AOP ,IoC和AspectJ整合在一起


Spring 对AOP的支持

引用至《Spring 实战》

并不是所有的AOP框架都是相同的,它们来连接点模型上可能有强弱之分。有些允许在字段修饰符级别应用通知,而另一些只支持与方法调用相关的连接点。它们织入切面的方式和时机也有所不同。但是无论如何,创建切点来定义切面所织入的连接点是AOP框架的基本功能

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

  • 基于代理的经典Spring AOP
    经典的Spring AOP编程模型并不怎么样,虽然它曾经也辉煌过。但是Spring 现在提供了更棒的面向切面编程模型,经典的Spring AOP编程模型是基于ProxyFactory实现的

  • 纯POJO切面
    借助Spring的AOP命名空间,我们可以将纯POJO转换成切面。事实上这些POJO只是提供了满足切点条件时所要调用的方法。但是这种技术有缺点,就是需要XML配置,但这的确是声明式地将对象转换成切面的简便方式。说白了这种方式就是Spring AOP的Xml版本,它对代码几乎没有侵入性,比如要声明Java的Bean为一个切面,我只需要在Xml通过aop命名空间声明就可以了,不需要对Java代码进行改造。

  • @Aspect注解驱动的切面
    Spring借鉴了AspectJ的切面,以提供注解驱动的AOP实现方案,本质上,它依然是Spring基于代理的AOP,但是编程模型几乎与编写成熟的AspectJ注解切面完全一致,可以说是套了AspectJ的外壳的Spring AOP。这种AOP风格的好处就在于能够不使用XML来完成功能,并且与AspectJ风格无缝衔接(这种方式应该是目前最流行的方式

  • 注入式AspectJ切面
    如果你的AOP需求超过了简单的方法调用(如构造器或属性拦截),那么你就需要考虑AspectJ来实现切面,在这种情况下,注入式AspectJ切面编程可以帮将值注入到AspectJ驱动的切面中。注入式AspectJ切面不属于Spring AOP的范畴,是真正的AspectJ,只不过Spring支持在自己的应用中使用AspectJ AOP方式进行开发(为什么叫注入式呢?可能是Spring能为AspectJ做的就是为AspectJ切面注入协助类吧)

前三种都是Spring AOP的变体,Spring AOP构建在动态代理基础之上,因此Spring AOP的支持局限于方法拦截。注入式AspectJ切面则是真正属于AspectJ的范畴,与Spring AOP没有什么关系,当Spring AOP无法满足我们的需求的时候,我们就可以在Spring应用中使用AspectJ切面来完成这项工作,当然你需要属于AspectJ特定的语法规则

所以之后,我们认真的区分Spring AOPAspectJ AOP 名词的区别


Spring AOP和注入式AspectJ切面在编程上的区别
  • Spring的增强是Java实现的
    Spring所创建的增强都是标准的Java类编写的。而且定义增强所在的切点通常会使用注解或XML去编写。这两种语法对于Java程序员来说是相当的熟悉。AspectJ与之相反。虽然AspectJ支持基于注解的切面,但AspectJ最初是以Java语言的扩展来实现的。虽然这可以给AspectJ带了很多好处,但是如果我们要去使用它,还要学习新的语法和工具。

  • Spring 在运行时增强对象
    通过在代理类中包裹切面,Spring在运行期把切面织入到Spring管理的Bean中。代理类封装了目标类,并拦截被增强的方法调用,再把调用转发给真正的目标Bean。当在调用目标bean方法之前,会执行切面逻辑。直到应用需要被代理的Bean时,Spring 才会创建代理对象。如果使用的是应用上下文的话,在应用上下文中加载Bean的时候,Spring 才会创建被代理的对象。因为Spring运行时才创建代理对象,所以我们不需要特殊的编译器来织入Spring AOP的切面

  • Spring 只支持方法级别的连接点
    通过使用各种AOP方案可以支持多种连接点模型。因为Spring基于动态代理,所以Spring只支持方法连接点。这与一些其他的AOP框架是不同的。例如AspectJ和JBoss,除了方法接入点,它们还提供字段和构造接入点。Spring缺少对字段连接点的支持,无法让我们创建细粒度的通知。例如拦截对象字段的修改。而且它还不支持构造器连接点,我们就无法在bean创建时应用通知


Spring AOP的基础知识


Spring AOP面向切面编程
Spring AOP的增强类型
  • 前置增强
  • 后置增强
  • 环绕增强
  • 异常抛出增强
  • 引介增强
Spring AOP的切点类型
  • 静态方法切点
  • 动态方法切点
  • 注解切点
  • 表达式切点
  • 流程切点
  • 复合切点
Spring AOP的切面类型
  • 一般切面
  • 切点切面
  • 引介切面

Spring AOP的实现原理|动态代理

Spring AOP的基本原理是基于代码模式的,准确的说在Spring中是通过JDK动态代理CGLIG动态代理两种方式去实现Spring AOP的。

两种代理模式的区别?
  • JDK动态代理是面向接口,利用拦截器与Java反射的机制生成实现代理接口的匿名类的方式实现的,是Java本身就提供的方式,不需要依赖第三方库

  • CGLIG动态代理是面向类的,利用ASM开源包,在目标类的字节码基础上进行修改,生成目标类的子类,它本身不属于Java自身的实现,所以需要依赖CGLIB第三方库

Spring 如何运用这两种代理模式?

Spring默认情况下是会自动根据条件和需求来判断具体使用哪种代理方式

  • 当目标类实现了接口,Spring默认情况下回采用JDK动态代理实现AOP
  • 当目标类实现了几口,可以通过设置,强制使用CGLIB动态代理实现
  • 当目标类没有实现接口,Spring则默认使用CGLIB动态代理实现
JDK动态代理和CGLIG动态代理的优劣之分
  • 使用CGLib实现动态代理,CGLib底层采用ASM字节码生成框架,使用字节码技术生成代理类,在JDK6之前比使用Java反射效率要高
  • 在JDK6、JDK7、JDK8逐步对JDK动态代理优化之后,在调用次数较少的情况下,JDK代理效率高于CGLIB代理效率,只有当进行大量调用的时候,JDK6和JDK7比CGLIB代理效率低一点,但是到JDK8的时候,JDK代理效率高于CGLIB代理,
  • CGLIB的出现是为了解决JDK动态代理无法面向类代理的缺点

文章后面相关问题章节,有关于JDK动态代理和CGLIB动态代理的自定义代码实现


基于@Aspect注解驱动的Spring AOP基础


因为基于注解驱动的@AspectJ式Spring AOP已经是Spring AOP中最流畅的方式之一,其他的3中方式我们就不过多讨论了。

所以本节点就是说明我们是怎么使用注解驱动的@AspectJ式的Spring AOP去实现面向切面编程的


@Aspect的语法基础

@Aspect的语法基础,我们总共从四个方面进行说明:

  • 切点表达式函数
  • 函数入参中的通配符
  • 逻辑运算符
  • 增强注解类型

切点函数详解

切点表达式与切点函数:

一般来说切点表达式和切点函数可以说是一个东西,函数就是一个@execution组成的东西,而一个表达式可以是由多个函数组成,有逻辑运算符进行连接

切点函数可以有两部分组成:

  • 关键字
    execution
  • 操作参数
    * *(**)

切点函数是什么?

  • 我们知道切点函数可以分为两个部分,关键字和操作参数;
  • 关键字表示以何种方式进行横切,操作参数表示匹配项,两者结合起来就可以组成切点

所以切点函数的作用就是通过关键字和操作参数来指定我们的切点是什么

Spring AOP的切点函数可以分为4类:

  • 方法切点函数
    通过描述目标类的信息定义连接点
  • 方法入参切点函数
    通过描述目标类方法入参的信息定义连接点
  • 目标类切点函数
    通过描述目标类型的信息定义连接点
  • 代理类切点函数
    通过描述目标类的代理类的信息定义连接点
方法切点函数
函数入参说明
execution()方法匹配串表示满足某一匹配模式的所有连接点,功能比较全能,拦截范围大到包,小到方法入参。如execution(* greet(…))表示所有目标类的greet()方法
@anntation()方法型注解名表示标注了特定注解的目标类方法连接点。如@annotation(com.snailmann.Test)表示所有被@Test注解所修饰的方法

@annotation:
@annotation比较简单,就时根据修饰在方法上的注解来拦截方法

.
execution:
execution()切点就比较复杂了,功能非常的齐全,拦截范围大到包,小的方法入参;大致实现可以分为四类

  • 通过方法入参来定义切点
    execution(* find(String , Integer)) ; 匹配所有叫find()的方法,且该方法仅有两个参数,一个是String,一个是Integer类型
    execution(* find (String , *)); 匹配所有叫find()的方法,且该方法仅有两个参数,一个是String, 另一个任意
    execution(* find(String , ..)) ; 匹配所有叫find()的方法,且第一入参是String类型,剩下可以有任意其他参数
    execution(* find(Object+)); 匹配所有叫find()的方法,只有一个入参,且入参类型必须是Object类或其子类;如果没有+,则只能匹配Object类
  • 通过方法签名定义切点
    execution(public * *(..)); 匹配所有的包下的所有类的public方法,返回值任意,参数类型与个数任意
    execution(* *To(..)); 匹配任意包下任意类的以To结尾的方法,返回值任意,参数类型与个数任意
  • 通过类定义切点
    execution(* com.snailmann.Car.*(..)); 匹配com.snailmann包下的Car类的任意方法
    execution(* com.snailmann.Car+.*(..)); 匹配Car类以及其子类、实现类的任意方法
  • 通过包定义切点
    execution(* com.snailmann.* (..)); 匹配com.snailmann包下的任意类的任意方法
    execution( * com.snailmann..* (..)); 匹配com.snailmann包下以及子包下的任意类的任意方法
    exectuion( * com.snailmann..*.*Dao.find *(..)); 匹配com.snaillann包下以及子包下的以Dao结尾的类的以find开头的方法
方法入参切点函数
函数入参说明
args()类名通过判别目标类方法运行时入参对象的类型定义指定连接点,如args(com.snailmann.Food)表示所有只有一个参数,且参数类型为Food类的方法
@args()类型注解名通过判断目标类方法运行时入参对象的类是否标注了特定注解来指定连接点。如@args(com.snailmann.Test)表示所有仅有一个参数,且这个参数的类声明被@Test注解所修饰的方法,注意不是注解在方法参数位置修饰

args()和@args()都比较简单

  • args()
    args(com.snailmann.Student,..);代表匹配任意方法的第一入参为Student类型的方法
  • @args()
    @args(com.snailmann.Test); 代表匹配任意方法的第一入参为类声明标注了@Test注解的方法
    .

execution()可以实现args()的功能

目标类切点函数
函数入参说明
within()类名匹配串表示指定域的所有连接点(大到包,小到类):比如within(com.snailmann.*)表示com.snailmann包下所有类的所有连接点,比如within(com.snailmann.*Controller表示com.snailmann包下所有以Controller结尾的类的所有方法(连接点)
target()类名指定目标类类名,即匹配目标类的所有连接点,within(com.snailmann.Controller)表示Controller类以及子类,实现类的所有连接点
@within()类型注解名匹配类声明带有指定注解的类以及其子类,例如@within(com.snailmann.Hello),那么被@Hello注解修饰的类以及其子类的所有连接点则会被匹配
@target()类型注解名匹配类声明带有指定注解的类的所有连接点
  • within
    execution其实已经囊括了within的所有实现

  • within(com.snailmann.*Controller); 匹配com.snailmann包下以Controller结尾的类的任意连接点

  • within(com.snailmann..*); 匹配com.snailmann包下以及子包的所有类的任意连接点

  • within(com.snailmnn.*); 匹配com.snailmann包下的连接点,不包括子包

  • target

  • target(com.snailmann.Controller); 匹配com.snailmann包下Controller类以及其子类的任意连接点

  • @within

  • @within(com.snailmann.Test); 匹配任意类声明中带来@Test注解的类以及其子类的任意连接点

  • @target

  • @target(com.snailmann.Test); 匹配任意类生命中带有@Test注解的类的任意连接点,不包含子类和实现类

代理类切点函数
函数入参说明
this()类名代理类按类型匹配于指定类,则被代理的目标类的所有连接点都匹配该其切点
要注意的地方
  • 除了以上的切点函数外,@Aspect还提供了其他的切点函数,比如call(),initalization(),preinitialzation(),staticinitialzation(),get(),set(),handler(),adviceexecution(),withincode()等…,但这些函数在Spring中并不能使用,否则是会抛出IllegalArgumentException异常的。因为Spring AOP并不是完整的AOP实现方案。

  • 我们发现我们能用的切点函数的操作参数,即入参也就这么几种类型: 类名,方法匹配串,类名匹配串,方法型注解名,类型注解名;方法型注解名,就是修饰在方法上的注解的名称;类型注解名,就是修饰在类上的注解的名称;方法匹配串是对方法的匹配,但不单独是方法名称,还有访问修饰符,返回值,方法签名(方法名和参数);

  • 切点函数中的操作参数如有多个,可以用逗号分隔

  • args(com.snailmann.Student)execution(public * *(com.snailmann.Student))的区别就是,args仅仅判断方法参数,而execution更全面,是判断方法签名的,比如访问修饰符,返回值,方法名,参数

  • 切点函数的操作参数只要指定完整类名或接口名称的,那么肯定可以接受向上转型的子类或是实现类,比如args,target,this ; 除了execution,虽然它可以在方法参数中指定类型

  • execution()切点函数是比较全能的,也是使用的比较多的,除了注解以外的拦截。它可以实现args,within,target等切点函数的实现


函数入参中的通配符

有的函数的入参可以接受通配符,@Aspect支持3种通配符:

  • *
    匹配任意字符;但它只能匹配一个元素,即可匹配一个任意字符元素
  • ..
    匹配任意字符;可以匹配多个元素,即可匹配多个的任意字符元素(不清楚能否是0个元素,还是至少1个以上);但在表示类时,必须和*联合使用,而在表示入参时则可以单独使用
  • +
    表示按类型匹配指定类的所有类,必须跟在类名后面。如com.snailmann.Car+。表示Car类本身以及继承或实现于其的扩展类(子类,实现类等)

@Aspect函数按其是否支持通配符以及支持的程度,可以分为以下3类:

  • 仅支持+通配符的:args(), this(), target()
    例如args(com.snailmann.Car+), target(com.snailmann.Car+), 虽然他们都支持,但是意义不大,因为效果都是一样的,比如args(com.snailmann.Car)与args(com.snailmann.Car+)都是一样的

  • 不支持通配符: @args,@within,@target,@annotation
    因为他们都是指定注解名称,不允许有模糊之处,也就是只能指定一个全限定名称的注解名

  • 支持任意通配符的注解:execution() , within()
    因为他们的入参都是匹配串,一个是方法匹配串,另一个是类名匹配串,都是存在模拟地带的


逻辑运算符

我们知道切点表达式由切点函数组成,即多个切点函数之间可以相互协作,组成复合切点;而多个切点函数可以通过逻辑运算符组合在一起。

Spring 支持3中逻辑运算符:

  • &&
    与操作符;相当于切点的交集运算,如果有两个切点函数,使用与操作符,可以达到即满足A切点函数也要满足B切点函数的交集切点效果;Spring 提供了and的等效字符
  • ||
    或操作符;相当于切点的并集运算;可以满足A切点函数,或满足B切点函数,or为等效字符

  • 非操作符;相当于切点的反集运算;即取反,例如只要不满足A切点函数即可,not为等效字符

例子:

@Around(value = "execution(public * *(com.snailmann.spring.aop.entity.Student)) && args(com.snailmann.spring.aop.entity.Student)")

增强注解类型
  • @Before

前置增强,相当于以前的BeforeAdvice;有两个成员:

  • value: 该成员用于定义切点
  • argNames: 由于无法通过Java反射机制获取方法入参名,所以如果在Java编译时未启动调试信息,或者需要在运行时解析切点,就必须通过这个成员指定注解所标注的增强的方法的参数名称,多个参数用逗号分隔
  • @AfterReturning

后置增强,相当于以前的AfterReturningAdvice;有四个成员:

  • value: 定义切点
  • pointcut: 表示切点的信息,作用于value相同,如果指定了poincut,那么会覆盖value中的切点定义。
  • returning:将目标对象方法的返回值绑定给增强的方法
  • argNames:
  • @Around

环绕增强,相当于MethodInterceptor; 它比较全能,也是使用的比较多的增强类型;它有两个成员:

  • value: 定义切点
  • argNames:
  • @AfterThrowing

抛出异常增强,相对于ThrowsAdvice;有四个成员

  • value: 定义切点
  • pointcut: 表示切点的信息,作用于value相同,如果指定了poincut,那么会覆盖value中的切点定义。
  • throwing:将抛出的异常绑定在增强方法上
  • argNames:
  • @After

final增强,无论是抛出异常还是正常退出,该增强都会被执行;该增强没有对象的增强接口,可以看做是ThrowsAdivce和AfterReturningAdvice的混合物。一般用于释放资源,相当于try{}finally{}的控制流;有两个成员:

  • value: 定义切点
  • argNames:
  • @DeclareParents

引介增强,相当于IntroductionIntercetor;有两个成员:

  • value: 定义切点
  • argNames:

基于@Aspect的AOP代码实现


声明一个切面以及切点
/**
 * 使用@Aspect注解声明切面
 */
@Aspect
public class HelloAspect {

    /**
     * 用@Pointcut注解声明一个切点
     */
    @Pointcut("within(com.snailmann.spring.aop..*)")
    public void doBefore() {

    }


}


定义符合切点
	/**
     * 切点1,匹配com.snailmann.spring.aop下的任意类
     */
    @Pointcut("execution(* *(com.snailmann.spring.aop.*))")
    public void pointCutA(){ }

    /**
     * 切点2,匹配任意set开头的方法
     */
    @Pointcut("execution(* set*(..))")
    public void pointCutB(){}

    /**
     * 复合切点,匹配com.snailmann.spring.aop包下的任意类的set开头的方法
     */
    @Pointcut("pointCutA() && pointCutB()")
    public void pointCutC(){}

	/**
     * 拦截匹配com.snailmann.spring.aop包下的任意类的set开头的方法
     * @param point
     * @return
     * @throws Throwable
     */
    @Around("pointCutC()")
    public Object setPointCutStudent(ProceedingJoinPoint point) throws Throwable {
        ...
        Object object = point.proceed();
        return object;
    }


访问连接点信息

AspectJ使用org.aspectj.lang.JoinPoint接口表示目标类连接点对象。如果是环绕增强,则使用org.aspectj.lang.ProceedingJoinPoint表示连接点对象,该类是JoinPoint接口的子接口。任何增强都可以通过将第一入参声明为JointPoint接口作为访问连接点上下文信息的入口

JoinPoint:

  • getArgs()
    获取连接点方法运行时入参列表
  • getSignature()
    获取连接点的方法签名对象
  • getTarget()
    获取连接点所在的目标对象
  • getThis()
    获取代理对象的本身
    .

ProceedingJoinPoint:

  • proceed()
    通过反射执行目标对象连接点处的方法
    proceed(args)
    通过反射执行目标对象连接点的方法,可传入参数,只不过可以用新参数代理原参数

绑定连接点方法XXX到增强方法中

args(), this(), target(), @args(), @within(), @target()和@annotation注解除了可以指定类名外,可以还可以指定参数名,将目标对象连接点的方法入参绑定到增强方法中;比如

绑定连接点方法参数到增强方法上

args()可以绑定连接点方法的入参
@annotation可以绑定连接点方法的注解对象
@args可以绑定连接点方法入参的注解

 /**
     * 拦截匹配com.snailmann.spring.aop包下的任意类的set开头的方法
     * @param point
     * @return
     * @throws Throwable
     */
    @Around("execution(* set*(..)) && args(student,string)")
    public Object setPointCutStudent(ProceedingJoinPoint point,Student student, String string) throws Throwable {
        System.out.println(student+ " " + string);
        ...
        Object object = point.proceed();
        return object;
    }
  • 通过&& args(student,string)的方式将拦截方法的入参绑定到增强方法上的参数来
绑定连接点方法所在对象或是代理对象到增强方法上
  	@Before("execution(* set*(..)) && this(helloHandler)")
    public void setStudent(JoinPoint joinPoint, HelloHandler helloHandler){
        System.out.println(helloHandler.getClass());
    }

	 @Before("execution(* set*(..)) && target(helloHandler)")
    public void setStudent(JoinPoint joinPoint, HelloHandler helloHandler){
        System.out.println(helloHandler.getClass());
    }
  • this获得的是生成的代理对象,target获得的是目标原始对象
其他绑定
  • 绑定类注解对象 | @within , @target
  • 绑定返回值 | returning属性
  • 绑定抛出的异常 | throwing属性

相关问题


Spring AOP和AspectJ之间的关系和区别

我想很多人看到Spring AOP的相关文档时,甚至是书籍时,都是提及AspectJ。有时候的确很懵逼,都搞不清他们是谁?他们之间的关系是什么?一些资料也仅仅是提及有这个东西,然后就没有然后了。的确,有时候这让人非常的困惑和恼火。而且在我以前的认知中,我的第一反应也是非常懵逼的,以为AspectJ是Spring AOP的一部分,或者说是Spring AOP的实际实现。然而…emmm 。现实总是打脸的,所以我在这里就好好的说说他们的联系和区别

什么是Spring AOP?什么是AspectJ?

我文章的AOP的实现者章节已经做了部分的介绍,从而得知,这肯定是两个不一样的家伙。我们知道Spring AOP和AspectJ都是Java界非常流行的AOP实现框架。

那Spring AOP和AspectJ有什么不一样呢?

首先,目标不一样,Spring AOP仅仅是想在配合IOC容器的更好的工作的基础上,通过OOP的形式实现部分AOP的功能,以解决企业开发中出现的问题。它并不是一种完整的AOP机制。它也仅仅只能应用在Spring Bean上。

而AspectJ一开始的目的就是为了提供一套完整的AOP解决方案,它比Spring AOP更加的完整,能对应用在所有域的对象。但是也更加的复杂,甚至需要专门的编译器(AJC)和类加载器的配合

其次,他们的织入原理不同,AspectJ使用了三种不同的织入方式(通常称为静态织入)

  1. 编译时织入(Compile-time weaving):
  2. 后编译织入(Post-compile weaving):
  3. 加载时织入(Load-time weaving)

而Spring AOP只用了一种方式(通常称为动态织入)

  • 运行时织入(Runtime weaving)
    底层原理使用的是动态代理的方式:Jdk动态代理或者Cglib动态代理
那Spring AOP和AspectJ既然这么不同,为什么他们有联系呢?

从文章头部的前提概要的介绍中,我们可以知道Spring支持的AOP有四种实现方式:

  • 基于代理的经典Spring AOP
  • 纯POJO切面
  • @AspectJ注解驱动的切面
  • 注入式AspectJ切面

前三者属于Spring AOP的范畴, 注入式AspectJ切面是真正的AspectJ AOP实现。他们的关系主体体现在第三种形式上:@AspectJ注解驱动的切面

我们都知道Spring 是一个不断进步的框架,所以Spring AOP肯定也在不断的演进中,从经典的Spring AOP实现到基于XML的纯POJO切面,再到像AspectJ靠拢,套用AspectJ壳子的注解驱动方式,Spring可谓是不断吸收优秀的开源方案去完整自己啊。

所以@AspectJ注解驱动的切面的底层原理依然是Spring AOP的原理,基于动态代理实现。但是表面的语法糖是完全套用AspectJ的优良设计。即@AspectJ注解驱动的切面的编程方式完全跟AspectJ几乎一致,俨然就是一个Spring AOP核心,AspectJ外套的AspectJ切面编程。这就是Spring AOP和AspectJ的最大联系。所以每次讨论到Spring AOP时,总会时不时的扯到AspectJ。

除此之外,他们就没有太多的必要关联了。


更多相关比较可以查看这两篇外文:
(虽然不想承认,但是老外的文档质量的确比国内高…很多)


Filter、Intercepter、Spring AOP、ControllerAdvice的介绍和区别

在Spring Web项目的开发中,相信我们经常有一些拦截或过滤的需求,而在Spring中去实现它们通常有这么几个东西:Filter、Intercepter、Spring AOP、ControllerAdvice

那么它们有什么区别吗?
  • Filter
    1.具有框架无关性,只依赖Servlet容器,通常称为过滤器
    2.Filter是基于回调机制实现的,使用了责任链设计模式,将所有的Filter可以联成一条链条
    3.作用于Web层的拦截,可以拦截几乎所有的请求,包括静态资源
    4.Filter可以拿到Request和Reponse,但拿不到执行的方法和参数
    5.Filter只能在Servet前后起作用
  • Intercepter
    1.属于Spring MVC框架的拦截器,依赖Spring框架
    2.拦截器是基于Java反射机制实现的,比如说动态代理就是一个拦截器的基本实现
    3.作用于Web层的方法拦截,比如Controller层,也可以拦截静动态资源。Spring 4.x默认不拦截静态资源,5.x之后默认会拦截静态资源
    4.Interceptor拿的到Request和Reponse以及执行的方法,但拿不到方法的参数
    5.相比Filter,Interceptor可以深入到方法前后,抛出异常后,所以更具有弹性,在Spring框架中,优先使用Interceptor
  • Spring AOP
    1.属于AOP面向切面的范畴,是Spring面向切面编程的体现,可以实现过滤器/拦截器的功能
    2.Spring AOP更加的强大和复杂,包括方法,方法参数,Request,Reponse都可以拿到
    3.相对Filter和Interceptor而言,Spring AOP的拦截目标不局限于Web层,它几乎谁都可以拦截。并且他也不是针对URL进行拦截的,准确的说他不是拦截器或过滤器
  • ControllerAdvice
    1.ControllerAdvice属于Controller的增强,通常用于和ExceptionHandler一起用来做全局异常,算是拦截器的一种变种

The difference between the filter and the interceptor

如果它们都存在的情况下,它们之间的调用执行顺序:

入请求:
Filter -> Interceptor -> ControllerAdvice -> Aspect
出请求:
Aspect -> ControllerAdvice -> Interceptor -> Filter

原理嘛,就像是一个栈,先进后出的顺序结构

小小小小结
  • Filter不具有框架依赖,可以拦截静动态资源,但有局限,无法获知拦截的方法以及传入的参数,所以可以作为Web层的第一层预处理
  • Interceptor是Spring MVC提供的框架级拦截器,主要也是作用于Web层,与Filter类型,在Filter之后拦截,也可拦截静动态资源,同时可以获知拦截的方法信息,但不知道方法传入的参数。所以可以作为Filter之后的Web层处理
  • Spring AOP是Spring面向切面编程的实现方案,它比Filter和Interceptor更为的复杂和强大,作用不局限Web层,所以它不仅仅可以完成Filter和Interceptor可以完成的Web层拦截外,它还能提供非Web层的各种拦截,比如根据注解拦截等。这里如果比较Web层的话,Spring AOP相比Interceptor和Filter,不仅仅可以获得方法信息,还能获得方法传入的参数。

要注意的是:

  • 经过我自己的测试,的确在Spring Boot 1.x(Spring 4.x)拦截器默认是不会拦截静态资源的。但从Spring Boot 2.x(Spring 5.x)开始,拦截器默认是会拦截静态资源的。所以从Spring Boot 2.x开始,使用拦截器,记得如果不想拦截静态资源,记得关闭掉
  • Spring AOP的切面要获得request和response,可以通过Spring自动注入或者获取方法参数获得

Sping Boot 1.x的Interceptor默认是不会拦截静态资源的。
spring boot 2.x静态资源会被HandlerInterceptor拦截的原因和解决方法 - @作者:Mr—D


Jdk动态代理和Cglib动态代理自定义实现
JDK动态代理代码实现
目标类的接口|GameConpany 、ToolCompany
/**
 * 游戏公司接口
 */
public interface GameConpany {

    /**
     * 卖游戏
     */
    void sellGame();


}

/**
 * 卖工具公司接口
 */
public interface ToolCompany {

    /**
     * 卖工具
     */
    void sellTools();

}

接口实现类|SteamCompany
/**
 * 大名鼎鼎的游戏以及工具发售平台Steam
 * 实现了游戏公司和工具公司的接口
 *
 */
public class SteamCompany implements GameConpany,ToolCompany{

    /**
     * 卖游戏,final方法,不可继承
     */
    @Override
    public final void sellGame() {
        System.out.println("sell game");
    }

    /**
     * 卖工具
     */
    @Override
    public void sellTools() {
        System.out.println("sell tools");
    }
}

ProxyCompany | InvocationHandler实现类

/**
 * JDK代理的InvocationHandler的实现类,类似AOP的切面
 */
public class ProxyCompany implements InvocationHandler {

    Object target;

    public Object createProxyCompany(Object target){
        this.target = target;
        return Proxy.newProxyInstance(this.getClass().getClassLoader(),SteamCompany.class.getInterfaces(),this);
    }

    /**
     * 类似AOP的Around
     * 只需要method和args即可
     *
     * @param proxy
     * @param method
     * @param args
     * @return
     * @throws Throwable
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        System.out.println("advertise...");
        Object obj = method.invoke(this.target,args);
        System.out.println("sell out...");

        return obj ;
    }
}

Main类|main方法测试
public class Main {

    public static void main(String[] args) {
		//1. 先创建JDK代理类  
        ProxyCompany proxyCompany = new ProxyCompany();
		//2. 通过JDK代理类,通过JDK代理类,创建代理类对象,因为被代理类实现了两个接口,所以可以有两种代理方式

        GameConpany gameConpany = (GameConpany) proxyCompany.createProxyCompany(new SteamCompany());
        ToolCompany toolCompany = (ToolCompany) proxyCompany.createProxyCompany(new SteamCompany());

		//3. 游戏公司卖游戏,工具公司卖工具
        gameConpany.sellGame();
        toolCompany.sellTools();
        

    }

}
结果
advertise...
sell game
sell out...
advertise...
sell tools
sell out...

CGLIG动态代理代码实现
FoodStop类|食品店

/**
 * 食品店
 */
public class FoodStop {


    /**
     * 卖食物,final类,不可被继承
     */
    public final void sellFood(){
        System.out.println("selling food");
    }

    /**
     * 卖果汁
     */
    public void sellJuice(){
        System.out.println("selling juice");
    }

    /**
     * 卖某物
     * @param something
     */
    public void sellSomething(String something){
    }


}

ProxyFoodStop|CGLIB MethodInterceptro接口实现类

/**
 * Cglib代理的MethodInterceptor实现类,定义类似AOP的切面
 */
public class ProxyFoodStop implements MethodInterceptor {

    Object target;


    /**
     * 创建FoodStop的代理对象
     * 依赖目标对象,Enhancer对象和Cglib的MethodInterceptor实现类对象
     *
     * @param target 目标对象
     * @return       cglib动态生成的代理对象
     */
    public Object createProxyFoodStop(Object target){
        this.target = target;
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(target.getClass());
        enhancer.setCallback(this);
        return enhancer.create();
    }

    /**
     * 拦截的方法,类似AOP的Around
     * 只需要method和args即可实现动态代理
     *
     * @param o
     * @param method       需要代理的方法
     * @param args         代理方法初入的参数
     * @param methodProxy
     * @return
     * @throws Throwable
     */
    @Override
    public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {

        if ("sellSomething".equals(method.getName())){
            System.out.println("selling " + args[0]);
            Object obj = method.invoke(this.target,args);
            return obj;
        }

        System.out.println("advertise...");
        Object obj = method.invoke(this.target,args);
        System.out.println("sell out...");

        return obj;
    }
}


Main类|main方法测试
public class Main {

    public static void main(String[] args) {
    	//1. 先创建CGLIB代理类
        ProxyFoodStop proxyFoodStop = new ProxyFoodStop();
        //2. 通过CGLIB代理类创建代理对象
        FoodStop foodStop = (FoodStop) proxyFoodStop.createProxyFoodStop(new FoodStop());
        //3. 执行代理对象的方法
        foodStop.sellSomething("apple");
        foodStop.sellFood();
        foodStop.sellJuice();
    }
}

结果
selling apple
selling food
advertise...
selling juice
sell out...
要注意的地方
  • CGLIG动态代理的原理是基于继承实现的,所以目标类如果是final类或者具有final方法,那么被final所修饰的类或方法是不会被CGLIB所代理的,但JDK动态代理则不会有对应的问题,因为JDK动态代理是基于接口代理的,接口方法或是接口都是不允许被final所修饰的

  • 另外,关于目标类的静态方法,无论是CGLIB还是JDK动态代理,都是无法去拦截代理的。因为静态方法是属于类的方法,不是对象层面的问题。而我们的动态代理通常都是拿父类或是接口变量去指向代理生成的代理对象。而静态方法的调用是静态多分派的,是看静态类型的,而非实际类型的。所以当我们向上转型,得到的对象去调用静态方法,调用的也是父类的静态方法,而不是代理对象的静态方法。总之静态方法与动态代理没有任何关系。


参考资料


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值