您的位置:首页 > 编程语言 > Java开发

Spring AOP文档解读和基础应用+源码实现流程及原理

2020-07-12 17:24 681 查看

本篇文章将依托Spring AOP官方文档,对AOP说明进行解读以及遵循demo对AOP进行初识,文章结尾另外通过一篇文章在Spring源码项目上调试研究AOP的底层实现流程和原理。学习AOP之前建议先大致了解一下Spring的容器,Bean,BeanDefinition,BeanFactory工厂,BeanPostprocessor及BeanFactoryPostprocessor等各个后置处理器实现类,Bean整个初始化的生命周期以及循环引用和CGLIB代理的知识,对于小编这类从没深入学习spring源码的小白,这些知识点则需要通过spring源码编译后逐个学习个基础周期大概在一个月左右,有相关了解后有助于深入理解AOP的源码实现。本文官网文档在https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#aop,对于spring源码编译和spring基础应用可参考小编spring学习专栏的其它文章。好了开始进入正题。

什么是Spring AOP?

Spring AOP是提供另一种思考程序结构的方式,面向切面编程(AOP)补充了面向对象编程(OOP)。OOP中模块性的关键单元是类,而在AOP中模块性的单元是切面。切面支持关注的模块化(例如事务管理,日志管理,权限管理),它跨越多个类型和对象。(在AOP文献中,这种关注经常被称为“横切”关注。)

Spring的关键组件之一是AOP框架。虽然Spring IoC容器不依赖于AOP(这意味着如果不想使用AOP,就不需要使用),但是AOP补充了Spring IoC,提供了一个非常强大的中间件解决方案。

Spring通过使用基于模式的方法或@AspectJ注释样式提供了编写定制方面的简单而强大的方法。这两种样式都提供了完全类型化的建议和AspectJ切入点语言的使用,同时仍然使用Spring AOP进行编织。

本章讨论了基于schema和@ aspectj的AOP支持。

Spring AOP的应用?

(1)日志管理

(2)权限管理

(3)事务管理

(4)监控管理

常见于以上几种应用场景,当然也有其它的场景可以使用。

Spring AOP的组成?

首先,让我们先了解关于AOP的一些术语以及含义,这些是构成AOP的重要组成。

(1)Aspect切面: 跨多个类的关注的模块化,可以理解为一个关注点,比如事务管理是企业Java应用程序中横切关注点的一个很好的例子。在Spring AOP中,切面是通过使用常规类(基于模式的方法)或使用@Aspect注释的常规类(@AspectJ样式)来实现的,简而言之就是可以通过一个类声明为一个切面,通过XML或注解的方式配置成一个可被Spring识别的切面关注点,切面关注点是一个整体包含后面的各个组成,是AOP的基本单元。

(2)Join point连接点:程序执行的某个点,这个点是AOP代理对象或类中经过增强了的某个方法。在Spring AOP中,连接点总是表示方法执行,因为方法时Spring AOP的最小粒度。

(3)Advice通知:指在特定连接点上采取的操作逻辑(执行的方法),同时通知还有一个执行时机,可以通过不同时机类型设置(例如方法前执行,方法后执行等)来决定操作逻辑在什么时机执行,通知类型将在后面讨论。许多AOP框架,包括Spring,将通知建模为拦截器,并维护围绕连接点的拦截器链。

(4)Pointcut切点:切点是连接点匹配出来,并且可能经过特殊类型过滤的连接点结果集。切点可以配合表达式在所有连接点中进行匹配,匹配出来的特定类型的连接点集合就是切点。在切点上,还可以将通知切点表达式一起组合使用,并在与切入点匹配的任何连接点上运行这个通知。连接点由切入点表达式匹配的概念是AOP的核心,Spring默认使用AspectJ切入点表达式语言。

(5)Introduction引介:引介是一种特殊的增强,它可以为其他类添加一些属性与方法,还可以为一个业务类实现某个接口而不需要改动原来的业务类。Spring AOP允许向任何被通知的对象引入新的接口(和相应的实现)。例如,您可以使用一个介绍来让一个bean实现一个IsModified接口,以简化缓存。(在AspectJ社区中,介绍称为类型间声明。)

