SpringBoot——IOC与AOP

文章目录

  • IOC AOP
  • 一、 分层解耦
    • 1.1 IOC - 控制反转 详细
    • 1.2 DI - 依赖注入 详解
  • 二、AOP
    • 2.1 了解
    • 2.2 快速入门 - AOP 开发步骤
      • 2.2.1 Maven依赖
      • 2.2.2 代码实现
      • 2.2.3 AOP 应用场景及优势
    • 2.3 核心概念
      • 2.3.1 连接点 - JoinPoint
      • 2.3.2 AOP执行流程
    • 2.4 通知
      • 2.4.1 通知类型
      • 2.4.2 通知顺序
    • 2.5 切入点表达式
      • 2.5.1 execution
        • 2.5.1.1 execution通配符
        • 2.5.1.2 execution表达式案例
        • 2.5.1.3 切入点表达式建议
      • 2.5.2 @annotation
      • 2.5.3 切入点表达式总结
    • 2.6 连接点
  • 三、 Spring AOP
    • 3.1 AOP思想实现方案
    • 3.2 模拟AOP基础代码
    • 3.3 AOP相关概念
    • 3.4 基于xml配置的AOP
      • 3.4.1 xml方式AOP快速入门
      • 3.4.2 AOP配置详解
        • 3.4.2.1 切点表达式的配置方式
        • 3.4.2.2 切点表达式的配置语法
        • 3.4.2.3 通知的类型
        • 3.4.2.4 AOP的配置的两种方式
          • 3.4.2.4.1 使用\ 配置切面
          • 3.4.2.4.2 使用\配置切面
          • 3.4.2.4.2 两种方式不同处
      • 3.4.3 原理剖析
      • 3.4.4 AOP底层两种生成Proxy方式
    • 3.5 基于注解配置的AOP
      • 3.5.1 原理剖析
        • 3.5.1.1 xml配置组件扫描形式
        • 3.5.1.2 纯注解方式
    • 3.6 AOP配置原理总结

IOC AOP

一、 分层解耦

  • 内聚: 软件中各个功能模块内部的功能联系
  • 耦合: 衡量软件中各个层/模块之间的依赖、关联的程度
  • 软件设计原则:高内聚、低耦合

控制反转:Inversion Of Control,简称IOC。对象的创建控制权由程序自身转移到外部(容器),这种思想成为控制反转

依赖注入:Dependency Injection,简称DI。容器为应用程序提供运行时,所依赖的资源,称为依赖注入。

Bean对象: IOC容器中创建、管理的对象,称为bean

1.1 IOC - 控制反转 详细

把某个对象交给IOC容器管理,需要添加如下注解之一:

注解 说明 位置
@Component 生命bean的基础注解 不属于以下三类时,用此注解
@Controller @Component衍生注解 标注在控制器
@Service @Component衍生注解 标注在业务类上
@Repository @Component衍生注解 标注在数据访问类上(由于与Mybatis整合,用的少)
  • 声名bean的时候,可以通过value属性指定bean的名字,如果没有指定,默认是类名首字母小写

  • 使用以上四个注解都可以生命bean,但是在Springboot集成web开发中,声名控制器bean只能用@Controller

  • bean的四大注解想要生效,需要被组件扫描注解@ComponentScan扫描

  • @ComponentScan注解虽然没有显示配置,但是实际上已经包含在了启动类生命注解@SpringBootApplication中,默认扫描的范围是启动类所在包及其子包

    如下包名是从“java”包后开始的,但是下面这种不推荐,我们希望的是按照Spring的规范,将包设置在启动类所在包及其子包

@ComponentScan({"dao","com.zhangjingqi"})

1.2 DI - 依赖注入 详解

@Autowired 注解默认是按照类型进行的,如果存在多个相同的bean,会报错。

EmpServiceA 实现 EmpService类,EmpServiceB 实现 EmpService类,我们在某个地方注入EmpService对象时便会出现注入错误。

解决方案

  • @Primary 设置bean的优先级

​ 如果我们想要哪个bean填入容器,可以在类名之上添加@Primary

  • @Qualifier 指定bean的名字
   @Qualifier("empServiceA")
   @Autowired
   private EmpService empService;
  • @Resource 按照名称注入

    @Autowired 注解默认按照类型注入,@Resource默认按照类名进行注入

   @Resource(name = "empServiceB")
   private EmpService empService;

二、AOP

2.1 了解

Spring的第二大核心,第一大核心是IOC

AOP:面向切面编程、面向方面编程,其实就是面向特定方法编程

实现

  • **动态代理是面向切面编程最主流的实现。**而SpringAOP是Spring框架的高级技术,目的是在管理bean对象的过程中,主要通过底层的动态代理机制,对特定的方法进行编程

  • 为什么要面向方法编程?

场景:案例部分功能运行较慢,定位执行耗时较长的业务方法,此时需要统计每一个业务方法的执行耗时,找到耗时较长的业务进行优化

​ 按照之前的方式,就是在方法开始前和开时候分别获取一个时间,两个时间相减就是执行耗时,但是这种方式是非常繁琐的

SpringBoot——IOC与AOP_第1张图片

如果我们基于AOP,面向方法编程,我们可以做到在不改动原始方法的基础上,来针对原始的方法进行编程,可以是对原始方法功能的增强,也可以改变原始方法的功能

比如我们现在要统计方法的耗时,我们只需要定义一个模板方法,将公共的代码定义在模板方法中

原始业务方法在这里指的是需要统计执行耗时的业务方法。而这样面向一个或者多个方法进行编程,就称为面向切面编程

比如我们调用list()方法,此时并不会直接执行原始的list方法,而是自动的去执行模板方法。

模板中所定义的代码逻辑其实是创建出来的代理对象方法中的逻辑

SpringBoot——IOC与AOP_第2张图片

2.2 快速入门 - AOP 开发步骤

需求:统计各个业务层方法执行耗时

2.2.1 Maven依赖

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-aopartifactId>
dependency>

2.2.2 代码实现

针对于特定方法根据业务需要进行编程

@Slf4j
@Component //交给容器IOC进行管理
@Aspect //加上这个注解表示不是一个普通的类,而是一个AOP类,在此类中定义模板方法
public class TimeAspect {

//  参数是一个表达式,表示针对哪些特定方法进行编程
//  com.zhangjingqi.service 包名
//  第一个*代表任意返回值 第二个*代表类名或者接口名  第三个*代表方法名
    @Around("execution(* com.zhangjingqi.service.*.*(..))") //切入点表达式
    public Object recordTime(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        long begin = System.currentTimeMillis();

//      result 原始方法执行返回值
        Object result = proceedingJoinPoint.proceed();//调用原始方式运行

        long end = System.currentTimeMillis();

//      proceedingJoinPoint.getSignature() 获取方法的签名,我们就知道是哪个方法了
//      如: List com.zhangjingqi.service.impl.DeptServiceImpl.list()执行耗时:239ms
        log.info(proceedingJoinPoint.getSignature() + "执行耗时:{}ms", end - begin);

//      原始方法的返回值我们需要返回回去
        return result;
    }
}

2.2.3 AOP 应用场景及优势

应用场景

  • 记录操作日志
  • 权限控制
  • 事务管理

SpringBoot——IOC与AOP_第3张图片

优势

  • 代码无侵入
  • 减少重复代码
  • 提高开发效率
  • 维护方便

2.3 核心概念

2.3.1 连接点 - JoinPoint

连接点:JoinPoint,可以被AOP控制的方法(暗含方法执行时的相关信息)

通知:Advice,指那些重读的逻辑,也就是共性功能(最终体现为一个方法)

切入点:PointCut,匹配连接点的条件,通知仅会在切入点方法执行时被应用(就是实际被AOP控制的方法)

我们通常会使用下面的切入点表达式来描述切入点

@Around("execution(* com.zhangjingqi.service.*.*(..))") 

切面:Aspect,描述通知与切入点的对应关系(通知+切入点),被@Aspect注解修饰的类我们一般称为切面类

目标对象:Target,通知所应用的对象。

SpringBoot——IOC与AOP_第4张图片

2.3.2 AOP执行流程

通知如何与目标对象结合在一起对目标对象中的方法进行功能增强的?

​ ①SpringAOP是基于动态代理技术来实现的。程序运行的时候会自动的基于动态代理技术为目标对象生成一个对应的代理对象。

​ ② 在代理对象中就会对目标对象中的原始方法进行功能的增强。

如何来增强的?增强的逻辑是什么样子的?

​ 其实就是我们的通知

​ ③最终在Spring容器中注入的是代理对象,调用的方法也是代理对象中的对应方法

SpringBoot——IOC与AOP_第5张图片

2.4 通知

2.4.1 通知类型

  • @Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行,出现异常后后置代码不会执行。(因为原始方法出现异常了)
  • @Before:前置通知,此注解标注的通知方法在目标方法前被执行
  • @After:后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
  • @AfterReturning:返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行
  • @AfterThrowing:异常后通知,此注解标注的通知方法在发生异常后执行
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
public class MyAspect1 {
    //前置通知
    @Before("execution(* com.zhangjingqi.service.*.*(..))")
    public void before(JoinPoint joinPoint) {
        log.info("before ...");
    }

    //环绕通知
    @Around("execution(* com.zhangjingqi.service.*.*(..))")
    public Object around(ProceedingJoinPoint proceedingJoinPoint)
            throws Throwable {
        log.info("around before ...");
        //调用目标对象的原始方法执行
        Object result = proceedingJoinPoint.proceed();
        //原始方法如果执行时有异常,环绕通知中的后置代码不会在执行了
        log.info("around after ...");
        return result;
    }

