Spring IoC

一、Spring IoC的理解

  IoC(Inversion of Control:控制反转) 是一种设计思想,而不是一个具体的技术实现。IoC 的思想就是将原本在程序中手动创建对象的控制权,交由 Spring 框架来管理。不过, IoC 并非 Spring 特有,在其他语言中也有应用。

控制反转?

  • 控制:指的是对象创建(实例化、管理)的权力

  • 反转:控制权交到外部环境(Spring框架、IoC容器)

img

  将对象之间的相互依赖关系交给 IoC 容器来管理,并由 IoC 容器完成对象的注入。这样可以很大程度上简化应用的开发,把应用从复杂的依赖关系中解放出来。 IoC 容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。

  在实际项目中一个 Service 类可能依赖了很多其他的类,假如我们需要实例化这个 Service,你可能要每次都要搞清这个 Service 所有底层类的构造函数,这可能会把人逼疯。如果利用 IoC 的话,你只需要配置好,然后在需要的地方引用就行了,这大大增加了项目的可维护性且降低了开发难度。

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

  Spring 时代我们一般通过 XML 文件来配置 Bean,后来开发人员觉得 XML 文件来配置不太好,于是 SpringBoot 注解配置就慢慢开始流行起来。

 

------------------------------------------  

这个地方可以看一下其他文档:

IoC源码阅读、IoC&AOP详解

 

二、什么是Spring Bean?

 简单来说,Bean 代指的就是那些被 IoC 容器所管理的对象。我们需要告诉 IoC 容器帮助我们管理哪些对象,这个是通过配置元数据来定义的。配置元数据可以是 XML 文件、注解或者 Java 配置类。

 <!-- Constructor-arg with 'value' attribute -->
 <bean id="..." class="...">
    <constructor-arg value="..."/>
 </bean>

下图简单地展示了 IoC 容器如何使用配置元数据来管理对象。

img

org.springframework.beansorg.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来实现。

img

img

 

img

 

五、注入Bean的注解有哪些?

img

 

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