(6)Target object目标对象:被一个或多个切面通知的对象,也称为“被通知对象”。因为Spring AOP是通过使用运行时代理如CGLIB来实现的,所以这个对象总是一个代理对象。

(7)AOP Proxy代理对象:由AOP框架创建的一个对象,用于实现切面契约(通知方法执行等等)。在Spring框架中,AOP代理是JDK动态代理或CGLIB代理,根据源码可知当代理接口时用哪个JDK动态代理,当对一个没有实现接口的类代理时则使用CGLIB。

(8)Weaving织入:将通知中的逻辑和执行时机,体现到切点方法上的过程就是织入过程。例如将一个通知中的打印逻辑L以及切点执行前时机T,在匹配到的A切点方法上执行的过程,代码运行过程就是在切点A被调用时但是还没有执行A内部方法前,会触发时机T,然后执行逻辑L。

上面就是一个Spring AOP的相关术语,相对比较抽象后面会通过代码来理解。

对于通知的类型,Spring AOP包括以下类型的通知:

1)Before advice:在连接点之前运行的通知,但不能阻止执行流继续执行到连接点(除非抛出异常)。

2)After returning advice:在连接点正常完成后运行的通知(例如,如果一个方法没有抛出异常返回)。

3)After throwing advice:如果一个方法通过抛出异常退出,则执行通知。

4)After (finally) advice:不管连接点以何种方式存在(正常或异常返回),都要执行的通知,在finally执行完成之后执行该通知。

5)Around advice:围绕连接点(如方法调用)的通知,这是最常用的通知类型。Around通知可以在方法调用之前和之后执行自定义行为。它还负责选择是继续执行到连接点,还是通过返回它自己的返回值或抛出异常来缩短通知的方法执行的时间,也就是说这种类型的通知可以改变连接点的执行过程。

Spring AOP的功能和目标?

Spring AOP是在纯Java中实现的。不需要特殊的编译过程。Spring AOP不需要控制类加载器的层次结构,因此适合在servlet容器或应用服务器中使用。

Spring AOP目前只支持方法执行连接点(建议在Spring bean上执行方法)。虽然可以在不破坏核心Spring AOP api的情况下添加对字段拦截的支持,但字段拦截还没有实现。如果需要通知字段访问和更新连接点,可以考虑AspectJ这样的语言。

Spring AOP的AOP方法与大多数其他AOP框架不同。目标不是提供最完整的AOP实现(尽管Spring AOP很有能力)。相反,其目的是在AOP实现和Spring IoC之间提供一个紧密的集成,以帮助解决企业应用程序中的常见问题。

因此,例如,Spring框架的AOP功能通常与Spring IoC容器一起使用。方面是通过使用普通的bean定义语法配置的(尽管这允许强大的“自动代理”功能)。这是与其他AOP实现的一个重要区别。在Spring AOP中,您无法轻松或有效地完成某些事情,比如通知非常细粒度的对象(通常是域对象)。AspectJ是这种情况下的最佳选择。然而,我们的经验是,Spring AOP为企业Java应用程序中大多数适合于AOP的问题提供了一个优秀的解决方案。

Spring AOP从未试图与AspectJ竞争来提供全面的AOP解决方案。我们认为,基于代理的框架(如Spring AOP)和成熟的框架(如AspectJ)都是有价值的,它们是互补的,而不是竞争的。Spring无缝地将Spring AOP和IoC与AspectJ集成在一起,以支持在一致的基于Spring的应用程序体系结构中使用AOP。这种集成不影响Spring AOP API或AOP Alliance API。Spring AOP保持向后兼容。也就是说,Spring AOP是与AspectJ进行互补的关系,而非竞争关系,Spring AOP的实现参考了AspectJ的语法风格,使得使用起来更贴合开发者的习惯。

Spring AOP代理?

Spring AOP默认为AOP代理使用标准JDK动态代理。这允许代理任何接口(或一组接口)。

