聊聊 Spring IoC 和 AOP

Spring IoC 和 AOP 详解

Spring IoC

什么是 IoC 控制反转

IoC (Inversion of Control:控制反转)是一种设计思想,而不是一个具体的技术实现。

IoC 的思想就是:将原本在程序中手动创建对象的控制权,交由 Spring 框架来管理。

注意:IoC 并非 Spring 特有,在其他语言中也有应用。

为什么叫控制反转?

  • 控制:指的是对象创建(实例化、管理)的权力
  • 反转:控制权交给外部环境(Spring 框架、IoC 容器)

什么是 IoC 容器

在 Spring 中, IoC 容器是 Spring 用来实现 IoC 的载体, IoC 容器实际上就是个 Map(key,value),Map 中存放的是各种对象。

工作流程:

  1. IoC 容器实例化对象并存储起来,
  2. 函数(比如 main)需要哪个对象就往 IoC 容器拿就好。

简单来说,就是起到了一个中间站的作用

总结

Spring IoC 是 Spring 框架的核心特性之一。

它通过 IoC 容器来管理对象,实现了控制反转和依赖注入。

在 Spring 中,IoC 容器负责管理对象的创建和依赖关系的维护,应用程序只需要通过 IoC 容器获取所需的对象即可。

在出现 IOC 之前,项目中是如何使用 Bean 的?

在 Spring 框架的出现之前,项目中通常使用【手动创建】和【管理对象】的方式来使用 “bean”,尤其是在 JavaEE(现在称为 Jakarta EE)开发中。下面是在出现 IOC(控制反转)和 Spring 之前,项目中如何使用 “bean” 的一些常见做法:

  1. 手动创建对象: 在没有 Spring 框架的情况下,开发人员需要手动实例化和管理对象。这意味着你需要在代码中直接使用 new 关键字来创建对象,然后自行处理对象的生命周期。(例如 new 对象,结合 get 方法使用)
  2. 工厂模式: 开发人员可能会使用工厂模式来创建对象。这涉及创建一个工厂类,其中包含创建和管理对象的逻辑。这种方式可以将对象的创建和使用分离开,但仍然需要手动管理对象的生命周期。
  3. 单例模式: 在很多情况下,开发人员会使用单例模式来确保系统中只有一个实例。这可以通过在类中添加一个私有构造函数和一个静态方法来实现。但这仍然需要手动管理单例实例的创建和使用。
  4. 依赖关系管理: 在没有 IOC 容器的情况下,开发人员需要手动处理对象之间的依赖关系。这可能会导致代码中充斥着对象之间的创建和注入逻辑。
  5. 手动资源释放: 在对象使用完毕后,开发人员需要手动释放资源,比如关闭数据库连接、释放文件句柄等。

总之,在没有 IOC 和 Spring 框架之前,项目的代码可能会更加繁琐和冗长,需要开发人员手动管理对象的创建、注入和生命周期。Spring 框架的出现极大地简化了这些过程,通过 IOC 容器和依赖注入,让开发人员更专注于业务逻辑,而无需过多关注对象的创建和管理。

说说循环依赖

什么是循环依赖?

循环依赖指的是多个 Bean 之间相互依赖,导致无法完成实例化。常见的两种情况:

  • 构造器循环依赖(Spring 无法 解决,会抛出异常)
  • Setter/字段注入循环依赖(Spring 可以 解决,通过三级缓存)

简单说就是自己依赖自己,或者和别的 Bean 相互依赖。

只有单例的 Bean 才存在循环依赖的情况,原型(Prototype)情况下,Spring 会直接抛出异常。原因很简单,AB 循环依赖,A 实例化的时候,发现依赖 B,创建 B 实例,创建 B 的时候发现需要 A,创建 A1 实例······无限套娃,直接把系统干垮。

