06|再回首:如何实现一个IoC容器?

你好,我是郭屹。 第一阶段的学习完成啦,你是不是自己也实现出了一个简单可用的IoC容器呢?如果已经完成了,欢迎你把你的实现代码放到评论区,我们一起交流讨论。 我们这一章学的IoC(Inversion of Control)是我们整个MiniSpring框架的基石,也是框架中最核心的一个特性,为了让你更好地掌握这节课的内容,我们对这一整章的内容做一个重点回顾。

IoC重点回顾

IoC是面向对象编程里的一个重要原则,目的是从程序里移出原有的控制权,把控制权交给了容器。IoC容器是一个中心化的地方,负责管理对象,也就是Bean的创建、销毁、依赖注入等操作,让程序变得更加灵活、可扩展、易于维护。 在使用IoC容器时,我们需要先配置容器,包括注册需要管理的对象、配置对象之间的依赖关系以及对象的生命周期等。然后,IoC容器会根据这些配置来动态地创建对象,并把它们注入到需要它们的位置上。当我们使用IoC容器时,需要将对象的配置信息告诉IoC容器,这个过程叫做依赖注入(DI),而IoC容器就是实现依赖注入的工具。因此,理解IoC容器就是理解它是如何管理对象,如何实现DI的过程。 举个例子来说,我们有一个程序需要使用A对象,这个A对象依赖于一个B对象。我们可以把A对象和B对象的创建、配置工作都交给IoC容器来处理。这样,当程序需要使用A对象的时候,IoC容器会自动创建A对象,并将依赖的B对象注入到A对象中,最后返回给程序使用。

我们在课程中是如何一步步实现IoC容器的呢?

我们先是抽象出了Bean的定义,用一个XML进行配置,然后通过一个简单的Factory读取配置,创建bean的实例。这个极简容器只有一两个类,但是实现了bean的读取,这是原始的种子。 然后再扩展Bean,给Bean增加一些属性,如constructor、property和init-method。此时的属性值还是普通数据类型,没有对象。然后我们将属性值扩展到引用另一个Bean,实现依赖注入,同时解决了循环依赖问题。之后通过BeanPostProcessor机制让容器支持注解。 最后我们将BeanFactory扩展成一个体系,并增加应用上下文和容器事件侦听机制,完成一个完整的IoC容器。
图片
aaaaaaa## 原始IoC:如何通过BeanFactory实现原始版本的IoC容器? aaaaaaa### 思考题 IoC的字面含义是“控制反转”,那么它究竟“反转”了什么?又是怎么体现在代码中的? aaaaaaa### 参考答案 在传统的编程实践中,当程序需要使用某个对象时,通常会直接通过new关键字来实例化这个对象,并且需要手动处理该对象与其依赖对象之间的关系。这种做法导致了组件间的高耦合度,使得维护和扩展变得困难。 而IoC(Inversion of Control,控制反转)模式则改变了这一传统方式,它将对象的创建和管理从应用程序代码中转移到了一个专门的容器——IoC容器。具体来说,IoC主要反转了两个方面:

  1. 对象实例化的控制:不再是应用程序代码主动创建对象,而是由IoC容器负责对象的创建。这样,对象的具体实现可以更加灵活地替换或配置。

  2. 依赖关系的控制:对象不再自己寻找或者创建其依赖的对象,而是由IoC容器根据配置信息自动注入这些依赖。这种方式减少了硬编码的依赖,提高了代码的可测试性和灵活性。 在代码层面,IoC体现为依赖注入(Dependency Injection, DI)。例如,在Spring框架中,可以通过XML配置、注解或是Java配置类等方式定义bean及其依赖关系,然后由Spring IoC容器自动完成依赖的解析与注入过程。 通过这种方式,IoC极大地促进了软件设计中的松耦合原则,使系统更易于维护、测试及扩展。 aaaaaaa

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class UserController {
    private UserService userService; // 对象不用手动创建,由容器负责创建

    public void setUserService(UserService userService) { // 不用手动管理依赖关系,由容器注入
        this.userService = userService;
    }

    public void getUser() {
        userService.getUser();
    }
}

在Spring框架中,构造器注入和Setter注入是两种主要的依赖注入方式。它们都旨在减少对象间的耦合度,使代码更加灵活和易于维护。构造器注入通过类的构造函数来传递依赖对象,而Setter注入则通过类的setter方法来设置依赖对象。两者都可以用于注入复杂对象和多个依赖对象,但它们之间也存在一些差异。构造器注入的主要优点是可以在对象创建时立即初始化所有必需的依赖关系,确保了对象在创建后立即处于完整状态。此外,由于依赖关系是通过构造器传递的,因此这些依赖关系是强制性的,这有助于避免部分初始化的对象状态。然而,构造器注入的缺点是,如果依赖对象较多,会导致构造函数参数列表过长,难以管理。相比之下,Setter注入提供了更大的灵活性,因为它允许在对象创建之后的任何时间点设置依赖关系。这使得它更适合于可选的依赖关系或在运行时可能发生变化的依赖关系。但是,Setter注入的缺点是,它可能导致对象在一段时间内处于不一致的状态,因为依赖关系可能在对象完全初始化之前就已经设置。总的来说,选择哪种注入方式取决于具体的应用场景和设计需求。
图片