Spring AOP还可以使用CGLIB代理。这对于代理类而不是接口是必要的。默认情况下,如果业务对象没有实现接口,则使用CGLIB。由于对接口而不是类进行编程是一种良好的实践,所以业务类通常实现一个或多个业务接口。强制使用CGLIB是可能的,在这种情况下(希望这种情况很少发生),您需要建议在接口上没有声明的方法,或者需要将经过代理的对象作为具体类型传递给方法。理解Spring AOP是基于代理的这一事实是很重要的。请参阅理解AOP代理,以全面了解这个实现细节的实际含义。

Spring AOP和AspectJ?

@AspectJ指的是将方面声明为用注释注释的常规Java类的样式。@AspectJ样式是由AspectJ项目作为AspectJ 5发行版的一部分引入的。Spring使用AspectJ提供的用于切入点解析和匹配的库来解释与AspectJ 5相同的注释。但是AOP运行时仍然是纯Spring AOP,并且不依赖于AspectJ编译器或编织器。使用AspectJ编译器和编织器可以使用完整的AspectJ语言,两者共同点是都是代理技术来实现AOP这种编程标准,差异是Spring AOP是运行期产生代理对象,AspectJ是编译期就产生了代理类。

如何声明一个切面?

Spring AOP参考了AspectJ的语法风格,那么就要引入AspectJ语法风格的支持,支持配置有XML和注解两种方式:

XML中通过<aop:aspectj-autoproxy/>来支持;

注解方式:在java配置类上,添加@EnableAspectJAutoProxy注解来支持。

然后新建一个类,可以分别通过XML或注解将这个类声明为一个切面。为了对AOP的使用有更进一步了解,我们分别就XML和注解两种方式,用同一个demo来进行展示。

(1)XML方式:https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#aop-schema

当您使用模式支持时,切面就是在Spring应用程序上下文中定义为bean的常规Java对象。状态和行为在对象的字段和方法中捕获,切入点和通知信息在XML中捕获。您可以使用元素来声明一个切面,并使用ref属性来引用支持bean,如下面的例子所示:

使用前,需要引入以下两个jar,否则会报找不到相关aop注解的错误:

[code]compile 'org.aspectj:aspectjrt:1.8.0'
compile 'org.aspectj:aspectjweaver:1.8.0'

接下来通过代码简单演示一下这个过程:

1)首先声明两个bean,其中ABean作为切面Bean,BBean作为业务Bean:

[code]<bean id="aBean" class="com.springtest.test.service.aoptest.ABean"></bean>
<bean id="bBean" class="com.springtest.test.service.aoptest.BBean"></bean>

2)ABean提供切面的相关连接点的通知方法:

[code]package com.springtest.test.service.aoptest;

public class ABean {

//Before advice通知方法,用于织入到切点方法执行前
public void before() {
System.out.println("ABean before");
}

//After returning advice,用于织入到切点方法执行返回后执行
//after的参数就是切点执行return后的返回值
public void after(Object retVal) {
System.out.println("ABean after.. get=" + retVal);
}

}

BBean则提供正常的业务方法:

[code]package com.springtest.test.service.aoptest;

public class BBean {

//业务方法sayHi
public void sayHi(){
System.out.println("BBean say hi");
}

//业务方法getName,带参数和返回值
public String getName(String name){
System.out.println("BBean getName, return name="+name);
return name;
}

}

 3)有了上面的类之后,我们要先明确目的。我们的目的是将ABean作为切面类,BBean作为业务类,当我们调用BBean相关方法时,在不同的执行时机织入ABean切面的相关通知方法。本例子我们将ABean的before通知织入到BBean的所有方法执行前,将ABean的after通知织入到BBean的getName方法执行完成后并在切面获取这个getName返回值。

明确这个目的后,我们需要有一个spring.xml配置文件,在spring.xml中声明<aop:config>的配置:

[code]<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">

<!-- 开启AspectJ支持 -->
<aop:aspectj-autoproxy/>

<aop:config>

<!-- 声明出一个切面,这个切面对应类aBean -->
<aop:aspect id="myAspect" ref="aBean">
<!-- 声明pointcut切点,
切点匹配规则是com.springtest.test.service.aoptest包下所有类的所有带不同参数的方法,
那么这里就会匹配出bBean类所有的方法,expression有很多种类型,这些通过expression匹配出的就是切点,
名字可以命名为bPointcut
-->
<aop:pointcut id="bPointcut"
expression="execution(* com.springtest.test.service.aoptest.*.*(..))"/>

