Spring Boot IOC详解


Spring IOC(Inversion of Control,控制反转)容器是 Spring 框架的核心组件之一,用于管理和组织应用程序中的对象(bean),它负责创建、配置和管理这些对象,以实现对象之间的解耦和依赖注入。

一、基本介绍

1. 主要功能

Spring IOC 容器的主要功能包括下述几点:

  • 实例化对象IOC 容器负责实例化应用程序中定义的 bean 对象。它根据配置文件或注解信息创建对象的实例。
  • 管理生命周期IOC 容器负责管理 bean 对象的生命周期,包括对象的创建、初始化和销毁。容器可以根据配置指定对象的初始化方法和销毁方法,确保对象在正确的时机被创建和销毁。
  • 依赖注入IOC 容器实现了依赖注入(DI)机制,它通过自动将对象的依赖注入到相应的位置,消除了手动编写代码进行依赖关系的管理。
  • 解耦和松耦合IOC 容器通过控制对象的创建和依赖注入,实现了对象之间的解耦。它使得对象的配置和使用可以独立于彼此进行修改,提高了代码的灵活性和可维护性。

2. 生命周期

Spring 中的 Bean 生命周期包括以下五个阶段:

  • 实例化(Instantiation): 这是 Bean 对象被创建的阶段。在这个阶段 Spring 使用 Bean 的构造方法来实例化对象,可以通过构造函数、静态工厂方法或者工厂 Bean 来创建实例。

  • 属性赋值(Population): 在这个阶段 Spring 将配置文件中或者注解中定义的属性值注入到 Bean 中。这包括基本类型、引用类型、集合等等,这一过程可以通过 XML 配置、Java 注解、JavaConfig 等方式进行。

  • 初始化(Initialization): 在这个阶段 Spring 会调用 Bean 的初始化方法。这个初始化方法可以是通过配置文件中的 init-method 属性指定,也可以是实现了 InitializingBean 接口的 afterPropertiesSet 方法。

  • 使用(In Use): 此时 Bean 对象已经被完全初始化,可以被应用程序使用了。

  • 销毁(Destruction): 这是 Bean 生命周期的最后阶段。在这个阶段 Spring 会调用 Bean 的销毁方法,该方法可以是通过配置文件中的 destroy-method 属性指定,也可以是实现了 DisposableBean 接口的 destroy 方法。

二、装配获取

Spring 提供了多个 IOC 容器实现,最常用的是基于 XML 配置的 ApplicationContext,此外还提供了 Java 配置类(如 @Configuration、@Bean 等)方式进行配置。

1. 装配方式

(Ⅰ) XML方式装配

首先先回顾一下传统 xml 配置文件装配 bean 对象,在工程 resources 下新建 spring-context.xml 文件,其对应的文件内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans ...>
    <bean id="testBean" class="xyz.ibudai.bean.User">
        <property name="id" value="123" />
        <property name="name" value="Alex" />
    </bean>
</beans>

完成后即可通过加载配置文件获取应用上下文 ApplicationContext 从而读取 Bean 实例。

// 测试类
public class Test {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("spring-context.xml");
        // 获取 Bean
        User user = (User) context.getBean("testBean");
        // User: {id=123, name=Alex}
        System.out.println(user.toString());
    }
}

(Ⅱ) 注解方式装配

Spring Boot 可配合 @Configuration@Bean 注解可达到同样效果。

(1) @Configuration
  • 作用于类上, 用于配置 Spring 容器应用上下文。
  • 作用效果等价于 xml 配置文件中的 beans 标签。
(2) @Bean
  • 作用于方法上,等价于 xml 配置文件中 bean 标签。
  • 通过 @Bean(name) 指定 bean 名称,未指定时为方法名(首字母自动小写)。
(3) @Scope
  • 通过 @Scope 定义 bean 作用域。
  • 作用于方法上,与 @Bean 搭配使用。