Spring 可以解决哪些情况的循环依赖?

  1. 当循环依赖的实例都采用 setter 方法注入的时候,Spring 可以解决。

  2. 都采用构造器注入的时候,不可以解决。

  3. 构造器注入和 setter 注入同时存在的时候,需要分情况。

    • 原因是 Spring 在创建 Bean 时默认会根据自然排序进行创建。比如 A 会先于 B 进行创建。
    • Spring 优先使用构造器注入,setter 注入是在构造器注入后才发生的。
    • 解决方案:
      • 使用 @Lazy 让某个 Bean 延迟加载。
      • 改用 setter 注入,完全避免构造器循环依赖问题。

那 Spring 怎么解决循环依赖的呢?

Spring 主要通过 三级缓存(三级 Map) 解决循环依赖问题,解决 “setter 方式注入” 的循环依赖。

  • 简单来说:Spring 通过提前暴露 Bean 的 ObjectFactory,让依赖的 Bean 先拿到一个代理对象,从而完成初始化。

三级缓存介绍

  1. 一级缓存singletonObjects —— 存放 完全实例化的 Bean(已初始化完成)。
  2. 二级缓存earlySingletonObjects —— 存放 实例化但未初始化的 Bean
  3. 三级缓存singletonFactories —— 存放 可以创建 Bean 代理的工厂ObjectFactory)。

三级缓存的作用

三级缓存的核心作用是 提前暴露对象,使 Spring 在创建 Bean 的过程中,即使发生循环依赖,也能获取到对象的引用。

三级缓存如何工作?

(1)实例化 Bean(创建对象)

  • Spring 先 创建 Bean 的原始对象
  • 不初始化,并放入三级缓存(singletonFactories)

(2)提前暴露对象(放入二级缓存)

  • 如果发现该 Bean 存在循环依赖,Spring 会从三级缓存中获取 Factory
  • 提前暴露早期对象,放入二级缓存(earlySingletonObjects)

(3)属性注入(完成依赖)

  • Spring 继续实例化依赖的 Bean,并从二级缓存获取已暴露的 Bean,完成注入。
  • 解决循环依赖。

(4)初始化完成(放入一级缓存)

  • Bean 初始化完成后,移除二级缓存的对象,放入一级缓存 singletonObjects
  • 这样,下次直接从一级缓存中取,避免重复创建。

为什么需要三级缓存?

  • 如果只有二级缓存(直接存放 Bean),A 和 B 都是未初始化状态,Spring 无法代理 A 或 B。
  • 三级缓存 可以让 Spring 提前暴露 Factory,创建代理对象,在 A 需要 B 时,可以拿到代理 B,而不是未初始化的 B。

@Autowired 的实现原理?

Spring 中的 @Autowired自动注入的核心注解,用于将依赖的 Bean 自动注入到目标对象中

  • 它的底层主要依赖 BeanPostProcessor 机制
  • 在 Bean 创建的不同阶段解析依赖并进行注入

@Autowired 的执行流程:

Spring 处理 @Autowired 主要涉及两个核心组件:

  1. AutowiredAnnotationBeanPostProcessor
    • 继承自 BeanPostProcessor,在 Bean 初始化前后处理依赖注入。
    • 解析 @Autowired 注解,找到需要注入的 Bean 并完成依赖注入。
  2. DefaultListableBeanFactory#resolveDependency
    • 负责根据 @Autowired 需求,在 Spring 容器中查找匹配的 Bean 并返回。

依赖查找逻辑:

  1. 按类型查找(默认按 byType 查找 Bean)。
  2. 按名称匹配(如果有 @Qualifier)。
  3. 如果存在多个匹配的 Bean:
    • 如果某个 Bean 标注 @Primary,优先注入。
    • 如果带 @Qualifier("beanName"),则按名称匹配。
    • 如果没有唯一的匹配,会报 NoUniqueBeanDefinitionException 异常。

Spring Bean

什么是 Bean?

简单来说,Bean 代指的就是那些被 IoC 容器所管理的对象

工作流程:

在应用程序运行时

Spring IoC 容器会根据 Bean 的定义和配置,创建对应的 Bean 对象,并对其进行初始化和依赖注入等操作。

一旦 Bean 对象被创建并注入了依赖关系,就可以被其他对象所引用和使用了。

在应用程序结束时

Spring IoC 容器会对 Bean 对象进行销毁操作,释放资源,从而完成 Bean 的生命周期。

将一个类声明为 Bean 的注解有哪些?

  • @Component:通用的注解,可标注任意类为 Spring 组件。

    如果一个 Bean 不知道属于哪个层,可以使用 @Component 注解标注。(config 配置层常用)

  • @Repository: 对应持久层即 Dao 层,主要用于数据库相关操作。

  • @Service : 对应服务层,主要涉及一些复杂的逻辑,需要用到 Dao 层。

  • @Controller : 对应 Spring MVC 控制层,主要用于接受用户请求并调用 Service 层返回数据给前端页面。

@Component 和 @Bean 的区别是什么?

  1. 使用方式不同

    • @Component 注解作用于

    • @Bean 注解作用于方法

  2. 装配方式不同

    • @Component 通常是通过类路径扫描来自动侦测以及自动装配到 Spring 容器中(我们可以使用 @ComponentScan 注解定义要扫描的路径从中找出标识了需要装配的类自动装配到 Spring 的 bean 容器中)。

    • @Bean 注解通常是我们在标有该注解的方法中定义产生这个 bean,@Bean告诉了 Spring 这是某个类的实例,当我需要用它的时候还给我。

      简单来说,就是告诉 Spring,这个方法会返回一个对象,需要被 Spring 管理,当需要使用这个对象的时候,就可以通过注入的方式来获取它。

  3. @Bean 注解比 @Component 注解的自定义性更强

    • 很多地方我们只能通过 @Bean 注解来注册 bean。比如当我们引用第三方库中的类需要装配到 Spring 容器时,则只能通过 @Bean 来实现

哪些注解可以用来注入 Bean?

有三种:

  • Spring 内置的 @Autowired
  • JDK 内置的 @Resource@Inject

@Autowired 和 @Resource 的区别是什么?

  1. @Autowired 是 Spring 提供的注解,@Resource 是 JDK 提供的注解。

  2. Autowired 默认的注入方式为byType(根据类型进行匹配),@Resource默认注入方式为 byName(根据名称进行匹配)。

  3. 当一个接口存在多个实现类的情况下,@Autowired@Resource 都需要通过名称才能正确匹配到对应的 Bean。

    • Autowired 可以通过 @Qualifier 注解来显式指定名称,

    • @Resource可以通过 name 属性来显式指定名称。

对比项@Autowired@Resource
来源Spring 提供的自动注入方式JDK javax.annotation 提供
注入方式默认按类型(byType)匹配默认按名称(byName)匹配
可与 @Qualifier 配合支持不支持 @Qualifier
是否支持 @Primary支持不支持

什么是 Bean 的作用域?

Bean 的作用域指的是:Bean 实例在容器中存在的范围。

通过配置 Bean 的作用域,可以控制在何种情况下容器会创建新的 Bean 实例,以及何时销毁 Bean 实例。

Bean 的作用域有哪些?

Spring 框架提供了以下 5 种作用域:

  1. singleton:单例模式,一个 Bean 在整个应用中只有一个实例(默认模式)。
  2. prototype:原型模式,每次请求获取 Bean 时,都会创建一个新的实例。
  3. request:请求作用域,每个 HTTP 请求都会创建一个新的实例。
  4. session:会话作用域,每个 HTTP 会话都会创建一个新的实例。
  5. application/global-session:全局会话作用域,每个 Web 应用在启动时创建一个 Bean(应用 Bean),该 bean 仅在当前应用启动时间内有效。

**注意:**作用域为 requestsessionglobal-session 的 Bean 只有在 Web 应用中才能使用。

配置方式

可以使用 @Scope 注解来指定 Bean 的作用域。

1、注解形式

@Configuration
public class AppConfig {
    @Bean(name = "userService")
    @Scope("singleton")
    public UserService userService() {
        return new UserServiceImpl();
    }
}

2、xml 形式

<bean id="userService" class="com.example.UserService" scope="singleton">
    <!-- ... -->
</bean>

单例 Bean 的线程安全问题

原因

单例 Bean 存在线程问题,主要是因为当多个线程操作同一个对象的时候存在资源竞争