<!-- 声明一个执行前通知,通知方法是monitor方法,织入时机是在bPointcut对应的各个连接点执行前,
也可以通过pointcut="execution(* com.xyz.myapp.dao.*.*(..))"来取代pointcut-ref匹配需要的切点
-->
<aop:before pointcut-ref="bPointcut" method="before"/>
<aop:after-returning pointcut="execution(* com.springtest.test.service.aoptest.*.getName(..))"
returning="retVal" method="after"/>

</aop:aspect>

</aop:config>

<bean id="aBean" class="com.springtest.test.service.aoptest.ABean"></bean>
<bean id="bBean" class="com.springtest.test.service.aoptest.BBean"></bean>

</beans>

引入aop相关的命名空间,然后开启AspectJ支持

xmlns:aop="http://www.springframework.org/schema/aop"
http://www.springframework.org/schema/aop

http://www.springframework.org/schema/aop/spring-aop-4.3.xsd

 <aop:aspectj-autoproxy/>:开启AspectJ支持;

在<aop:config>中声明切面,同时开启CGLIB代理模式,也就是说所有的AOP都是由CGLIB方式代理;

[code]<aop:aspect id="myAspect" ref="aBean"> 这里通过ref引入一个bean,这个bean就是切面bean;
<aop:pointcut id="bPointcut"
expression="execution(* com.springtest.test.service.aoptest.*.*(..))"/>
通过<aop:pointcut>声明一个切点,其中配合expression表达式来查找所有的连接点,符合条件的连接点就是切点集合,命名为bPointcut;<aop:before pointcut-ref="bPointcut" method="before"/>
<aop:after-returning pointcut="execution(* com.springtest.test.service.aoptest.*.getName(..))"
returning="retVal" method="after"/>
通过<aop:before>,<aop:after-returning>等通知(有5种)可以声明一种类型的通知,通知中可以使用pointcut-ref引入一个切点名称,这意味着这个切点名称对应的连接点集合只要被调用时,就会视执行时机去执行通知方法。如<aop:before pointcut-ref="bPointcut" method="before"/>就表示只要bPointcut对应的所有连接点任意被执行,那么执行就会触发这个aop:before通知,在执行前会优先织入ABean切面的before方法。aop:after-returning通知同理。

4)最后在main方法运行spring容器并调用BBean的相关方法,即可看到AOP的作用过程:

以上通过两个简单的例子展示了AOP的过程,当然通知类型有5种这里没有完整举例,在这里也可以看出连接点的筛选,通知类型和通知时机是非常重要的。对于连接点的筛选除了execution外,还有within,this等等其它不同用法可以自行了解。

(2)注解方式:https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#aop-ataspectj

使用步骤依照官方文档说明,

1)使用AOP之前需要引入aspectjweaver.jar,在扫描类中要加上@EnableAspectJAutoProxy开启AOP功能,

2)不像XML配置方式需要配置<bean>标签,注解方式则直接创建切面类,使用@Aspect注解即可将一个类声明为切面,

但是值得注意的是@Aspect注解相对于包扫描自动检测是不够的,还是需要配合@Component来完成bean的注入。特别注意一点就是连接点表达式可以匹配任何一个类,但是在源码设计中一个切面类是不能被切点通知作用的,也就是说切面类本身不能被AOP代理。

2)声明一个切点:

切点会确定匹配的连接点,从而使我们能够控制何时执行通知。Spring AOP只支持Spring bean的方法执行连接点,所以可以把切点看作Spring bean的特殊方法。切点中包含连接点,连接点是通过连接点表达式来匹配对应的连接点,这列匹配的连接点就会与这个切点进行绑定。

对于连接点表达式,有很多种方式,如execution,within,this,target,args,@target,@args,@within和@annotation,说明如下,用法和区别可以自行百度搜索了解,这里以常用的execution为例:

对于表达式,Spring AOP规定了一个固定的表达式模板,这个模板就是切点表达式的规定范式,其中?是可选:

[code] execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern)
throws-pattern?)