作用域 描述
singleton 默认值,在整个 IoC 容器中只存在一个共享的 bean 实例。
prototype 每次通过容器的 getBean() 方法获取 bean 时都会创建一个新的实例。
request 在一次 HTTP 请求中,该 bean 实例将保持活动状态。
session 在一个 HTTP Session 中,该 bean 实例将保持活动状态。
application 在 ServletContext 范围内,该 bean 实例将保持活动状态。

下述为通过注解配置类的方式装配 bean 对象,最终实现效果等价于上述 xml 方式。

// 开启配置
@Configuration
public class TestConfig {
    @Bean
    @Scope("prototype")
    public User testBean() {
        User user = new User();
        user.setId("123");
        user.setName("Alex");
        return user;
    }
}

// 测试类
public class Test {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(TestConfig.class);
        // 获取 Bean
        User user = (User) context.getBean("testBean");
        // User: {id=123, name=Alex}
        System.out.println(user.toString());
    }
}

2. 装配类型

Bean 的装配一共存在两种类型,分别为 ByNameByType

(Ⅰ) ByName(按名称注入)

  • 根据依赖的名称来查找与之匹配的 bean 对象进行注入。
  • ByName 要求依赖的名称在容器中是唯一的或者能够通过自动装配策略 (@Qualifier) 解决。
  • 通过名字查找与属性完全一致的 bean ,并将其与属性自动装配。
  • 若容器中找不到与依赖名称匹配的 bean 对象则会抛出异常;如果存在多个与依赖名称匹配的 bean 对象,会根据自动装配策略进行选择。

(Ⅱ) ByType(按类型注入)

  • 根据依赖的类型 (class) 来查找与之匹配的 bean 对象进行注入。
  • ByType 要求依赖的类型在容器中是唯一的或者明确指定了所需的 bean 对象。
  • 通过类型查找与属性完全一致的 bean ,并将其与属性自动装配。
  • 如果存在多个该类型 bean 则会抛出异常,并指出不能使用 byType 方式进行自动装配;如果没有匹配的 bean 对象,会将依赖设置为 null

3. 注入方式

Spring 在装配 bean 的时提供了两种方式,但需要注意无论通过哪种方式其对应的对象需要在 IOC 容器中存在,否则需要加上属性 required=false 表示忽略当前要注入的 bean ,否则程序将无法正常运行。

(1) @Resource

@Resource 是由 J2EE 本身提供的,注解默认通过 byName 方式注入。

当存在多个类型不同但名称相同的 bean 对象时此方式注入将会抛出异常。

(2) @Autowired

@Autowired 是由 Spring 提供的,注解默认通过 byType 方式注入。

@Autowired 默认读取与声明实例变量同名的 bean 对象,当工程中同时包含多个实例时必须指明实例名否则将会异常报错,可以通过 @Qualifier 指定实例名。

// 创建两个同类型实例
@Service("userService1")
public class UserServiceImpl1 implements UserService { }

@Service("userService2")
public class UserServiceImpl2 implements UserService { }


// 测试类
public class UserController {

    @Autowired
    private UserService userService1;
        
    @Autowired
    @Qualifier(value = "userService2")
    private UserService userService;

    // 声明非法,多个实例时必须指明 bean 名称
    @Autowired
    private UserService userService;
}

4. Bean获取

在上面介绍了 bean 的注入和装配, Spring 中同时也提供通过 bean 名称直接获取 bean 对象,通常搭配反射等特性使用,下面介绍两种的不同的 bean 对象获取方式。

(Ⅰ) 接口方式获取 Bean 实例

通过实现 ApplicationContextAware 接口从而获取应用上下文对象 ApplicationContext ,即可利用其实现 bean 对象获取。

下面是一个 bean 对象查询示例,注意类需要标注 @Component 注解。

@Component
public class BeanService implements ApplicationContextAware {

    private ApplicationContext applicationContext;

    /**
     * 获取上下文对象
     */
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    /**
     * 通过名称获取 bean 对象
     */
    public Object getBean(String name) throws BeansException {
        return applicationContext.getBean(name);
    }
}

(Ⅱ) 注解方式获取 Bean 实例

Spring 中同样提供了注解的方式获取应用上下文对象 ApplicationContext,无需上述那么复杂直接使用 @Autowire 注入即可,更推荐使用此类方式。