两者之间的优劣,人们有不同的观点,存在持久的争议。Spring团队本身的观点也在变,早期版本他们推荐使用Setter注入,Spring5之后推荐使用构造器注入。当然,我们跟随Spring团队,现在也是建议用构造器注入。

03|依赖注入:如何给Bean注入值并解决循环依赖问题?

思考题

你认为能不能在一个Bean的构造器中注入另一个Bean?

参考答案

可以在一个Bean的构造器中注入另一个Bean。具体的做法就是通过构造器注入或者通过构造器注解方式注入。

方式一:构造器注入

在一个Bean的构造器中注入另一个Bean,可以使用构造器注入的方式。例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class ABean {
    private final BBean Bbean;

    public ABean(BeanB Bbean) {
        this.Bbean = Bbean;
    }

    // ...
}

public class BBean {
    // ...
}

可以看到,上述代码中的 ABean 类的构造器使用了 BBean 类的实例作为参数进行构造的方式,通过这样的方式可以将 BBean 实例注入到 ABean 中。

方式二:构造器注解方式注入

在Spring中,我们也可以通过在Bean的构造器上增加注解来注入另一个Bean,例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class ABean {
    private final BBean Bbean;

    @Autowired
    public ABean(BBean Bbean) {
        this.Bbean = Bbean;
    }

    // ...
}

public class BBean {
    // ...
}

在上述代码中,ABean 中的构造器使用了 @Autowired 注解,这个注解可以将 BBean 注入到 ABean 中。

通过这两种方式,我们都可以在一个Bean的构造器中注入另一个Bean,需要根据具体情况来选择合适的方式。通常情况下,通过构造器注入是更优的选择,可以确保依赖项的完全初始化,避免对象状态的污染。

对MiniSpring来讲,只需要做一点改造,在用反射调用Constructor的过程中处理参数的时候增加Bean类型的判断,然后对这个构造器参数再调用一次getBean()就可以了。

当然,我们要注意了。构造器注入是在Bean实例化过程中起作用的,一个Bean没有实例化完成的时候就去实例化另一个Bean,这个时候连“早期的毛胚Bean”都没有,因此解决不了循环依赖的问题。

04|增强IoC容器:如何让我们的Spring支持注解?

思考题

我们实现了Autowired注解,在现有框架中能否支持多个注解?

参考答案

如果这些注解是不同作用的,那么在现有架构中是可以支持多个注解并存的。比如要给某个属性上添加一个@Require注解,表示这个属性不能为空,我们来看下实现的思路。

MiniSpring中,对注解的解释是通过BeanPostProcessor来完成的。我们增加一个RequireAnnotationBeanPostProcessor类,在它的postProcessAfterInitialization()方法中解释这个注解,判断是不是为空,如果为空则抛出BeanException。

然后改写ClassPathXmlApplicationContext类中的registerBeanPostProcessors()方法,将这个新定义的beanpostprocessor注册进去。

1
2
beanFactory.addBeanPostProcessor(new
RequireAnnotationBeanPostProcessor());

多线程环境下的单例模式管理Bean的线程安全问题及解决方案

在多线程环境中,单例模式管理的Bean可能会面临线程安全问题。以下是一些解决方案:

1. 避免共享数据

  • 在Bean中尽量使用局部变量而非成员变量。

  • 确保方法中不修改成员变量,以减少数据共享。

2. 使用线程安全的数据结构

  • 推荐使用线程安全的数据结构,例如使用ConcurrentHashMap代替HashMap

  • 也可以使用CopyOnWriteArrayList代替ArrayList等。

3. 同步机制

  • 对于需要操作共享数据的场景,可以通过synchronized关键字实现同步。

  • 也可以使用更高级的同步机制,如ReentrantLockReadWriteLock等。 注意: 使用同步机制可能会影响系统性能,并可能引发死锁,因此需要谨慎使用。

4. 使用ThreadLocal

  • 当需要在多线程环境下共享数据且保证线程安全时,可以使用ThreadLocal

  • ThreadLocal确保每个线程拥有独立的数据副本,避免数据竞争。 综上所述,为确保单例模式下Bean的线程安全性,应避免共享数据,选用线程安全的数据结构,正确使用同步机制,并在必要时使用ThreadLocal