@Autowired属于 Spring 内置的注解,默认的注入方式为byType`(根据类型进行匹配),也就是说会优先根据接口类型去匹配并注入 Bean (接口的实现类)。

这会有什么问题呢? 当一个接口存在多个实现类的话,byType这种方式就无法正确注入对象了,因为这个时候 Spring 会同时找到多个满足条件的选择,默认情况下它自己不知道选择哪一个。

这种情况下,注入方式会变为 byName(根据名称进行匹配),这个名称通常就是类名(首字母小写)。就比如说下面代码中的 smsService 就是我这里所说的名称,这样应该比较好理解了吧。

img

 

我们还是建议通过 @Qualifier 注解来显式指定名称而不是依赖变量的名称。 @Resource属于 JDK 提供的注解,默认注入方式为 byName。如果无法通过名称匹配到对应的 Bean 的话,注入方式会变为byType @Resource 有两个比较重要且日常开发常用的属性:name(名称)、type(类型)。

img

简单总结一下:

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

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

  • 当一个接口存在多个实现类的情况下,@Autowired@Resource都需要通过名称才能正确匹配到对应的 Bean。Autowired 可以通过 @Qualifier 注解来显式指定名称,@Resource可以通过 name 属性来显式指定名称。

  • @Autowired 支持在构造函数、方法、字段和参数上使用。@Resource 主要用于字段和方法上的注入,不支持在构造函数或参数上使用。

 

七、注入Bean的方式有哪些

依赖注入 (Dependency Injection, DI) 的常见方式:

  1. 构造函数注入:通过类的构造函数来注入依赖项。

  2. Setter 注入:通过类的 Setter 方法来注入依赖项。

  3. Field(字段) 注入:直接在类的字段上使用注解(如 @Autowired@Resource)来注入依赖项。

构造函数注入示例:

 @Service
 public class UserService {
 
     private final UserRepository userRepository;
 
     public UserService(UserRepository userRepository) {
         this.userRepository = userRepository;
    }
 
     //...
 }

 

Setter 注入示例:

 @Service
 public class UserService {
 
     private UserRepository userRepository;
 
     // 在 Spring 4.3 及以后的版本,特定情况下 @Autowired 可以省略不写
     @Autowired
     public void setUserRepository(UserRepository userRepository) {
         this.userRepository = userRepository;
    }
 
     //...
 }

 

Field 注入示例:

 @Service
 public class UserService {
 
     @Autowired
     private UserRepository userRepository;
 
     //...
 }

 

构造函数注入还是 Setter 注入?

Spring 官方推荐构造函数注入,这种注入方式的优势如下:

  1. 依赖完整性:确保所有必需依赖在对象创建时就被注入,避免了空指针异常的风险。

  2. 不可变性:有助于创建不可变对象,提高了线程安全性。

  3. 初始化保证:组件在使用前已完全初始化,减少了潜在的错误。

  4. 测试便利性:在单元测试中,可以直接通过构造函数传入模拟的依赖项,而不必依赖 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
 @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
 public Person personPrototype() {
     return new Person();
 }

 

九、Bean 是线程安全的吗?

Spring 框架中的 Bean 是否线程安全,取决于其作用域和状态。

我们这里以最常用的两种作用域 prototype 和 singleton 为例介绍。几乎所有场景的 Bean 作用域都是使用默认的 singleton ,重点关注 singleton 作用域即可。

prototype 作用域下,每次获取都会创建一个新的 bean 实例,不存在资源竞争问题,所以不存在线程安全问题。singleton 作用域下,IoC 容器中只有唯一的 bean 实例,可能会存在资源竞争问题(取决于 Bean 是否有状态)。如果这个 bean 是有状态的话,那就存在线程安全问题(有状态 Bean 是指包含可变的成员变量的对象)。

 

有状态 Bean 示例:

 // 定义了一个购物车类,其中包含一个保存用户的购物车里商品的 List
 @Component
 public class ShoppingCart {
     private List<String> items = new ArrayList<>();
 
     public void addItem(String item) {
         items.add(item);
    }
 
     public List<String> getItems() {
         return items;
    }
 }

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

无状态 Bean 示例:

// 定义了一个用户服务,它仅包含业务逻辑而不保存任何状态。 @Component public class UserService {

 // 定义了一个用户服务,它仅包含业务逻辑而不保存任何状态。
 @Component
 public class UserService {
 
     public User findUserById(Long id) {
         //...
    }
     //...
 }

对于有状态单例 Bean 的线程安全问题,常见的三种解决办法是:

  1. 避免可变成员变量: 尽量设计 Bean 为无状态。

  2. 使用ThreadLocal: 将可变成员变量保存在 ThreadLocal 中,确保线程独立。

  3. 使用同步机制: 利用 synchronizedReentrantLock 来进行同步控制,确保线程安全。

这里以 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的生命周期?

  1. 创建 Bean 的实例:Bean 容器首先会找到配置文件中的 Bean 定义,然后使用 Java 反射 API 来创建 Bean 的实例。

  2. Bean 属性赋值/填充:为 Bean 设置相关属性和依赖,例如@Autowired 等注解注入的对象、@Value 注入的值、setter方法或构造函数注入依赖和值、@Resource注入的各种资源。

  3. 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 销毁之前执行的方法。

     

AbstractAutowireCapableBeanFactorydoCreateBean() 方法中能看到依次执行了这 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 接口主要有:

  1. BeanNameAware:注入当前 bean 对应 beanName;

  2. BeanClassLoaderAware:注入加载当前 bean 的 ClassLoader;

  3. 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 方法之后执行。

InitializingBeaninit-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>
如何记忆呢?
  1. 整体上可以简单分为四步:实例化 —> 属性赋值 —> 初始化 —> 销毁。

  2. 初始化这一步涉及到的步骤比较多,包含 Aware 接口的依赖注入、BeanPostProcessor 在初始化前后的处理以及 InitializingBeaninit-method 的初始化操作。

  3. 销毁这一步会注册相关销毁回调接口,最后通过DisposableBeandestory-method 进行销毁。

img

 

posted @ 2024-10-19 15:48  墨羽寻觅  阅读(28)  评论(0编辑  收藏  举报