public class BeanService {
    /**
     * 通过装配获取上下文对象
     */
    @Autowired
    private ApplicationContext applicationContext;
        
    /**
     * 通过名称获取 bean 对象
     */
    public User getBean(String bean) {
        return (User) applicationContext.getBean(bean);
    }
}

三、Bean导入

1. 默认导入

Spring 中除了 @Bean 注解还提供 @Import 注入 bean 对象,其使用方式与前者类似,默认注入的 bean 对象名为类的完整限定民。

如下示例中即注入了一个 User 对象,若 User 类的完整包路径为:xyz.ibudai,则对应的 bean 名称为:xyz.ibudai.User

@Configuration
@Import(User.class)
public class ImportConfig {

}

完成上述操作后当启动项目将注册对应 Bean 对象,我们即可利用 ApplicationContext 查询获取该 Bean 实例。

@Service
public class UserService {

    @Autowired
    private ApplicationContext applicationContext;

    public Object getBean() {
        return applicationContext.getBean("xyz.ibudai.User");
    }
}

2. 手动导入

默认 @Import 注入 bean 对象属性值都为空且对象名为类的完整限定名,若想要配置更复杂信息则需要配合 ImportBeanDefinitionRegistrar 使用。

Spring Boot 工程启动时则会执行 ImportBeanDefinitionRegistrarregisterBeanDefinitions() 方法,顾名思义即执行 bean 对象的注入。其中方法的第一个参数 clsMetaData@Import 注解所作用的类的元信息,如上述示例中及为类 ImportConfig 的元数据对象,而第二参数为注册器。

下述示例中通过实现 ImportBeanDefinitionRegistrar 接口手动向 IOC 容器中注入了一个 Userbean 对象。

public class UserRegisterFactory implements ImportBeanDefinitionRegistrar {

    /**
     * @param clsMetaData annotation metadata of the importing class
     * @param registry    current bean definition registry
     */
    @Override
    public void registerBeanDefinitions(AnnotationMetadata clsMetaData, BeanDefinitionRegistry registry) {
        Class<User> aClass = User.class;
        if (!registry.containsBeanDefinition(aClass.getSimpleName())) {
            // If not contain then register it
            BeanDefinition beanDefinition = new RootBeanDefinition(aClass);
            registry.registerBeanDefinition(aClass.getSimpleName(), beanDefinition);
        }
    }
}

四、容器缓存

1. 二级缓存

Spring 中为了解决循环依赖的问题引入了二级缓存从而解决该问题。

首先我们先看一下循环依赖带来的问题,假设存在两个 Bean 实例,其中 BeanA 中依赖了 BeanB,而 BeanB 又依赖了 BeanA,代码描述如下:

public class BeanA {
    BeanB beanB;
}

public class BeanB {
    BeanA beanA;
}

二者对象的依赖关系图示如下:

在使用 IOC 容器注入 BeanA 实例时,当为其注入属性时由于其又依赖于 BeanB,因此需要先创建出 BeanB 实例,而 BeanB 又依赖于 BeanA 二者则陷入了死循环,这也是循环依赖所带来的问题。

那二级缓存又是如何解决如何这个问题的呢?让我们一步一步来拆解。

循环依赖问题在于属性注入阶段对象属性与自身互为成员变量,那么只需每次在在创建 bean 实例后将其存入一份至缓存中(Spring 中通过 Map 对象缓存),后续在执行属性注入时若需要依赖了直接读取缓存即可,避免了相互依赖导致的死循环。

2. 三级缓存

既然二级缓存已经解决了循环依赖的问题那为什么还需要引入三级缓存?三级缓存的引入主要是为了解决切面等动态代理生成的 bean 对象。

先看下图中两个流程,第一个为普通的 bean 实例创建流程,第二为包含动态代理等操作时的流程。

在二级缓存中讲过了缓存的存入时间在实例创建之后放入,而当包含动态代理时存入缓存的实例与最终存入 IOC 容器的对象显然不是同一个,这就造成了一个缓存不一致的原因,因此在此需要引入三级缓存从而解决缓存对象不一致。

