Spring框架的第一大核心:IOC控制反转,Spring框架的第二大核心:AOP面向切面编程
在AOP基础这个阶段,我们首先介绍一下什么是AOP,再通过一个快速入门程序,让大家快速体验AOP程序的开发。最后再介绍AOP当中所涉及到的一些核心的概念。
什么是AOP?
AOP英文全称:Aspect Oriented Programming(面向切面编程、面向方面编程),其实说白了,面向切面编程就是面向特定的方法进行编程。
那什么又是面向方法编程呢,为什么又需要面向方法编程呢?
来我们举个例子做一个说明:
然而有一些业务功能执行效率比较低,执行耗时较长,我们需要针对于这些业务方法进行优化。 那首先第一步就需要定位出执行耗时比较长的业务方法,再针对于业务方法再来进行优化。
此时我们就需要统计当前这个项目当中每一个业务方法的执行耗时。
那么统计每一个业务方法的执行耗时该怎么实现呢?
可能多数人首先想到的就是在每一个业务方法运行之前,记录这个方法运行的开始时间。在这个方法运行完毕之后,再来记录这个方法运行的结束时间。拿结束时间减去开始时间,不就是这个方法的执行耗时吗?
以上分析的实现方式是可以解决需求问题的。
但是对于一个项目来讲,里面会包含很多的业务模块,每个业务模块又包含很多增删改查的方法,如果我们要在每一个模块下的业务方法中,添加记录开始时间、结束时间、计算执行耗时的代码,就会让程序员的工作变得非常繁琐。
而AOP面向方法编程,就可以做到在不改动这些原始方法的基础上,针对特定的方法进行功能的增强。
AOP的作用:在程序运行期间在不修改源代码的基础上对已有方法进行增强(无侵入性: 解耦)
我们要想完成统计各个业务方法执行耗时的需求,我们只需要定义一个模板方法,将记录方法执行耗时这一部分公共的逻辑代码,定义在模板方法当中,在这个方法开始运行之前,来记录这个方法运行的开始时间,在方法结束运行的时候,再来记录方法运行的结束时间,中间就来运行原始的业务方法。
而中间运行的原始业务方法,可能是其中的一个业务方法,也可能是一个业务模块当中的多个业务方法。
那面向这样的指定的一个或多个方法进行编程,我们就称之为 面向切面编程。
那此时,当我们再调用部门管理的 list 业务方法时,并不会直接执行原始业务方法的逻辑 list 方法的逻辑,而是会执行我们所定义的 模板方法 , 然后再模板方法中:
记录方法运行开始时间
运行原始的业务方法(那此时原始的业务方法,就是 list 方法)
记录方法运行结束时间,计算方法执行耗时
不论,我们运行的是哪个业务方法,最后其实运行的就是我们定义的模板方法,而在模板方法中,就完成了原始方法执行耗时的统计操作 。(那这样呢,我们就通过一个模板方法就完成了指定的一个或多个业务方法执行耗时的统计)
而大家会发现,这个流程,我们是不是似曾相识啊?
对了,就是和我们之前所学习的动态代理技术是非常类似的。 我们所说的模板方法,其实就是代理对象中所定义的方法,那代理对象中的方法以及根据对应的业务需要, 完成了对应的业务功能,当运行原始业务方法时,就会运行代理对象中的方法,从而实现统计业务方法执行耗时的操作。
其实,AOP面向切面编程和OOP面向对象编程一样,它们都仅仅是一种编程思想,而动态代理技术是这种思想最主流的实现方式。而Spring的AOP是Spring框架的高级技术,旨在管理bean对象的过程中底层使用动态代理机制,对特定的方法进行编程(功能增强)。
AOP的优势:
减少重复代码
提高开发效率
维护方便
在了解了什么是AOP后,我们下面通过一个快速入门程序,体验下AOP的开发,并掌握Spring中AOP的开发步骤。
需求:统计各个业务层方法执行耗时。
实现步骤:
导入依赖:在pom.xml中导入AOP的依赖
编写AOP程序:针对于特定方法根据业务需要进行编程
1. 由于当前是SpringBoot环境,所以我们要引入SpringBoot AOP的起步依赖:pom.xml
org.springframework.boot
spring-boot-starter-aop
2. 编写AOP程序:TimeAspect
package com.gch.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Slf4j
@Component // 当前类的实例交给Spring的IOC容器管理,成为IOC容器当中的bean对象
@Aspect // 表示当前类为AOP切面类
public class TimeAspect {
/**
* 记录方法的执行时间
* @param joinPoint ProceedingJoinPoint的形参
* @return 返回原始方法执行后的返回值
* @throws Throwable
* 通过@Around注解当中的表达式来指定当前我们所编写的AOP程序(当前共性的功能)要针对于哪些特定的方法来进行编程
* 第一个*表示返回值,任意 第二个*表示类名/接口名 第三个*表示方法名
* 代表当我们在运行com.gch.service这个包下所有的接口或者是类当中所有的方法时,都会运行这个方法当中所封装的公共的逻辑代码
*/
@Around("execution(* com.gch.service.*.*(..))") // 切入点表达式
public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {
// 1.记录开始时间
long begin = System.currentTimeMillis();
// 2.调用原始方法运行 - 借助AOP当中提供的AIP - ProceedingJoinPoint - 封装了原始方法的相关信息
// ProceedingJoinPoint的proceed()实例方法就可以来调用原始方法运行 result指的是原始方法的返回值
Object result = joinPoint.proceed();
// 3.记录结束时间.计算方法的执行耗时
long end = System.currentTimeMillis();
// 4.记录日志:ProceedingJoinPoint的getSignature()实例方法就可以拿到方法的签名
log.info(joinPoint.getSignature() + "方法执行耗时:{}ms",end - begin);
return result;
}
}
我们通过AOP入门程序完成了业务方法执行耗时的统计,那其实AOP的功能远不止于此,常见的应用场景如下:
记录系统的操作日志
完成权限的控制
完成事务管理:我们前面所学习的Spring事务管理,底层其实也是通过AOP来实现的,只要添加@Transactional注解之后,AOP程序自动会在原始方法运行前先来开启事务,在原始方法运行完毕之后提交或回滚事务
这些都是AOP应用的典型场景。
通过入门程序,我们也应该感受到了AOP面向切面编程的一些优势:
代码无侵入:在不修改原始的业务方法的前提下,就已经对原始的业务方法进行了功能的增强或者是功能的改变
减少了重复代码
提高开发效率
维护方便:因为如果说共性的逻辑我需要改变,此时我只需要去改变AOP当中的模板方法就可以了,所有的原始方法都不需要改动
1. 连接点:JoinPoint,指的是可以被AOP控制的方法,其实所有的方法都是连接点(暗含方法执行时的相关信息) 通过连接点来获取方法执行时的相关信息!
例如:入门程序当中所有的业务方法都是可以被aop控制的方法。
在SpringAOP提供的JoinPoint当中,封装了连接点方法在执行时的相关信息。
2. 通知:Advice,指的就是那些抽取出来的重复的逻辑,也就是共性功能(最终体现形式为一个方法,定义了要做什么)
在AOP面向切面编程当中,我们只需要将这部分重复的代码逻辑抽取出来单独定义。抽取出来的这一部分重复的逻辑,也就是共性的功能。
3. 切入点:Pointcut,切入点指的是匹配连接点的条件,通知仅会在切入点方法执行时被应用
在通知当中,我们所定义的共性功能到底要应用在哪些方法上?
假如:切入点表达式改为DeptServiceImpl.list(),此时就代表仅仅只有list这一个方法是切入点。只有list()方法在运行的时候才会应用通知。
@Around注解代表的是环绕,
4. 切面:Aspect,切面描述通知与切入点的对应关系,每个切面由切入点和通知组成(通知+切入点)
5. 目标对象:Target, 目标对象指的就是通知所应用 / 作用的对象,我们就称之为目标对象。
分析:我们所定义的通知是如何与目标对象结合在一起,对目标对象当中的方法进行功能增强的?
Spring AOP底层的另外一种动态代理技术:Cglib动态代理。
AOP的基础知识学习完之后,下面我们对AOP当中的各个细节进行详细的学习。主要分为4个部分:
通知类型:Spring AOP当中支持的通知类型
通知顺序:多个通知之间,通知的执行顺序
切入点表达式的写法
连接点JoinPoint:通过连接点来获取方法执行时的相关信息
在入门程序当中,我们已经使用了一种功能最为强大的通知类型:Around环绕通知。
- 只要我们在通知方法上加上了@Around注解,就代表当前通知是一个环绕通知。
Spring AOP当中所支持的五种通知类型:
@Around:环绕通知 Around Advice,此注解标注的通知方法在目标方法前、后都被执行,都会来执行通知方法当中的逻辑,而中间运行的就是原始方法。@Around环绕通知中如果原始方法在调用时出现异常,那么通知方法中环绕之后的代码逻辑就不会再执行了(因为原始方法调用已经出异常了) 。
@Before:前置通知,此注解标注的通知方法在目标方法运行之前被执行
@After :后置通知 / 最终通知,此注解标注的通知方法在目标方法运行之后被执行,无论目标方法在执行的过程中是否有异常都会执行,因此也叫最终通知。
@AfterReturning : 返回后通知,此注解标注的通知方法在目标方法运行之后被执行,如果目标方法执行过程中有异常,@AfterReturning标识的通知方法 则不会执行;如果目标方法在执行的过程当中抛出了异常,那这个返回后通知就不会执行;只有目标方法正常执行返回,它才会执行
@AfterThrowing : 异常后通知,此注解标注的通知方法会在目标方法运行发生异常后执行,正常情况下@AfterThrowing通知类型它是不会被执行的。原始方法(程序)没有发生异常的情况下,@AfterThrowing标识的通知方法是不会执行的。
@AfterReturning与@AfterThrowing两个之间是互斥的!
注意:目标方法指的就是原始方法!
在使用通知时的注意事项:
@Around环绕通知需要自己调用 ProceedingJoinPoint.proceed() 来让原始方法执行,其他通知不需要考虑目标方法执行
@Around环绕通知方法的返回值,必须指定为Object,来接收原始方法的返回值,否则原始方法执行完毕,是获取不到返回值的。
五种常见的通知类型,我们已经测试完毕了,此时我们再来看一下刚才所编写的代码,有什么问题吗?
//前置通知
@Before("execution(* com.itheima.service.*.*(..))")
//环绕通知
@Around("execution(* com.itheima.service.*.*(..))")
//后置通知
@After("execution(* com.itheima.service.*.*(..))")
//返回后通知(程序在正常执行的情况下,会执行的后置通知)
@AfterReturning("execution(* com.itheima.service.*.*(..))")
//异常通知(程序在出现异常的情况下,执行的后置通知)
@AfterThrowing("execution(* com.itheima.service.*.*(..))")
我们发现啊,每一个注解里面都指定了切入点表达式,而且这些切入点表达式都一模一样。此时我们的代码当中就存在了大量的重复性的切入点表达式;假如此时切入点表达式需要变动,就需要将所有的切入点表达式一个一个的来改动,就变得非常繁琐了。
package com.gch.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@Aspect // 标识当前类为AOP切面类
public class MyAspect1 {
/**
* 切入点方法
* 通过@Pointcut注解来指定公共的切入点表达式
*/
@Pointcut("execution(* com.gch.service.*.*(..))")
private void pt(){
}
/**
* @Before 前置通知
* @param joinPoint 用于表示正在执行的连接点,通过JoinPoint可以访问方法的参数、方法签名、目标对象等信息
*/
@Before("pt()") // 类似于方法调用的形式来引入切入点表达式
public void before(JoinPoint joinPoint){
log.info("before ...");
}
/**
* @Around 环绕通知
* @param proceedingJoinPoint(Interface) extends JoinPoint(Interface)
* @return 原始方法的返回值
* @throws Throwable
*/
@Around("pt()") // 引入切入点
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
log.info("around before ...");
//调用目标对象的原始方法执行
Object result = proceedingJoinPoint.proceed();
//原始方法在执行时:发生异常,后续代码将不再执行
log.info("around after ...");
return result;
}
/**
* @After 后置通知
* @param joinPoint 用于表示正在执行的连接点
*/
@After("pt()") // 引入切入点
public void after(JoinPoint joinPoint){
log.info("after ...");
}
/**
* @AfterReturning 返回后通知(程序在正常执行返回的情况下,会执行的后置通知)
* @param joinPoint 用于表示正在执行的连接点
*/
@AfterReturning("pt()") // 引入切入点
public void afterReturning(JoinPoint joinPoint){
log.info("afterReturning ...");
}
/**
* @AfterThrowing 异常通知(程序在出现异常的情况下,执行的后置通知)
* @param joinPoint 用于表示正在执行的连接点
*/
@AfterThrowing("pt()") // 引入切入点
public void afterThrowing(JoinPoint joinPoint){
log.info("afterThrowing ...");
}
}
需要注意的是:当切入点方法使用private修饰时,仅能在当前切面类中引用该表达式, 当外部其他切面类中也要引用当前类中的切入点表达式,就需要把private改为public,而在引用的时候,具体的语法为:全类名.方法名()
具体形式如下:
package com.gch.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@Aspect
/**
切面类
*/
public class MyAspect2 {
/**
* 引入MyAspect1切面类中的切入点表达式,引用的具体语法:全类名.方法名()
*/
@Before("com.gch.aop.MyAspect1.pt()")
public void before(){
log.info("MyAspect2 -> before...");
}
}