本文依靠Spring官方文档对AOP和Spring AOP的介绍,再加上自己的理解而展开。
一,对AOP的理解
AOP(Aspect-oriented Programming,面向切片编程);通过于预编译方式和运行期动态代理实现不修改源代码的情况下给程序动态统一的添加功能的一种技术。
体现在编程思想上就是,把很多类对象的横切问题点,从业务逻辑分离出来,从而达到解耦的目的,增加代码的重用性,提高开发效率。
从Spring官方文档可以看出, AOP是对OOP(面向对象编程)的一个补充,是在程序结构方面的不同于OOP的另一种思想;OOP中模块化的关键单位是类,而AOP中是aspect(切面)。
就比如程序当需要有记录日志的功能时:
如果在每个处理业务逻辑的类中都写一个记录日志的方法logMsg(),很显然会造成代码冗余、编程复杂、维护困难等问题;通过OOP的思想进行改进,如果将这种与业务逻辑的无关的代码(比如记录日志)抽取出来写在另外一个类中,但很多类对象和Logger仍存在依赖关系,每个类的所有方法都需要调用Logger,如果修改了方法,需要修改多次,仍然冗余、维护困难,仍不能很好的解决问题;所以,AOP——不修改源代码的情况下给程序动态统一的添加功能!
AOP可以应用在,日志记录、异常处理、权限验证、缓存处理、事务处理、数据持久化、效率检查、内容分发等场景中,因为这些情况都不是核心业务逻辑,从当前代码中拆分出来完全不会影响当前代码。
二,AOP术语和执行原理
(1)Join Point(连接点)
程序执行期间的一个点,比如方法的执行和异常的处理;在Srping AOP中,一个join point总代表一个方法的执行,好像不知所云,结合其他地方的理解,连接点就是一个方法,如调用写日志这个操作的函数,上图中的,class A和class B。
(2)pointcut(切点)
很多地方都说可以将pointcut理解成join point的集合;在官方文档中:advice(通知)与pointcut表达式相关联并且在pointcut匹配的任何join point上运行,它还说这是AOP的核心。
Spring默认使用AspectJ的pointcut表达式语言。
(3)advice(通知,也叫增强)
aspect(切面)在特定连接点上采取的操作,是织入(weaving)到目标类(Target object)的连接点(join point)的一段代码,会增强到目标函数中去,需要指示①增强到什么地方和②增强的内容。
Spring,将advice建模为一个拦截器,并在join point周围维护一个拦截器链。
(4)Target object(目标对象)
被一个或多个aspect增强的对象,或称advice织入的目标类。
由于Spring AOP是通过使用运行时代理实现的,因此该对象始终是一个代理对象。
(5) AOP proxy(代理对象)
一个有AOP框架创建的对象,为了实现切面;
在Spring框架中,AOP代理是JDK动态代理或CGLIB代理。 :在运行时期在对象初始化阶段织入(weaving)代码的
JDK动态代理:基于接口实现;CGLIB代理:基于类的继承实现
(6)Weaving(织入)
将增强添加到目标类的具体连接点的这个过程;
Spring AOP在运行时执行织入
(7)aspect(切面)
aspcet由pointcut和advice组成,可以理解成这一套东西合起来称作切面,切面可以包含几个类。
在Spring AOP中,切面是通过使用常规类(基于模式的方法)或使用@Aspect注释(@AspectJ风格)注释的常规类来实现的。
以上概念都不是非常直观容易理解的,并且一些概念又由其他概念解释,所以还可以从下到上在认真阅读一遍以增强理解;为了更好的理解,在此举出一个例子:
🔺图示
▲有一个任意的controller类的任意方法f(),需要对这个方法的执行记录日志。
- 在没有AOP之前,我们会在f()中的某个或某些位置(比如,如果需要在业务逻辑代码执行前去记录日志就在f()的方法体最前面添加记录日志的代码,如果需要在业务逻辑代码执行后去记录日志就在f()的方法体最后面添加记录日志的代码,如果...中...中间,等)添加几行用于写日志的代码或调用写日志的方法——注意:这里添加代码时要指定的内容包括了代码本身和要放的位置(后面就会理解用意)。但这种方式造成代码冗余、维护困难等问题,于是诞生AOP。
- 有了AOP以后,在与controller类很远的地方直接定义一个与controller类没有任何关系(关系:指依赖、继承等)的用于写日志的类Logger,在这个类中完成写日志的操作,所以,(1)在这个写日志的类Logger会有一段完成写日志操作的代码,但是我们要让controller的方法f()去记录日志又不能在controller的方法f()去调用写日志的类(如果这样,还是存在依赖),所以,(2)我们需要告知这个写日志的类Logger去让哪个包下的哪个类的哪个方法去执行这个Logger类的代码以完成写日志的操作,同时,还要(3)告知这个写日志的类Logger的写日志操作的这段代码要在controller类的方法f()中的哪个地方执行。(结合红字理解)所以Spring AOP就需要采取一系列的方式去完成这三个要求,以至于能够实现在controller类中不出现任何有关写日志的代码,却能通过写日志的Logger类让controller类的方法去记录日志;于是定义了一系列理论和术语。
▲实现不在controller类中包含记录日志的代码而通过其他一个类Logger让controller类的方法去记录日志这个操作(目标),Spring AOP的实现过程
- 有一个controller类的方法f(),它本身完成的是功能m,这个m是处理业务逻辑的代码(这个代码与记录日志没有任何关系)。
- 在Logger类中,我做了一些努力(完成了(1)(2)(3),即:实现了写日志的代码、指定了在controller类的方法f()中、在f()被调用前和调用后记录日志)——这个“努力”稍后介绍。于是,Spring AOP开始根据这三个指示完成目标。
- Spring AOP对f()方法(因为Logger类中(2)——已经指定了在哪个controller类的哪个方法)加入写日志的代码(就是(1)Logger类的写日志的代码,加入的位置由(3)确定),方法f()被修改成了f1();
这个被加入到f()中的代码本身和位置,叫做advice③,翻译成中文叫通知或增强。
这个将代码添加到指定位置(advice)的过程,叫做weaving④,织入。
方法f()所在的类,叫做Target,目标类;对应Target object①,目标对象。
- 🔺但,Spring AOP并不是在原来的controller类对象中直接修改方法f()(修改:把Logger的写日志的代码添加给f()),而是重新创建了一个对象,在这个对象中包含了包含写日志代码的f1()方法。
这个被增强(加入了Lgger类的写日志的代码)而产生的对象,叫做AOP proxy②,AOP的代理对象。
三,程序示例
//1,定义一个日志切面类
@Aspect //这是一个切面类
@Component
public class LoggingAspect {
//private Logger logger= LoggerFactory.getLogger(this.getClass());
//2,定义切点
@Pointcut(value = "execution(* com.example.demo.controller.*.* (..))")
public void myLoggerPointCut(){
}
//3,定义advice通知
@Around("myLoggerPointCut()")
public Object myLoggerAdcice(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("调用前");
Object obj = pjp.proceed();
System.out.println("调用后");
return obj;
}
}
@RestController
public class testController {
@RequestMapping("testPage")
public String testPage(){
System.out.println("testPage");
return "test";
}
}
Ⅰ,实现步骤
①引入spring-boot-stater-aop依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
@Aspect、@Pointcut等切面相关的注释在aspectj.jar包中。
②定义一个切面类Aspect
在声明类的时候,用@Component和@Aspect注释,@Component将实现类加入Spring容器中进行管理,@Aspect声明切面类。
在该程序中,定义了类LoggingAspect,并@Component和@Aspect注释。
③定义切点Pointcut
从官方文档可以看出, 1,切点确定了连接点,从而使编程人员能够控制advice运行在哪个地方;2,Spring AOP只支持Spring bean的方法执行连接点;3,切点由两部分组成:名称和任何参数的签名、确定方法执行的切点表达式;4,在@AspectJ注释风格的AOP中,切点签名由常规方法定义提供,切点表达式通过使用@Pointcut注释来指示;5,作为切点签名的方法的返回值类型必须是void类型。
在该程序中,在切面类中定义了一个方法myLoggerPointCut(组成成分1——签名),该方法用@Pointcut注解,这个方法便是一个切点,方法名myLoggerPointCut即是切点名称;在@Pointcut中输入切点表达式(组成成分2——切点表达式)(切点表达式的内容很多、形式多样,将在后面介绍)
@Pointcut(value = "execution(* com.example.demo.controller.*.* (..))")
描述了连接点,指示了修饰符、返回类型、要执行增强的包、要执行增强的类、要执行增强的方法、方法的参数。
连接点:在程序中用@Pointcut中的表达式描述,指示要增强的哪个包下的哪个类的哪个方法,这个方法就叫连接点;因为一个切点可以同时指示很多个方法,所以说切点里面包含连接点,或切点使连接点的结合。
这个方法myPointcut的方法体是空的,因为这个方法myPointcut仅仅是@Pointcut的载体,在后面定义Advice的时候通过引用切点名myPointcut便将要插入的代码和被插入的包下的类中的方法连接了起来。
④定义Advice通知
通知与切入点表达式相关联,利用通知的5种注解类型,@Before,@After,@AfterReturning,@AfterThrowing,@Around完成某些切点的增强动作同时确定是在被增强的方法之前、之后、或前后等位置执行。
Ⅱ,本代码的内涵
在这个程序中,通过@Around将方法myLoggerAdcice中的代码插入到了@Around中的参数”myLoggerPointCut()”指定的切点的函数中去并由@Around本身指定了在切点的前后执行,而myLoggerPointCut这个切点指定了要增强的方法在哪个包下的哪个类;最终实现了将Logger类的代码插入到controller的方法中去。
Ⅲ,运行结果
截至目前,已经对AOP的整个流程和浅显的原理进行了理解,我认为整个AOP的理论、概念或术语是有一点难以理解的,但在实际编程中却是相对简单的,
只需要一个简单的绑定即可;但确定要增强哪个包下的哪个类中的哪个方法的形式多种多样(切点表达式),以及指定要织入进包下的类中的方法的代码(比如写日志的代码)被织入的位置有五种方式,所以,在后面单独分两块介绍切点表达式和@Before,@After,@AfterReturning,@AfterThrowing,@Around这五种织入方式。
随着Spring框架发展至今天,介绍和讲解Spring AOP的文章、视频等已经遍地都是,本文作者依然本着自我学习之后的梳理的宗旨写下这边文章,故文章结构、逻辑、条理等方面比较随性,若读者觉得不方便阅读学习请移步其他优秀的文章,当然也希望对大家有所帮助。