Spring IoC
IoC(Inversion of Control:控制反转) 是一种设计思想,而不是一个具体的技术实现。IoC 的思想就是将原本在程序中手动创建对象的控制权,交由 Spring 框架来管理。不过, IoC 并非 Spring 特有,在其他语言中也有应用。
控制反转?
-
控制:指的是对象创建(实例化、管理)的权力
-
反转:控制权交到外部环境(Spring框架、IoC容器)
将对象之间的相互依赖关系交给 IoC 容器来管理,并由 IoC 容器完成对象的注入。这样可以很大程度上简化应用的开发,把应用从复杂的依赖关系中解放出来。 IoC 容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。
在实际项目中一个 Service 类可能依赖了很多其他的类,假如我们需要实例化这个 Service,你可能要每次都要搞清这个 Service 所有底层类的构造函数,这可能会把人逼疯。如果利用 IoC 的话,你只需要配置好,然后在需要的地方引用就行了,这大大增加了项目的可维护性且降低了开发难度。
在 Spring 中, IoC 容器是 Spring 用来实现 IoC 的载体, IoC 容器实际上就是个 Map(key,value),Map 中存放的是各种对象。
------------------------------------------
这个地方可以看一下其他文档:
IoC源码阅读、IoC&AOP详解
二、什么是Spring Bean?
简单来说,Bean 代指的就是那些被 IoC 容器所管理的对象。我们需要告诉 IoC 容器帮助我们管理哪些对象,这个是通过配置元数据来定义的。配置元数据可以是 XML 文件、注解或者 Java 配置类。
<!-- Constructor-arg with 'value' attribute -->
<bean id="..." class="...">
<constructor-arg value="..."/>
</bean>
下图简单地展示了 IoC 容器如何使用配置元数据来管理对象。
org.springframework.beans
和 org.springframework.context
这两个包是 IoC 实现的基础,如果想要研究 IoC 相关的源码的话,可以去看看
三、将一个类声明为Bean的注释有哪些
-
@Component
:通用的注解,可标注任意类为Spring
组件。如果一个 Bean 不知道属于哪个层,可以使用@Component
注解标注。 -
@Repository
: 对应持久层即 Dao 层,主要用于数据库相关操作。 -
@Service
: 对应服务层,主要涉及一些复杂的逻辑,需要用到 Dao 层。 -
@Controller
: 对应 Spring MVC 控制层,主要用于接受用户请求并调用Service
层返回数据给前端页面
四、@Component 和 @Bean 的区别是什么?
-
@Component
注解作用于类,而@Bean
注解作用于方法。 -
@Component
通常是通过类路径扫描来自动侦测以及自动装配到 Spring 容器中(我们可以使用@ComponentScan
注解定义要扫描的路径从中找出标识了需要装配的类自动装配到 Spring 的 bean 容器中)。@Bean
注解通常是我们在标有该注解的方法中定义产生这个 bean,@Bean
告诉了 Spring 这是某个类的实例,当我需要用它的时候还给我。 -
@Bean
注解比@Component
注解的自定义性更强,而且很多地方我们只能通过@Bean
注解来注册 bean。比如当我们引用第三方库中的类需要装配到Spring
容器时,则只能通过@Bean
来实现。
五、注入Bean的注解有哪些?
六、@Autowired 和 @Resource 的区别是什么?
@Autowired属于 Spring 内置的注解,默认的注入方式为
byType`(根据类型进行匹配),也就是说会优先根据接口类型去匹配并注入 Bean (接口的实现类)。
这会有什么问题呢? 当一个接口存在多个实现类的话,byType
这种方式就无法正确注入对象了,因为这个时候 Spring 会同时找到多个满足条件的选择,默认情况下它自己不知道选择哪一个。
这种情况下,注入方式会变为 byName
(根据名称进行匹配),这个名称通常就是类名(首字母小写)。就比如说下面代码中的 smsService
就是我这里所说的名称,这样应该比较好理解了吧。
我们还是建议通过 @Qualifier
注解来显式指定名称而不是依赖变量的名称。 @Resource
属于 JDK 提供的注解,默认注入方式为 byName
。如果无法通过名称匹配到对应的 Bean 的话,注入方式会变为byType
。 @Resource
有两个比较重要且日常开发常用的属性:name
(名称)、type
(类型)。
简单总结一下:
-
@Autowired
是 Spring 提供的注解,@Resource
是 JDK 提供的注解。 -
Autowired
默认的注入方式为byType
(根据类型进行匹配),@Resource
默认注入方式为byName
(根据名称进行匹配)。 -
当一个接口存在多个实现类的情况下,
@Autowired
和@Resource
都需要通过名称才能正确匹配到对应的 Bean。Autowired
可以通过@Qualifier
注解来显式指定名称,@Resource
可以通过name
属性来显式指定名称。 -
@Autowired
支持在构造函数、方法、字段和参数上使用。@Resource
主要用于字段和方法上的注入,不支持在构造函数或参数上使用。
七、注入Bean的方式有哪些
依赖注入 (Dependency Injection, DI) 的常见方式:
-
构造函数注入:通过类的构造函数来注入依赖项。
-
Setter 注入:通过类的 Setter 方法来注入依赖项。
-
Field(字段) 注入:直接在类的字段上使用注解(如
@Autowired
或@Resource
)来注入依赖项。
构造函数注入示例:
Setter 注入示例:
Field 注入示例:
构造函数注入还是 Setter 注入?
Spring 官方推荐构造函数注入,这种注入方式的优势如下:
-
依赖完整性:确保所有必需依赖在对象创建时就被注入,避免了空指针异常的风险。
-
不可变性:有助于创建不可变对象,提高了线程安全性。
-
初始化保证:组件在使用前已完全初始化,减少了潜在的错误。
-
测试便利性:在单元测试中,可以直接通过构造函数传入模拟的依赖项,而不必依赖 Spring 容器进行注入。
构造函数注入适合处理必需的依赖项,而 Setter 注入 则更适合可选的依赖项,这些依赖项可以有默认值或在对象生命周期中动态设置。虽然 @Autowired
可以用于 Setter 方法来处理必需的依赖项,但构造函数注入仍然是更好的选择。
在某些情况下(例如第三方类不提供 Setter 方法),构造函数注入可能是唯一的选择。
八、Bean的作用域有哪些
Spring 中 Bean 的作用域通常有下面几种:
-
singleton : IoC 容器中只有唯一的 bean 实例。Spring 中的 bean 默认都是单例的,是对单例设计模式的应用。
-
prototype : 每次获取都会创建一个新的 bean 实例。也就是说,连续
getBean()
两次,得到的是不同的 Bean 实例。 -
request (仅 Web 应用可用): 每一次 HTTP 请求都会产生一个新的 bean(请求 bean),该 bean 仅在当前 HTTP request 内有效。
-
session (仅 Web 应用可用) : 每一次来自新 session 的 HTTP 请求都会产生一个新的 bean(会话 bean),该 bean 仅在当前 HTTP session 内有效。
-
application/global-session (仅 Web 应用可用):每个 Web 应用在启动时创建一个 Bean(应用 Bean),该 bean 仅在当前应用启动时间内有效。
-
websocket (仅 Web 应用可用):每一次 WebSocket 会话产生一个新的 bean。
如何配置 bean 的作用域呢?
xml 方式:
<bean id="..." class="..." scope="singleton"></bean>
注解方式:
九、Bean 是线程安全的吗?
Spring 框架中的 Bean 是否线程安全,取决于其作用域和状态。
我们这里以最常用的两种作用域 prototype 和 singleton 为例介绍。几乎所有场景的 Bean 作用域都是使用默认的 singleton ,重点关注 singleton 作用域即可。
prototype 作用域下,每次获取都会创建一个新的 bean 实例,不存在资源竞争问题,所以不存在线程安全问题。singleton 作用域下,IoC 容器中只有唯一的 bean 实例,可能会存在资源竞争问题(取决于 Bean 是否有状态)。如果这个 bean 是有状态的话,那就存在线程安全问题(有状态 Bean 是指包含可变的成员变量的对象)。
有状态 Bean 示例:
// 定义了一个购物车类,其中包含一个保存用户的购物车里商品的 List
不过,大部分 Bean 实际都是无状态(没有定义可变的成员变量)的(比如 Dao、Service),这种情况下, Bean 是线程安全的。
无状态 Bean 示例:
// 定义了一个用户服务,它仅包含业务逻辑而不保存任何状态。 @Component public class UserService {
// 定义了一个用户服务,它仅包含业务逻辑而不保存任何状态。
对于有状态单例 Bean 的线程安全问题,常见的三种解决办法是:
-
避免可变成员变量: 尽量设计 Bean 为无状态。
-
使用
ThreadLocal
: 将可变成员变量保存在ThreadLocal
中,确保线程独立。 -
使用同步机制: 利用
synchronized
或ReentrantLock
来进行同步控制,确保线程安全。
这里以 ThreadLocal
为例,演示一下ThreadLocal
保存用户登录信息的场景:
public class UserThreadLocal { private UserThreadLocal() {} private static final ThreadLocal<SysUser> LOCAL = ThreadLocal.withInitial(() -> null); public static void put(SysUser sysUser) { LOCAL.set(sysUser); } public static SysUser get() { return LOCAL.get(); } public static void remove() { LOCAL.remove(); } }
十、Bean的生命周期?
-
创建 Bean 的实例:Bean 容器首先会找到配置文件中的 Bean 定义,然后使用 Java 反射 API 来创建 Bean 的实例。
-
Bean 属性赋值/填充:为 Bean 设置相关属性和依赖,例如
@Autowired
等注解注入的对象、@Value
注入的值、setter
方法或构造函数注入依赖和值、@Resource
注入的各种资源。 -
Bean 初始化:
-
如果 Bean 实现了
BeanNameAware
接口,调用setBeanName()
方法,传入 Bean 的名字。 -
如果 Bean 实现了
BeanClassLoaderAware
接口,调用setBeanClassLoader()
方法,传入ClassLoader
对象的实例。 -
如果 Bean 实现了
BeanFactoryAware
接口,调用setBeanFactory()
方法,传入BeanFactory
对象的实例。 -
与上面的类似,如果实现了其他
*.Aware
接口,就调用相应的方法。 -
如果有和加载这个 Bean 的 Spring 容器相关的
BeanPostProcessor
对象,执行postProcessBeforeInitialization()
方法 -
如果 Bean 实现了
InitializingBean
接口,执行afterPropertiesSet()
方法。 -
如果 Bean 在配置文件中的定义包含
init-method
属性,执行指定的方法。 -
如果有和加载这个 Bean 的 Spring 容器相关的
BeanPostProcessor
对象,执行postProcessAfterInitialization()
方法。
4.销毁 Bean:销毁并不是说要立马把 Bean 给销毁掉,而是把 Bean 的销毁方法先记录下来,将来需要销毁 Bean 或者销毁容器的时候,就调用这些方法去释放 Bean 所持有的资源。
-
如果 Bean 实现了
DisposableBean
接口,执行destroy()
方法。 -
如果 Bean 在配置文件中的定义包含
destroy-method
属性,执行指定的 Bean 销毁方法。或者,也可以直接通过@PreDestroy
注解标记 Bean 销毁之前执行的方法。
-
AbstractAutowireCapableBeanFactory
的 doCreateBean()
方法中能看到依次执行了这 4 个阶段:
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args) throws BeanCreationException { // 1. 创建 Bean 的实例 BeanWrapper instanceWrapper = null; if (instanceWrapper == null) { instanceWrapper = createBeanInstance(beanName, mbd, args); } Object exposedObject = bean; try { // 2. Bean 属性赋值/填充 populateBean(beanName, mbd, instanceWrapper); // 3. Bean 初始化 exposedObject = initializeBean(beanName, exposedObject, mbd); } // 4. 销毁 Bean-注册回调接口 try { registerDisposableBeanIfNecessary(beanName, bean, mbd); } return exposedObject; }
Aware
接口能让 Bean 能拿到 Spring 容器资源。
Spring 中提供的 Aware
接口主要有:
-
BeanNameAware
:注入当前 bean 对应 beanName; -
BeanClassLoaderAware
:注入加载当前 bean 的 ClassLoader; -
BeanFactoryAware
:注入当前BeanFactory
容器的引用。
BeanPostProcessor
接口是 Spring 为修改 Bean 提供的强大扩展点。
public interface BeanPostProcessor { // 初始化前置处理 default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { return bean; } // 初始化后置处理 default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { return bean; } }
-
postProcessBeforeInitialization
:Bean 实例化、属性注入完成后,InitializingBean#afterPropertiesSet
方法以及自定义的init-method
方法之前执行; -
postProcessAfterInitialization
:类似于上面,不过是在InitializingBean#afterPropertiesSet
方法以及自定义的init-method
方法之后执行。
InitializingBean
和 init-method
是 Spring 为 Bean 初始化提供的扩展点。
public interface InitializingBean { // 初始化逻辑 void afterPropertiesSet() throws Exception; }
指定 init-method
方法,指定初始化方法:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="demo" class="com.chaycao.Demo" init-method="init()"/> </beans>
如何记忆呢?
-
整体上可以简单分为四步:实例化 —> 属性赋值 —> 初始化 —> 销毁。
-
初始化这一步涉及到的步骤比较多,包含
Aware
接口的依赖注入、BeanPostProcessor
在初始化前后的处理以及InitializingBean
和init-method
的初始化操作。 -
销毁这一步会注册相关销毁回调接口,最后通过
DisposableBean
和destory-method
进行销毁。