聊聊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 中存放的是各种对象。
工作流程:
- IoC 容器实例化对象并存储起来,
- 函数(比如
main
)需要哪个对象就往 IoC 容器拿就好。
简单来说,就是起到了一个中间站的作用。
总结
Spring IoC 是 Spring 框架的核心特性之一。
它通过 IoC 容器来管理对象,实现了控制反转和依赖注入。
在 Spring 中,IoC 容器负责管理对象的创建和依赖关系的维护,应用程序只需要通过 IoC 容器获取所需的对象即可。
在出现 IOC 之前,项目中是如何使用 Bean 的?
在 Spring 框架的出现之前,项目中通常使用【手动创建】和【管理对象】的方式来使用 "bean",尤其是在 JavaEE(现在称为 Jakarta EE)开发中。下面是在出现 IOC(控制反转)和 Spring 之前,项目中如何使用 "bean" 的一些常见做法:
- 手动创建对象: 在没有 Spring 框架的情况下,开发人员需要手动实例化和管理对象。这意味着你需要在代码中直接使用
new
关键字来创建对象,然后自行处理对象的生命周期。(例如 new 对象,结合 get 方法使用) - 工厂模式: 开发人员可能会使用工厂模式来创建对象。这涉及创建一个工厂类,其中包含创建和管理对象的逻辑。这种方式可以将对象的创建和使用分离开,但仍然需要手动管理对象的生命周期。
- 单例模式: 在很多情况下,开发人员会使用单例模式来确保系统中只有一个实例。这可以通过在类中添加一个私有构造函数和一个静态方法来实现。但这仍然需要手动管理单例实例的创建和使用。
- 依赖关系管理: 在没有 IOC 容器的情况下,开发人员需要手动处理对象之间的依赖关系。这可能会导致代码中充斥着对象之间的创建和注入逻辑。
- 手动资源释放: 在对象使用完毕后,开发人员需要手动释放资源,比如关闭数据库连接、释放文件句柄等。
总之,在没有 IOC 和 Spring 框架之前,项目的代码可能会更加繁琐和冗长,需要开发人员手动管理对象的创建、注入和生命周期。Spring 框架的出现极大地简化了这些过程,通过 IOC 容器和依赖注入,让开发人员更专注于业务逻辑,而无需过多关注对象的创建和管理。
# 说说循环依赖
# 什么是循环依赖?
Spring 循环依赖:简单说就是自己依赖自己,或者和别的 Bean 相互依赖。
只有单例的 Bean 才存在循环依赖的情况,原型(Prototype)情况下,Spring 会直接抛出异常。原因很简单,AB 循环依赖,A 实例化的时候,发现依赖 B,创建 B 实例,创建 B 的时候发现需要 A,创建 A1 实例······无限套娃,直接把系统干垮。
# Spring 可以解决哪些情况的循环依赖?
当循环依赖的实例都采用 setter 方法注入的时候,Spring 可以支持,
都采用构造器注入的时候,不支持,
构造器注入和 setter 注入同时存在的时候,看天。
原因是 Spring 在创建 Bean 时默认会根据自然排序进行创建。比如 A 会先于 B 进行创建。
# 那 Spring 怎么解决循环依赖的呢?
# @Autowired 的实现原理?
# 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 的区别是什么?
使用方式不同
@Component
注解作用于类,而
@Bean
注解作用于方法。
装配方式不同
@Component
通常是通过类路径扫描来自动侦测以及自动装配到 Spring 容器中(我们可以使用@ComponentScan
注解定义要扫描的路径从中找出标识了需要装配的类自动装配到 Spring 的 bean 容器中)。@Bean
注解通常是我们在标有该注解的方法中定义产生这个 bean,@Bean
告诉了 Spring 这是某个类的实例,当我需要用它的时候还给我。简单来说,就是告诉 Spring,这个方法会返回一个对象,需要被 Spring 管理,当需要使用这个对象的时候,就可以通过注入的方式来获取它。
@Bean
注解比@Component
注解的自定义性更强- 很多地方我们只能通过
@Bean
注解来注册 bean。比如当我们引用第三方库中的类需要装配到Spring
容器时,则只能通过@Bean
来实现。
- 很多地方我们只能通过
# 哪些注解可以用来注入 Bean?
有三种:
- Spring 内置的
@Autowired
- JDK 内置的
@Resource
和@Inject
# @Autowired 和 @Resource 的区别是什么?
@Autowired
是 Spring 提供的注解,@Resource
是 JDK 提供的注解。Autowired
默认的注入方式为byType
(根据类型进行匹配),@Resource
默认注入方式为byName
(根据名称进行匹配)。当一个接口存在多个实现类的情况下,
@Autowired
和@Resource
都需要通过名称才能正确匹配到对应的 Bean。Autowired
可以通过@Qualifier
注解来显式指定名称,@Resource
可以通过name
属性来显式指定名称。
# 什么是 Bean 的作用域?
Bean 的作用域指的是:Bean 实例在容器中存在的范围。
通过配置 Bean 的作用域,可以控制在何种情况下容器会创建新的 Bean 实例,以及何时销毁 Bean 实例。
# Bean 的作用域有哪些?
Spring 框架提供了以下 5 种作用域:
singleton
:单例模式,一个 Bean 在整个应用中只有一个实例(默认模式)。prototype
:原型模式,每次请求获取 Bean 时,都会创建一个新的实例。request
:请求作用域,每个 HTTP 请求都会创建一个新的实例。session
:会话作用域,每个 HTTP 会话都会创建一个新的实例。application/global-session
:全局会话作用域,每个 Web 应用在启动时创建一个 Bean(应用 Bean),该 bean 仅在当前应用启动时间内有效。
**注意:**作用域为 request
、session
和 global-session
的 Bean 只有在 Web 应用中才能使用。
配置方式
可以使用 @Scope
注解来指定 Bean 的作用域。
1、注解形式
@Configuration
public class AppConfig {
@Bean(name = "userService")
@Scope("singleton")
public UserService userService() {
return new UserServiceImpl();
}
}
2
3
4
5
6
7
8
2、xml 形式
<bean id="userService" class="com.example.UserService" scope="singleton">
<!-- ... -->
</bean>
2
3
# 单例 Bean 的线程安全问题
原因
单例 Bean 存在线程问题,主要是因为当多个线程操作同一个对象的时候存在资源竞争。
有两种常见的解决方法
- 在 Bean 中尽量避免定义可变的成员变量。
- 在类中定义一个
ThreadLocal
成员变量,将需要的可变成员变量保存在ThreadLocal
中(推荐)。
无状态的 bean 是线程安全的
不过,大部分 Bean 实际都是无状态(没有实例变量)的(比如 Dao、Service),这种情况下,Bean 是线程安全的。
# Bean 的生命周期了解么?
Bean 的生命周期可以分为以下几个阶段:
- 实例化:容器根据 Bean 的定义创建一个 Bean 实例。
- 填充属性:容器将 Bean 的属性赋值给相应的属性或构造函数参数。
- 初始化:容器会调用 Bean 的初始化方法,可以通过实现 InitializingBean 接口或在配置文件中指定 init-method 方法来定义 Bean 的初始化方法。
- 使用:容器将 Bean 实例化后,可以直接使用它了。
- 销毁:当容器关闭时,会调用 Bean 的销毁方法,可以通过实现 DisposableBean 接口或在配置文件中指定 destroy-method 方法来定义 Bean 的销毁方法。
Bean 的处理过程可以干扰吗?
可以通过改一些配置信息来进行干扰。
需要注意的点
如果 Bean 实现了 DisposableBean 接口或指定了 destroy-method 方法,容器会自动调用 Bean 的销毁方法。
但如果应用程序是非正常关闭的,如直接关闭 JVM 进程,容器就无法进行正常的销毁操作,这时需要通过注册钩子函数,在 JVM 关闭时手动调用销毁方法。
# Spring AOP
# 谈谈自己对于 AOP 的了解
AOP(Aspect-Oriented Programming,面向切面编程)是一种编程范式,它通过将程序的横切关注点(如日志、事务、权限控制等)从业务逻辑中剥离出来,并通过切面与业务逻辑进行解耦,从而提高程序的模块化、可维护性和可扩展性。
其中核心是使用动态代理技术,在运行时生成代理对象,并将切面织入到目标对象的方法调用过程中。
实现方式
Spring 框架提供了两种方式来实现 AOP:
- 基于代理的 AOP:通过运行时生成代理对象并将切面织入到目标对象的方法调用过程中来实现 AOP。可以通过
JDK
动态代理(对象实现了某个接口的情况下使用)或者CGLIB
动态代理来实现。 - 基于字节码的 AOP:通过在编译期间修改字节码来实现 AOP。可以使用
AspectJ
框架来实现。
AOP 切面编程设计到的 5 个专业术语
AOP 的核心概念是切面(Aspect)、连接点(Join Point)、切点(Pointcut)、通知(Advice)和织入(Weaving)。
- 切面(Aspect):一个关注点的模块化,这个关注点跨越多个对象,通常解决的是与业务无关的问题。
- 连接点(Join Point):程序执行过程中的某个特定点,如方法的调用、异常抛出等。
- 切点(Pointcut):一组连接点的集合,通常使用表达式指定。
- 通知(Advice):在切面的连接点上执行的动作,包括前置通知、后置通知、环绕通知、异常通知和最终通知等。
- 织入(Weaving):将切面应用到目标对象并创建新的代理对象的过程。
优点
- 代码复用:将横切关注点分离出来,可以避免在业务逻辑中重复编写相同的代码。
- 可维护性:将横切关注点与业务逻辑分离,在修改横切关注点时不会影响业务逻辑的实现。
- 可扩展性:可以方便地添加新的切面,而不需要修改现有的业务逻辑代码。
- 提高程序的可读性:将横切关注点从业务逻辑中分离出来,可以使业务逻辑更加清晰明了。
# Spring AOP 和 AspectJ AOP 有什么区别?
- AOP 集成了 AspectJ。
- AOP 基于代理(Proxying),而 AspectJ 基于字节码操作。
- AspectJ 的功能更加强大。
- 切面少,两者性能差异不大;切面太多的话,最好选择 AspectJ,会更快。