[code]modifiers-pattern? :方法修饰符,public private等可选,默认所有;
ret-type-pattern :返回类型,可以用*表示所有;
declaring-type-pattern?:声明类型,如哪个包路径下,可选;
name-pattern(param-pattern):方法名字(参数名字),可选
throws-pattern?:是否抛异常,可选。

如下面例子等等表达式:

execution(public * *(..))

execution(* com.xyz.service.AccountService.*(..))
execution(* com.xyz.service..*.*(..))

3)声明一个通知:

通知也可以理解为Bean的一个特殊方法,只不过这个通知会与切点表达式相关联,并在切点匹配的方法执行之前、之后或前后运行。切点表达式可以是对指定切点的简单引用,也可以是切点表达式来匹配通知需要作用到哪些切点上。

通知要关联的切点表达式写法如上规范,也可以用execution等匹配,如@Before("execution(* com.xyz.myapp.dao.*.*(..))")。

通知类型也有多种:

(a)Before Advice:通过@Before(切点表达式或切点引用),在匹配的切点执行前触发的通知;

(b)After Returning Advice:通过@afterreturned(切点表达式或切点引用),在匹配的切点执行结束后触发的通知;

(c)After Throwing Advice:通过@AfterThrowing(切点表达式或切点引用),当一个匹配的方法执行通过抛出异常退出时,运行通知。

(d)After (Finally) Advice:通过@After(切点表达式或切点引用),在匹配的方法执行finally退出时运行。

(e)Around Advice:通过@Around(切点表达式或切点引用),它有机会在方法执行之前和之后执行工作,并确定何时、如何执行,甚至是否真正执行方法。

下面是结合开启AOP功能的配置类,业务类和业务方法,切面,切点,通知的示例,如下:

配置类:

业务类和业务方法:

切面:

切点:切面的特殊方法,这里以sayHi和getName方法作为连接点,匹配出sayHiPointcut和getNamePointcut切点为例:

通知:分别给这两个切点添加通知,

(a)添加@Before通知,这个通知作用于所有sayHiPointcut切点,在切点执行前触发:

执行结果如下:

(b)添加@afterMethod通知,作用于getNamePointcut切点,切点执行结束后触发:

执行结果如下:

其它通知类型不再做演示,可自行测试。

4)目标对象和代理对象:

在本例中,目标对象可以理解为提供业务方法的BBean类,

由于这个BBean类没有实现其它接口,因此默认会使用CGLIB的代理方式进行代理,代理后context2.getBean的到就是一个CGLIB的代理对象:

在这里还要留意一个知识点,就是上面关于连接点表达式的几个特殊表达式,分别是@target,@args,@within,@annotation,这几个分别由execution,target,this,args,within对应。

@target:将匹配限制为连接点(使用Spring AOP时方法的执行),其中执行对象的类具有给定类型的注释。

@args:将匹配限制为连接点(使用Spring AOP时方法的执行),其中实际传递的参数的运行时类型具有给定类型的注释。

@within:将匹配限制为具有给定注释的类型中的连接点(在使用Spring AOP时,使用给定注释在类型中声明的方法的执行)。

@annotation:将匹配限制为连接点的主题(在Spring AOP中执行的方法)具有给定注释的连接点。

我们以@winthin为例,这个连接点表达式的意思是可以声明为实现了XXX注解类型的连接点,当这些连接点的指定方法被执行时

触发切点通知,如下例子,创建一个自定义注解@Yang,新建一个YangBean类引入@Yang注解,提供sayHi方法,在切面类中就可以对这个引入了@Yang注解的类型进行连接点声明:

切面类中,声明连接点方法以及before通知方法(以before通知为例):

从上面可以看出,在切点方法中,使用了@within表达式来声明关联所有实现了这个@Yang注解的连接点,最终执行结果就是

当通过context2.getBean(YangBean.class).sayHi(); 时,会先后触发通知和业务方法,并且这个getBean得到的对象就是YangBean的CGLIB代理对象。

自此,关于AOP的XML方式和注解方式声明及基本应用到这里了,还有很多使用细节没有提及需要工作中学习使用,下面将通过下一篇文档从Spring源码的角度研究学习AOP的工作原理和流程https://blog.csdn.net/qq_20395245/article/details/106109331

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: