SpringBoot——IOC与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对象的过程中,主要通过底层的动态代理机制,对特定的方法进行编程

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

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

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

image-20230517160748642

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

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

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

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

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

image-20230517161356224

2.2 快速入门 - AOP 开发步骤

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

2.2.1 Maven依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</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 应用场景及优势

应用场景

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

image-20230517164446034

优势

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

2.3 核心概念

2.3.1 连接点 - JoinPoint

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

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

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

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

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

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

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

image-20230517175551925

2.3.2 AOP执行流程

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

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

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

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

​ 其实就是我们的通知

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

image-20230517180323565

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中又特制方法的执行

image-20230517175551925

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

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

image-20230520103824339

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

image-20230520103842552

对于@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对象(代理对象)

image-20230608094202317

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之前执行

如下图所示

image-20230608112044658

//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();

image-20230608115959562

3.3 AOP相关概念

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

image-20230608135908848

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

image-20230517175551925

3.4 基于xml配置的AOP

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

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

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

image-20230608143513157

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

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

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

3.4.1 xml方式AOP快速入门

  • 导入AOP相关坐标
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.6</version>
        </dependency>

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

image-20230608144418630

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

  • 准备目标类、准备增强类,并配置给Spring管理
<!--配置目标类-->
<bean id="userService" class="com.zhangjingqi.service.impl.UserServiceImpl">
</bean>

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

需要一个对应的命名空间

image-20230608145153122

<!--配置AOP-->
<aop:config>
    <!--配置切入点表达式,目的就是指定哪些方法要被增强-->
    <aop:pointcut id="myPointcut"
                  expression="execution(void com.zhangjingqi.service.impl.UserServiceImpl.show1())"/>
</aop:config>
  • 配置织入(切点被哪些通知方法增强,是前置增强还是后置增强)
<!--配置AOP-->
<aop:config>
    <!--配置切入点表达式,目的就是指定哪些方法要被增强-->
    <aop:pointcut id="myPointcut"
                  expression="execution(void com.zhangjingqi.service.impl.UserServiceImpl.show1())"/>
    <!--配置织入,目的是指定哪些切点与哪些通知进行结合的-->
    <!--ref参数指定通知类是谁-->
    <aop:aspect ref="myAdvice">
        <!--aop:before是前置通知,method是前置增强的方法,pointcut-ref配置切点是哪个-->
        <!--这个地方说白了就是myAdvice对象为UserServiceImpl类的show1方法提供一个前置通知(前置增强)beforeAdvice-->
        <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();

image-20230608150439367

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:pointcut id="myPointcut2"-->
<!--                      expression="execution(void com.zhangjingqi.service.impl.UserServiceImpl.show2())"/>-->
        <!--配置织入,目的是指定哪些切点与哪些通知进行结合的-->
        <!--ref参数指定通知类是谁-->
        <aop:aspect ref="myAdvice">
            <!--aop:before是前置通知,method是前置增强的方法,pointcut-ref配置切点是哪个-->
            <!--这个地方说白了就是myAdvice对象为UserServiceImpl类的show1方法提供一个前置通知(前置增强)beforeAdvice-->
            <aop:before method="beforeAdvice" pointcut-ref="myPointcut"></aop:before>
            <!--增强show2-->
            <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连接点对象,任何通知都可使用,可以获得当前目标对象、目标方法参数等信息
ProceedingJoinPointJoinPoint子类对象,主要是在环绕通知中执行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 使用<advisor> 配置切面

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 使用<aspect>配置切面

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

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

    <!--配置AOP-->
    <aop:config>
        <!--配置切入点表达式,目的就是指定哪些方法要被增强-->
        <aop:pointcut id="myPointcut"
                      expression="execution(void com.zhangjingqi.service.impl.UserServiceImpl.show1())"/>
<!--        <aop:pointcut id="myPointcut2"-->
<!--                      expression="execution(void com.zhangjingqi.service.impl.UserServiceImpl.show2())"/>-->
        <!--配置织入,目的是指定哪些切点与哪些通知进行结合的-->
        <!--ref参数指定通知类是谁-->
        <aop:aspect ref="myAdvice">
            <!--aop:before是前置通知,method是前置增强的方法,pointcut-ref配置切点是哪个-->
            <!--这个地方说白了就是myAdvice对象为UserServiceImpl类的show1方法提供一个前置通知(前置增强)beforeAdvice-->
            <aop:before method="beforeAdvice" pointcut-ref="myPointcut"></aop:before>
            <!--增强show2-->
            <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是可以的

image-20230608174029077

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

image-20230608174322762

使用场景不同

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

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

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

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

3.4.3 原理剖析

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

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

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

image-20230609092845202

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

image-20230609093312160

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

image-20230609093559002

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

image-20230609093536815

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

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

image-20230609093944295

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

image-20230609094122299

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

image-20230609094250800

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

image-20230609094338304

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

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

image-20230609094818240

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

image-20230609095741894

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

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

image-20230609100343892

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

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

image-20230609100850459

点进下面这个方法

image-20230609100951608

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

image-20230609101051232

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

image-20230609101202499

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

image-20230609101255854

最终发现是一个接口

image-20230609101636067

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

image-20230609102111461

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

image-20230609102313647

3.4.4 AOP底层两种生成Proxy方式

代理技术使用条件配置方式
JDK 动态代理技术目标类有接口,是基于接口动态生成实现类的代理对象目标类有接口的情况下,默认方式
Cglib 动态代理技术目标类无接口且不能使用final修饰,是基于被代理对象动态生成子对象为代理对象目标类无接口时,默认使用该方式;目标类有接口时,手动配置<aop:config proxy-target-class=“true”>强制使用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>

找命名空间处理器

image-20230609150157115

点进去找到对应的解析器

image-20230609150230775

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

image-20230609150428651

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

image-20230609150532206

如下图所示,点进registerOrEscalateApcAsRequired方法

image-20230609150614382

如下图所示

image-20230609150700265

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

image-20230609150805593

看一下AnnotationAwareAspectJAutoProxyCreator对象

image-20230609151209576

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

最终在AbstractAutoProxyCreator类中找到

点进上面那个方法

image-20230609151622688

就来到了这里

image-20230609151756724

之后来到了这里

image-20230609152047684

来到了这里

image-20230609152116582

来到了这里

image-20230609152131502

有两种此方法的实现

image-20230609152205782

3.5.1.2 纯注解方式

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

此注解就代替了<aop:aspectj-autoproxy></aop:aspectj-autoproxy>

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

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

image-20230609152736456

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

image-20230609153242451

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

image-20230609153327138

3.6 AOP配置原理总结

image-20230609153643057

  • 3
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我爱布朗熊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值