    //后置通知
    @After("execution(* com.zhangjingqi.service.*.*(..))")
    public void after(JoinPoint joinPoint) {
        log.info("after ...");
    }

    //返回后通知(程序在正常执行的情况下,会执行的后置通知)
    @AfterReturning("execution(* com.zhangjingqi.service.*.*(..))")
    public void afterReturning(JoinPoint joinPoint) {
        log.info("afterReturning ...");
    }

    //异常通知(程序在出现异常的情况下,执行的后置通知)
    @AfterThrowing("execution(* com.zhangjingqi.service.*.*(..))")
    public void afterThrowing(JoinPoint joinPoint) {
        log.info("afterThrowing ...");
    }
}

注意事项

  • @Around环绕通知需要自己调用ProceedingJoinPoint.proceed()来执行原始方法,其他通知不需要考虑原始方法的执行

  • @Around环绕通知的方法的返回值,必须指定为Object,来接收原始方法的返回值

​ 如果不return,在调用这个方法的地方时拿不到返回值的

对切入点表达式进行抽取

//   生命切入点表达式的注解,切点
    @Pointcut("execution(* com.zhangjingqi.service.*.*(..))")
    private void pt(){

    }

    //前置通知
    @Before("pt()")
    public void before(JoinPoint joinPoint) {
        log.info("before ...");
    }

其他类中也可以进行抽取,只需要定位到切入点表达式的位置即可。

@Slf4j
@Component
@Aspect
public class MyAspect2 {
//引用MyAspect1切面类中的切入点表达式
@Before("com.zhangjingqi.aspect.MyAspect1.pt()")
public void before(){
log.info("MyAspect2 -> before ...");
 }
}

2.4.2 通知顺序

当有多个切面的切入点都匹配到了目标方法,目标方法运行,多个通知方法都会被执行。

下面研究多个切面类的通知顺序。同个切面类的通知顺序不再研究

  • 不同切面类中,默认按照切面类的类名字母排序

​ 目标方法前的通知方法:字母排名靠前的先执行

​ 目标方法后的通知方法:字母排名靠前的后执行

  • 使用@Order(数字)加在切面类上来控制顺序

2.5 切入点表达式

  • 切入点表达式:描述切入点方法的一种表达式
  • 作用:主要用来决定项目中哪些方法需要加入通知
  • 常见形式

​ execution(…):根据方法的签名来匹配

​ @annotation(…):根据注解匹配

2.5.1 execution

​ 主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配

下面来描述的时候,可以基于接口。也可以基于实现类

execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数) throws 异常?)

​ 其中?表示可省略的部分

  • 访问修饰符:可省略,比如public、protected
  • 包名.类名:可省略,但是不建议
  • throws 异常:可省略(注意是方法上声明抛出的异常,不是实际抛出的异常)

2.5.1.1 execution通配符

*:单个独立的任意符号,可以匹配任意返回值、包名、类名、方法名、方法参数等信息来匹配

​ 此案例表示返回值人任意,二级包任意,类或接口任意,方法参数任意但是有且只有一个