有两种常见的解决方法

  1. 在 Bean 中尽量避免定义可变的成员变量
  2. 在类中定义一个 ThreadLocal 成员变量,将需要的可变成员变量保存在 ThreadLocal 中(推荐)。

无状态的 bean 是线程安全的

不过,大部分 Bean 实际都是无状态(没有实例变量)的(比如 Dao、Service),这种情况下,Bean 是线程安全的。

Bean 的生命周期了解么?

Bean 的生命周期可以分为以下几个阶段:

  1. 实例化:容器根据 Bean 的定义创建一个 Bean 实例。
  2. 填充属性:容器将 Bean 的属性赋值给相应的属性或构造函数参数。
  3. 初始化:容器会调用 Bean 的初始化方法,可以通过实现 InitializingBean 接口或在配置文件中指定 init-method 方法来定义 Bean 的初始化方法。
  4. 使用:容器将 Bean 实例化后,可以直接使用它了。
  5. 销毁:当容器关闭时,会调用 Bean 的销毁方法,可以通过实现 DisposableBean 接口或在配置文件中指定 destroy-method 方法来定义 Bean 的销毁方法。

Bean 的处理过程可以干扰吗?

可以通过改一些配置信息来进行干扰。

需要注意的点

如果 Bean 实现了 DisposableBean 接口或指定了 destroy-method 方法,容器会自动调用 Bean 的销毁方法。

但如果应用程序是非正常关闭的,如直接关闭 JVM 进程,容器就无法进行正常的销毁操作,这时需要通过注册钩子函数,在 JVM 关闭时手动调用销毁方法。

Spring AOP

谈谈自己对于 AOP 的了解

AOP(Aspect-Oriented Programming,面向切面编程)是一种编程范式,它通过将程序的横切关注点(如日志、事务、权限控制等)从业务逻辑中剥离出来,并通过切面与业务逻辑进行解耦,从而提高程序的模块化、可维护性和可扩展性。

其中核心是使用动态代理技术,在运行时生成代理对象,并将切面织入到目标对象的方法调用过程中。

实现方式

Spring 框架提供了两种方式来实现 AOP:

  1. 基于代理的 AOP:通过运行时生成代理对象并将切面织入到目标对象的方法调用过程中来实现 AOP。可以通过 JDK 动态代理(对象实现了某个接口的情况下使用)或者 CGLIB 动态代理来实现。
  2. 基于字节码的 AOP:通过在编译期间修改字节码来实现 AOP。可以使用 AspectJ 框架来实现。

AOP 切面编程设计到的 5 个专业术语

AOP 的核心概念是切面(Aspect)、连接点(Join Point)、切点(Pointcut)、通知(Advice)和织入(Weaving)。

  • 切面(Aspect):一个关注点的模块化,这个关注点跨越多个对象,通常解决的是与业务无关的问题。
  • 连接点(Join Point):程序执行过程中的某个特定点,如方法的调用、异常抛出等。
  • 切点(Pointcut):一组连接点的集合,通常使用表达式指定。
  • 通知(Advice):在切面的连接点上执行的动作,包括前置通知、后置通知、环绕通知、异常通知和最终通知等。
  • 织入(Weaving):将切面应用到目标对象并创建新的代理对象的过程。

优点

  1. 代码复用:将横切关注点分离出来,可以避免在业务逻辑中重复编写相同的代码。
  2. 可维护性:将横切关注点与业务逻辑分离,在修改横切关注点时不会影响业务逻辑的实现。
  3. 可扩展性:可以方便地添加新的切面,而不需要修改现有的业务逻辑代码。
  4. 提高程序的可读性:将横切关注点从业务逻辑中分离出来,可以使业务逻辑更加清晰明了。

Spring AOP 和 AspectJ AOP 有什么区别?

  1. AOP 集成了 AspectJ。
  2. AOP 基于代理(Proxying),而 AspectJ 基于字节码操作。
  3. AspectJ 的功能更加强大。
  4. 切面少,两者性能差异不大;切面太多的话,最好选择 AspectJ,会更快。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值