五、Bean定义

IOC 容器中 Bean 对象存在两个十分重要的定义,即 BeanDefinitionHolderBeanDefinition,二者都是关于 Bean 元信息的类,它们的主要作用是描述和持有 Bean 的定义信息。

1. 对象定义

Spring 中每个注册到 IOC 容器中的 Bean 都有一个关联的 BeanDefinition 对象,该对象描述了如何创建和配置该 Bean。其定义了 Bean 的属性、依赖关系、作用域(scope)、初始化方法、销毁方法等配置信息。

BeanDefinition 中包含的主要方法即描述参考下标:

方法 作用
getBeanClassName() 获取 Bean 的类名。
getScope() 获取 Bean 的作用域。
isSingleton() 判断是否是单例。
isPrototype() 判断是否是原型。
getConstructorArgumentValues() 获取构造函数参数值。
getPropertyValues() 获取属性值。
getInitMethodName() 获取初始化方法名。
getDestroyMethodName() 获取销毁方法名。

2. 定义包装

BeanDefinitionHolder 是对 BeanDefinition 的包装,同时持有一个 String 类型的 beanName,通常在 Spring 容器中扫描、注册或管理 Bean 时使用。

Spring 中,通常在注册 Bean 定义时会使用 BeanDefinitionHolder,将 Bean 的定义信息和名称一并封装,然后一起注册到容器中,从而更方便管理和操作 Bean

六、动态导入

基于 @Import 的功能特性,即可实现动态的 Bean 注册,扫描指定包路径下的类并注册到 Spring 容器。

1. 注解定义

定义 @BeanScan 注解用于标注目标类包路径,以及 @BeanItem 注解标注需要被扫描的类。

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Import(BeanRegisterFactory.class)
public @interface BeanScan {
    String[] basePackages() default {};
}


@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface BeanItem {
    String value() default "";
}

2. 工程配置

在启动类上通过 @BeanScan 注解指定需要被扫描的包路径。

@SpringBootApplication
@BeanScan(basePackages = "xyz.ibudai.ioc")
public class BeanRegisterApplication {
    public static void main(String[] args) {
        SpringApplication.run(BeanRegisterApplication.class, args);
    }
}

3. 注册实现

创建 BeanRegisterFactory 用于执行具体的扫描注册逻辑,启动项目时获取 @BeanScan 配置的包路径。

获取目标包后通过 BeanScannerFactory 实现 bean 的扫描生成 BeanDefinitionHolder 对象,并通过 registry 实现自定义对象的注册。

public class BeanRegisterFactory implements ImportBeanDefinitionRegistrar {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata clsMetaData, BeanDefinitionRegistry registry) {
        // Get annotation attributes
        Map<String, Object> attrMaps = clsMetaData.getAnnotationAttributes(BeanScan.class.getName());
        // Convert attribute type to "AnnotationAttributes"
        AnnotationAttributes attributes = AnnotationAttributes.fromMap(attrMaps);
        if (attributes == null) {
            System.err.println(">>>>>>>>>>>>>>>>>>>>>>> Annotation attribute is null");
            return;
        }

        // Register annotation and Scan the package to find
        // the class that use the registered annotation.
        String[] basePackages = attributes.getStringArray("basePackages");
        BeanScannerFactory scannerFactory = new BeanScannerFactory(registry);
        scannerFactory.registerTypeFilter();
        scannerFactory.doScan(basePackages);
    }
}

4. Bean扫描

Spring 中通过 ClassPathBeanDefinitionScanner 实现 bean 的扫描注册,即通过扫描目标包将 bean 注册为上述的 BeanDefinitionHolder 对象从而实现管理。

ClassPathBeanDefinitionScanner 中涉及了两个相对重要的方法,其描述如下:

方法 作用
registerTypeFilter() 添加扫描过滤器,如只扫描某一部分特定类。
doScan() 根据过滤器扫描指定包下的类。