execution(* com.*.service.*.update(*)

匹配类名以Service结尾,方法以delete开头的方法

execution(void
com.itheima.service.impl.*Service.delete*(java.lang.Integer)
)

多个连续的任意符号,可以通配任意层级的包,或者任意类型、任意个数的参数

​ 层级包任意,方法的参数任意

execution(* com.zhangjingqi..DeptService.*(..)

返回值任意,方法名任意,方法参数任意

execution(* *(..)

2.5.1.2 execution表达式案例

@Pointcut("execution(* com.zhangjingqi.service.*.*(..))")
  • 省略异常
execution(public void
com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer)
)
  • 省略方法访问修饰符

    参数是全类名

execution(void
com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer)
)
  • 使用"…"省略包名
execution(public void  com..DeptServiceImpl.delete(java.lang.Integer))
  • 省略包名类名

​ 指定方法名。

​ 不建议将包名和方法名省略。一旦省略,将表达式的范围扩大,一是影响匹配的效率,而是可能匹配到其他不需要的方法

execution(public void  delete(java.lang.Integer))
  • 匹配所有的方法

​ 此时表示匹配DeptServiceImpl类中的所有方法

execution(public void  com..DeptServiceImpl.*(java.lang.Integer))
  • 使用 且(&&)、或(||)、非(!) 来组合比较复杂的切入点表达式

    execution(* com.zhangjingqi.service.DeptService.list(..)) ||
    execution(* com.zhangjingqi.service.DeptService.delete(..))
    

2.5.1.3 切入点表达式建议

  • 所有业务方法名在命名时尽量规范,方便切入点快速匹配。

​ 如查询方法find开头,更新类方法update开头

  • 描述切入点方法通常基于接口描述,而不是直接描述实现类,增强拓展性

  • 在满足业务需要的前提下,尽量缩小切入点的匹配范围

​ 包名匹配进行不使用“…”,使用“*”匹配单个包

2.5.2 @annotation

用于匹配标识有特定注解的方法

@Before("@annotation(com.zhangjingqi.anno.MyLog)")

简化下列表达式

execution(* com.zhangjingqi.service.DeptService.list(..)) ||
execution(* com.zhangjingqi.service.DeptService.delete(..))

实现步骤

  1. 编写自定义注解

  2. 在业务类要做为连接点的方法上添加自定义注解

创建自定义注解类

@Retention(RetentionPolicy.RUNTIME)//描述注解什么时候生效的:运行时有效
@Target(ElementType.METHOD)//当前注解可以作用在哪些地方
public @interface MyLog {
}

添加自定义注解

@Override
@MyLog //自定义注解(表示:当前方法属于目标方法)
public List<Dept> list() { ... }

@Override
@MyLog //自定义注解(表示:当前方法属于目标方法)
public void delete(Integer id) { ... }

切面类

@Slf4j
@Component
@Aspect
public class MyAspect6 {
//针对list方法、delete方法进行前置通知和后置通知
//前置通知
@Before("@annotation(com.zhangjingqi.anno.MyLog)")
   public void before(){
   log.info("MyAspect6 -> before ...");

//后置通知
@After("@annotation(com.zhangjingqi.anno.MyLog)")
   public void after(){
   log.info("MyAspect6 -> after ...");
 }
}

2.5.3 切入点表达式总结

  • execution切入点表达式

​ 根据我们所指定的方法的描述信息来匹配切入点方法,这种方式也是最为常用的一种方式

​ 如果我们要匹配的切入点方法的方法名不规则,或者有一些比较特殊的需求,通过

​ execution切入点表达式描述比较繁琐

  • annotation 切入点表达式

​ 基于注解的方式来匹配切入点方法。这种方式虽然多一步操作,我们需要自定义一个注解,但

​ 是相对来比较灵活。我们需要匹配哪个方法,就在方法上加上对应的注解就可以了

2.6 连接点

被AOP控制的方法,目标对象中所有的方法都可以被AOP控制,在Spring AOP中又特制方法的执行

SpringBoot——IOC与AOP_第6张图片

  • 在Spring中用JoinPoint抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法类名、方法参数等

​ 对于@Around通知,获取连接点信息只能用ProceedingJoinPoint

SpringBoot——IOC与AOP_第7张图片

​ 对于其他四种通知,获取连接点信息只能使用JoinPoint,它是ProceedingJoinPoint父类型

SpringBoot——IOC与AOP_第8张图片

对于@Around通知,为什么获取连接点信息只能用ProceedingJoinPoint?

 在Spring AOP中,@Around通知是最为强大和灵活的通知类型,它可以决定是否执行连接点,以及如何处理连接点返回的结果。因此,@Around通知需要通过ProceedingJoinPoint参数来获取连接点信息。

​ ProceedingJoinPoint是JoinPoint的子类,同时也是JoinPoint的扩展版本。JoinPoint表示连接点,也就是被Advice修饰的方法。而ProceedingJoinPoint除了表示连接点外,还具有一个proceed()方法,该方法是执行目标方法的关键。在@Before和@After通知中,JoinPoint足以满足需要,因为它们只需要获取连接点信息即可,不需要执行目标方法。但在@Around中,除了获取连接点信息,还需要控制目标方法的执行,因此需要用到ProceedingJoinPoint。

​ 在@Around通知中,可以通过ProceedingJoinPoint的proceed()方法,手动控制目标方法的执行。例如,可以在proceed()方法前后进行一些预处理或后处理。同时,ProceedingJoinPoint还提供了一些其他的工具方法,例如getArgs()获取目标方法参数,getSignature()获取目标方法签名等,这些方法在编写@Around通知时也非常有用。

三、 Spring AOP

3.1 AOP思想实现方案

动态代理技术:在运行期间,对目标对象的方法进行增强,代理对象同名方法内可以执行原有逻辑的同时嵌入执行其他增强逻辑或其他对象的方法

如下图所示

A对象是要被增强的对象,叫做目标对象

methodA1与methodA2是要被增强的方法,叫做目标方法

B对象叫做增强对象

B对象内部的方法叫做增强方法

要对A对象产生一个Proxy代理对象

代理对象的方法与目标对象中方法名字是一个样子的,并且类型也是一个样子的

之后在调用A对象的时候,其实调用的是A对象的Proxy对象(代理对象)

SpringBoot——IOC与AOP_第9张图片

3.2 模拟AOP基础代码

但是与Spring AOP的代码相差很多

会修改一下A对象的BeanDefinition信息,将全限定名改为Proxy对象的

创建一个接口

public interface UserService {
    void show1();
    void show2();
}

创建一个实现类

public class UserServiceImpl implements UserService{

    @Override
    public void show1() {
        System.out.println("show1......");
    }

    @Override
    public void show2() {
        System.out.println("show2.......");
    }
}

创建一个增强类,内部提供增强方法

//自定义增强类
public class MyAdvice {
    public void beforeAdvice() {
        System.out.println("beforeAdvice ...");
    }

    public void afterAdvice() {
        System.out.println("afterAdvice ...");
    }
}

配置Bean对象

<bean id="userService" class="com.zhangjingqi.service.impl.UserServiceImpl">
bean>
<bean id="myAdvice" class="com.zhangjingqi.advice.MyAdvice">
bean>

准备Proxy对象

BeanPostProcessor

Bean后处理器,一般在Bean实例化之后,填充到单例池singletonObjects之前执行

如下图所示

SpringBoot——IOC与AOP_第10张图片

//BeanPostProcessor:Bean后处理器,一般在Bean实例化之后,填充到单例池singletonObjects之前执行
public class MockAopBeanPostProcessor implements BeanPostProcessor, ApplicationContextAware {

    private ApplicationContext applicationContext;

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
//      目的: 对UserServiceImpl中的show1和show2方法进行增强,增强方法存在于MyAdvice中
//      问题: 1.筛选出UserServiceImpl 或者是service.impl包下的所有类的所有方法都可以增强
//              如果这个地方不筛选的话,所有的类的方法都会增强,这显然不是我们的目的
//            解决方案: 使用if...else 判断一下就可以了
//            2. MyAdvice怎么获取?
//            解决方案: 可以将MyAdvice存入容器
         if (bean.getClass().getPackage().getName().equals("com.zhangjingqi.service.impl")) {
//          TODO 生成Bean的Proxy对象
//          参数一: 类加载器
//          参数二:它实现的接口
//          参数三: new InvocationHandler()
            Object beanProxy = Proxy.newProxyInstance(bean.getClass().getClassLoader(),
                    bean.getClass().getInterfaces(),
                    new InvocationHandler() {
                        @Override
                        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//                           TODO 增强对象的before方法
                            MyAdvice myAdvice = applicationContext.getBean(MyAdvice.class);
                            myAdvice.beforeAdvice();

                            //TODO 执行目标对象的目标方法
                            //  参数一: 我们要执行的是哪个对象
                            //  参数二: 参数
                            //  result是method.invoke(bean, args)执行完成的返回值
                            Object result = method.invoke(bean, args);

//                          TODO 增强对象的after对象
                            myAdvice.afterAdvice();

                            return result;
                        }
                    });
//          返回代理对象
            return beanProxy;
        }
//      运行到这里说明不需要代理对象
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        return null;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

将MockAopBeanPostProcessor注入到容器

<bean class="com.zhangjingqi.processor.MockAopBeanPostProcessor">bean>

进行测试

ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml");
UserService bean = applicationContext.getBean(UserService.class);
bean.show1();

SpringBoot——IOC与AOP_第11张图片

3.3 AOP相关概念

从这个地方开始,可以选择看标题二中的内容就可以

SpringBoot——IOC与AOP_第12张图片

我还是觉得下面这个图清晰一点

SpringBoot——IOC与AOP_第13张图片

3.4 基于xml配置的AOP

之前我们在自定义AOP的时候,bean所在的位置是写死的,这个地方显然不能写死,我们需要在配置文件中配置一个动态的

if (bean.getClass().getPackage().getName().equals("com.zhangjingqi.service.impl"))

包括我们在切面中,把增强的方法也写死了,这显然不是很合理

SpringBoot——IOC与AOP_第14张图片

所以我们需要通过配置类去解决一些问题

配置方式的设计、配置文件(注解)的解析工作,Spring已经帮我们封装好了

  • 配置哪些包、哪些类、哪些方法需要被增强(切点表达式的配置)
  • 配置目标方法要被哪些通知方法所增强,在目标方法执行之前还是之后执行增强

3.4.1 xml方式AOP快速入门

  • 导入AOP相关坐标
        <dependency>
            <groupId>org.aspectjgroupId>
            <artifactId>aspectjweaverartifactId>
            <version>1.9.6version>
        dependency>

为什么Context坐标中有AOP的包,我们还要导入一个呢?

SpringBoot——IOC与AOP_第15张图片

因为Spring觉得好,就把他集成了,并且座位了Spring开发的一部分

  • 准备目标类、准备增强类,并配置给Spring管理

<bean id="userService" class="com.zhangjingqi.service.impl.UserServiceImpl">
bean>


<bean id="myAdvice" class="com.zhangjingqi.advice.MyAdvice">
bean>
  • 配置切点表达式(哪些方法被增强)

需要一个对应的命名空间

SpringBoot——IOC与AOP_第16张图片


<aop:config>
    
    <aop:pointcut id="myPointcut"
                  expression="execution(void com.zhangjingqi.service.impl.UserServiceImpl.show1())"/>
aop:config>
  • 配置织入(切点被哪些通知方法增强,是前置增强还是后置增强)

<aop:config>
    
    <aop:pointcut id="myPointcut"
                  expression="execution(void com.zhangjingqi.service.impl.UserServiceImpl.show1())"/>
    
    
    <aop:aspect ref="myAdvice">
        
        
        <aop:before method="beforeAdvice" pointcut-ref="myPointcut">aop:before>

    aop:aspect>
aop:config>
  • 测试

    非常的成功

ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml");
UserService bean = applicationContext.getBean(UserService.class);
bean.show1();

SpringBoot——IOC与AOP_第17张图片

3.4.2 AOP配置详解

下AOP详细配置的细节

3.4.2.1 切点表达式的配置方式

切点表达式的配置方式有两种,直接将切点表达式配置在通知上,也可以将切点表达式抽取到外面,在通知上进行引用

如果一个方法有好几种通知/增强,在不同情况下通知/增强不一样,那我们就单独的把pointcut抽取出来

如果一直放置只有一种增强,我们就写在aop:before(或者是aop:after)标签pointcut属性中即可

    <aop:config>
        
        <aop:pointcut id="myPointcut"
                      expression="execution(void com.zhangjingqi.service.impl.UserServiceImpl.show1())"/>


        
        
        <aop:aspect ref="myAdvice">
            
            
            <aop:before method="beforeAdvice" pointcut-ref="myPointcut">aop:before>
            
            <aop:before method="beforeAdvice" pointcut="execution(void com.zhangjingqi.service.impl.UserServiceImpl.show2())">aop:before>
        aop:aspect>
    aop:config>

3.4.2.2 切点表达式的配置语法

直接查看 标题2.5即可

3.4.2.3 通知的类型

查看标题2.4

通知名称 配置方式 执行时间
前置通知 < aop:before > 目标方法执行之前执行
前置通知 < aop:after-returning > 目标方法执行之后执行,目标方法异常时,不在执行
环绕通知 < aop:around > 目标方法执行前后执行,目标方法异常时,环绕后方法不在执行
异常通知 < aop:after-throwing > 目标方法抛出异常时执行
最终通知 < aop:after > 不管目标方法是否有异常,最终都会执行

都很简单,简单的看一下环绕通知吧

在MyAdvice类中添加环绕方法

如果有返回值的话,将void给改成对应类型,添加个return即可

    public void around(ProceedingJoinPoint joinPoint) throws Throwable {
//      环绕前
        System.out.println("环绕前通知");
//      目标方法
        joinPoint.proceed();
///     环绕后
        System.out.println("环绕后通知");
    }

配置文件中配置

<aop:around method="around" pointcut-ref="myPointcut"/>

通知方法在被调用时,Spring可以为其传递一些必要的参数

参数类型 作用
JoinPoint 连接点对象,任何通知都可使用,可以获得当前目标对象、目标方法参数等信息
ProceedingJoinPoint JoinPoint子类对象,主要是在环绕通知中执行proceed(),进而执行目标方法
Throwable 异常对象,使用在异常通知中,需要在配置文件中指出异常对象名称

看一下最后一个Throwable

public void afterThrowing(JoinPoint joinPoint,Throwable th){
//获得异常信息
System.out.println("异常对象是:"+th+"异常信息是:"+th.getMessage());
}

配置文件,并且要指出异常对象的名称,这里是th

<aop:after-throwing method="afterThrowing" pointcut-ref="myPointcut" throwing="th"/>

3.4.2.4 AOP的配置的两种方式

AOP的xml有两种配置方式,如下:

3.4.2.4.1 使用 配置切面

Spring定义了一个Advice接口,实现了该接口的类都可以作为通知类出现

通知类可以实现接口,这个接口很干净

我们一般把啥都没有的接口称为标志接口,但是这个地方不是标志接口

public interface Advice {
}

比如下面这个类

public class MyAdvice2 implements MethodBeforeAdvice, AfterReturningAdvice {
    //MethodBeforeAdvice接口中的方法
    @Override
    public void before(Method method, Object[] objects, Object o) throws Throwable {
        System.out.println("前置通知.....");
    }

    //AfterReturningAdvice接口中的方法
    @Override
    public void afterReturning(Object o, Method method, Object[] objects, Object o1) throws Throwable {
        System.out.println("后置通知.....");
    }
}

xml配置,与之前的不同就是,不用在xml文件中配置前置通知、环绕通知等等通知类型,我们在MyAdvice2类中实现了一些接口,也就是说实现了一些规范


<bean id="userService" class="com.zhangjingqi.service.impl.UserServiceImpl">
bean>

<bean id="myAdvice2" class="com.zhangjingqi.advice.MyAdvice2">
bean>

<aop:config>
    
    <aop:pointcut id="myPointcut2"
                  expression="execution(void com.zhangjingqi.service.impl.UserServiceImpl.show1())"/>
    <aop:advisor advice-ref="myAdvice2" pointcut-ref="myPointcut2"/>
    
aop:config>

我们在来实现一下MethodInterceptor接口,这个特别像环绕通知

public class MyAdvice3 implements MethodInterceptor {

    @Override
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        System.out.println("环绕前****");
//      执行目标方法
//      getMethod得到字节码文件,
//      invoke方法需要两个参数,一个是当前要被执行对象是谁,第二个是方法的参数
        Object result = methodInvocation.getMethod().invoke(methodInvocation.getThis(), methodInvocation.getArguments());
        System.out.println("环绕后****");
        return result;
    }
}

配置文件


<bean id="userService" class="com.zhangjingqi.service.impl.UserServiceImpl">
bean>

<bean id="myAdvice3" class="com.zhangjingqi.advice.MyAdvice3">
bean>

<aop:config>
    
    <aop:pointcut id="myPointcut3"
                  expression="execution(void com.zhangjingqi.service.impl.UserServiceImpl.show1())"/>
    <aop:advisor advice-ref="myAdvice3" pointcut-ref="myPointcut3"/>

aop:config>
3.4.2.4.2 使用配置切面

这个相对advisor来说,是重点,这个经常用

这个就是最开始快速开发中接触的,下面的代码看一下,详细的在上面

    
    <aop:config>
        
        <aop:pointcut id="myPointcut"
                      expression="execution(void com.zhangjingqi.service.impl.UserServiceImpl.show1())"/>


        
        
        <aop:aspect ref="myAdvice">
            
            
            <aop:before method="beforeAdvice" pointcut-ref="myPointcut">aop:before>
            
            <aop:before method="beforeAdvice" pointcut="execution(void com.zhangjingqi.service.impl.UserServiceImpl.show2())">aop:before>
        aop:aspect>
    aop:config>
3.4.2.4.2 两种方式不同处

语法形式不同

  • advisor通过实现接口来确认通知的类型
  • aspect是通过配置确认通知的类型,更加灵活

可配置的切面数量不同

  • 一个advisor只能配置一个固定通知和一个切点表达式

  • 一个aspect可以配置多个通知和多个切点表达式任意组合、

    如下图所示,多个aspect是可以的

SpringBoot——IOC与AOP_第18张图片

而对于一个advisor只能配置一个固定通知和一个切点表达式,并不是说我们只能有一个aop:advisor标签,而是说我们切点MyAdvice3内部实现的接口与重写的方法固定了,内部就是一个环绕通知

SpringBoot——IOC与AOP_第19张图片

使用场景不同

  • 允许随意搭配情况下可以使用aspect进行配置

  • 如果通知类型单一、切面单一的情况下可以使用advisor进行配置

    直接实现某个接口就可以,xml文件配置就简单了

  • 在通知类型已经固定,不用人为指定通知类型时,可以使用advisor进行配置,例如后面要学习的Spring事务控制的配置

3.4.3 原理剖析

我们aop的配置是通过标签,而这个标签来自于第三方aop命名空间(也是Spring的)

所以我们应该找http://www.springframework.org/schema/aop对应的命名空间处理器是谁,如下图所示

对应的是org.springframework.aop.config.AopNamespaceHandler

SpringBoot——IOC与AOP_第20张图片

搜索AopNamespaceHandler类,在init方法中对应了不同标签的解析器

SpringBoot——IOC与AOP_第21张图片

我们看一下config对应的解析器ConfigBeanDefinitionParser

SpringBoot——IOC与AOP_第22张图片

ConfigBeanDefinitionParser类实现了BeanDefinitionParser接口,BeanDefinitionParser接口中有一个parse方法

SpringBoot——IOC与AOP_第23张图片

最终ConfigBeanDefinitionParser重写的parse方法最终也会被调用

看一下parse方法中的下面这个语句,大体意思就是创建一个Proxy自动代理器

SpringBoot——IOC与AOP_第24张图片

如下图所示,方法名称表示如果有必要的话注册一个切面自动代理

SpringBoot——IOC与AOP_第25张图片

点进去之后,看方法的第一条语句,注意这个工具来与上面的不一样

SpringBoot——IOC与AOP_第26张图片

再点进去,发现是一个过度方法

image-20230609094338304

点进去之后最终的方法浮现出来了

在下面这个地方向Spring容器中注入了一个AspectJAwareAdvisorAutoProxyCreator对象

SpringBoot——IOC与AOP_第27张图片

在AspectJAwareAdvisorAutoProxyCreator类的继承体系中,最上面有一个BeanPostProcessor Bean后处理器

SpringBoot——IOC与AOP_第28张图片

我们找一下BeanPostProcessor 的方法在AspectJAwareAdvisorAutoProxyCreator类中的实现

可能存在下图中的某一个方法或两个都有

SpringBoot——IOC与AOP_第29张图片

发现AspectJAwareAdvisorAutoProxyCreator类中没有,但是AspectJAwareAdvisorAutoProxyCreator继承了类AbstractAdvisorAutoProxyCreator,我们可以在此类中找找

也没有,但是AbstractAdvisorAutoProxyCreator继承了AbstractAutoProxyCreator,我们可以在此类中找找,发现找到了

SpringBoot——IOC与AOP_第30张图片

点进下面这个方法

SpringBoot——IOC与AOP_第31张图片

然后发现在这个地方创建了一个Proxy,创建了一个代理对象

SpringBoot——IOC与AOP_第32张图片

点进createProxy方法,调用了一个proxyFactory.getProxy方法

SpringBoot——IOC与AOP_第33张图片

再点进这个方法,就是下面这个样子

SpringBoot——IOC与AOP_第34张图片

最终发现是一个接口

SpringBoot——IOC与AOP_第35张图片

并且此接口有两个实现,也就是生成代理对象的两种方式,看一下JDK的,第二个

SpringBoot——IOC与AOP_第36张图片

Spring用JDK的代码代理生成了一个Proxy对象

SpringBoot——IOC与AOP_第37张图片

3.4.4 AOP底层两种生成Proxy方式

代理技术 使用条件 配置方式
JDK 动态代理技术 目标类有接口,是基于接口动态生成实现类的代理对象 目标类有接口的情况下,默认方式
Cglib 动态代理技术 目标类无接口且不能使用final修饰,是基于被代理对象动态生成子对象为代理对象 目标类无接口时,默认使用该方式;目标类有接口时,手动配置强制使用Cglib方式

3.5 基于注解配置的AOP

直接看 标题二

与springAOP的区别就是需要开启AOP自动代理

<aop:aspectj-autoproxy>aop:aspectj-autoproxy>

再开启组件扫描

<context:component-scan base-package="com.zhangjingqi">context:component-scan>

不用上面的这组配置,全注解的也行

@Configuration
@ComponentScan("com.zhangjingqi.aop")
@EnableAspectJAutoProxy
public class ApplicationContextConfig {
}

只不过这两种加载时加载器不同而已,区分一下

3.5.1 原理剖析

3.5.1.1 xml配置组件扫描形式

我们可以先看一下这个的原理

<context:component-scan base-package="com.zhangjingqi">context:component-scan>

找命名空间处理器

SpringBoot——IOC与AOP_第38张图片

点进去找到对应的解析器

SpringBoot——IOC与AOP_第39张图片

解析器中有一个parse方法,点进registerAspectJAnnotationAutoProxyCreatorIfNecessary方法

SpringBoot——IOC与AOP_第40张图片

如下图所示,再点进去registerAspectJAnnotationAutoProxyCreatorIfNecessary方法

SpringBoot——IOC与AOP_第41张图片

如下图所示,点进registerOrEscalateApcAsRequired方法

image-20230609150614382

如下图所示

SpringBoot——IOC与AOP_第42张图片

在这个地方注册了一个如下图所示的对象AnnotationAwareAspectJAutoProxyCreator到IOC容器当中

image-20230609150805593

看一下AnnotationAwareAspectJAutoProxyCreator对象

SpringBoot——IOC与AOP_第43张图片

AnnotationAwareAspectJAutoProxyCreator类找BeanPostDefinition方法,此类中找不到就在其父类中找,直到找到为止

最终在AbstractAutoProxyCreator类中找到

点进上面那个方法

SpringBoot——IOC与AOP_第44张图片

就来到了这里

SpringBoot——IOC与AOP_第45张图片

之后来到了这里

SpringBoot——IOC与AOP_第46张图片

来到了这里

SpringBoot——IOC与AOP_第47张图片

来到了这里

SpringBoot——IOC与AOP_第48张图片

有两种此方法的实现

image-20230609152205782

3.5.1.2 纯注解方式

那我们就应该探究@EnableAspectJAutoProxy注解

此注解就代替了

@Configuration
@ComponentScan("com.zhangjingqi.aop")
@EnableAspectJAutoProxy
public class ApplicationContextConfig {
}

点进注解@EnableAspectJAutoProxy看一下,发现有一个类AspectJAutoProxyRegistrar

SpringBoot——IOC与AOP_第49张图片

AspectJAutoProxyRegistrar类实现了ImportBeanDefinitionRegistrar接口,此接口有两个方法

SpringBoot——IOC与AOP_第50张图片

AspectJAutoProxyRegistrar类重写的接口方法,从这一步往下翻与之前一个样了,不看了

SpringBoot——IOC与AOP_第51张图片

3.6 AOP配置原理总结

SpringBoot——IOC与AOP_第52张图片

你可能感兴趣的:(#,SpringbootWeb,spring,boot,java,mybatis)