如下示例中即定义了只扫描类上包含 BeanItem 注解的类,其中 proxy() 方法为通过 FactoryBean 自定义自定义构建 BeanDefinitionHolder 对象,并通过 registerTypeFilter()方法筛选被 @BeanItem 注解标识的类。

public class BeanScannerFactory extends ClassPathBeanDefinitionScanner {

    public BeanScannerFactory(BeanDefinitionRegistry registry) {
        super(registry, false);
    }

    /**
     * Register the annotation that want scan
     */
    public void registerTypeFilter() {
        // Only scan the class the Annotation of "BeanItem"
        super.addIncludeFilter(new AnnotationTypeFilter(BeanItem.class));
    }

    /**
     * Scan the specify package path to find the bean.
     */
    @Override
    protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
        Set<BeanDefinitionHolder> beanDefinitionHolders = super.doScan(basePackages);
        beanDefinitionHolders = proxy(beanDefinitionHolders);
        return beanDefinitionHolders;
    }

    /**
     * Convert bean set to proxy bean set.
     */
    protected Set<BeanDefinitionHolder> proxy(Set<BeanDefinitionHolder> beanDefinitionHolders) {
        Set<BeanDefinitionHolder> holderSet = new HashSet<>(beanDefinitionHolders.size());
        for (BeanDefinitionHolder holder : beanDefinitionHolders) {
            GenericBeanDefinition beanDefinition = (GenericBeanDefinition) holder.getBeanDefinition();
            String beanName = holder.getBeanName();
            String beanClassName = beanDefinition.getBeanClassName();
            if (!StringUtils.hasLength(beanClassName)) {
                continue;
            }

            Class<?> aClass;
            try {
                aClass = Class.forName(beanClassName);
            } catch (ClassNotFoundException e) {
                continue;
            }
            // Transfer the parameter.
            // getConstructorArgumentValues(): 获取 beanDefinition 中构造函数参数值的方法
            // addGenericArgumentValue: 用于向 Bean 的构造函数参数中添加通用(泛型)参数值的方法。
            beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(aClass);
            // Set "BeanFactory" to use "dynamic proxy" instance the bean object
            beanDefinition.setBeanClass(MyFactoryBean.class);
            holderSet.add(new BeanDefinitionHolder(beanDefinition, beanName));
        }

        return holderSet;
    }
}

5. Bean工厂

通过扫描注册到 IOC 容器的仅为 BeanDefinitionHolder,即 bean 对象的相关元信息,只有在使用到 bean 对象的时候才会进行实例化。

而通过工厂类 FactoryBean 即可实现动态的对象生成等操作,如实现 AOP 切面与 RPC 远程服务调用等等。

如下示例中的 MyFactoryBean 即通过动态代理 BeanInvokeHandler 方式生成代理对象从而实现方法的调用信息打印实现切面的效果。

public class MyFactoryBean<T> implements FactoryBean<T> {

    private final Class<?> aClass;

    public MyFactoryBean(Class<?> aClass) {
        this.aClass = aClass;
    }

    @Override
    @SuppressWarnings("unchecked")
    public T getObject() throws Exception {
        Class<?> aInterface = aClass.getInterfaces()[0];
        Constructor<?> constructor = aClass.getDeclaredConstructor();
        Object instance = constructor.newInstance();
        BeanInvokeHandler handler = new BeanInvokeHandler(instance);
        return (T) Proxy.newProxyInstance(aClass.getClassLoader(), new Class[]{aInterface}, handler);
    }

    @Override
    public Class<?> getObjectType() {
        return aClass;
    }
}

/**
 * 动态代理处理器
 */
public class BeanInvokeHandler implements InvocationHandler {

    private final Logger logger = LoggerFactory.getLogger(getClass());

    private final Object object;

    public BeanInvokeHandler(Object object) {
        this.object = object;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        logger.error("Invoke method of [{}].", method.getName());
        Object result = method.invoke(this.object, args);
        logger.error("Invoke handle finish, [{}].", result);
        return result;
    }
}

参考链接

  1. 浅谈 Spring 如何解决 Bean 的循环依赖问题

文章作者: 烽火戏诸诸诸侯
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 烽火戏诸诸诸侯 !
  目录