01|原始IoC:如何通过BeanFactory实现原始版本的IoC容器?

你好,我是郭屹,从今天开始我们来学习手写MiniSpring。 这一章,我们将从一个最简单的程序开始,一步步堆积演化,最后实现Spring这一庞大框架的核心部分。这节课,我们就来构造第一个程序,也是最简单的一个程序,将最原始的IoC概念融入我们的框架之中, 我们就用这个原始的IoC容器来管理一个Bean。 不过要说的是,它虽然原始,却也是一个可以运行的IoC容器。

IoC容器

如果你使用过Spring或者了解Spring框架,肯定会对IoC容器有所耳闻。它的意思是使用Bean容器管理一个个的Bean,最简单的Bean就是一个Java的业务对象。在Java中,创建一个对象最简单的方法就是使用 new 关键字。IoC容器,也就是 BeanFactory,存在的意义就是将创建对象与使用对象的业务代码解耦,让业务开发人员无需关注底层对象(Bean)的构建和生命周期管理,专注于业务开发。 那我们可以先想一想,怎样实现Bean的管理呢?我建议你不要直接去参考Spring的实现,那是大树长成之后的模样,复杂而庞大,令人生畏。 作为一颗种子,它其实可以非常原始、非常简单。实际上我们只需要几个简单的部件:我们用一个部件来对应Bean内存的映像,一个定义在外面的Bean在内存中总是需要有一个映像的;一个XML reader 负责从外部XML文件获取Bean的配置,也就是说这些Bean是怎么声明的,我们可以写在一个外部文件里,然后我们用XML reader从外部文件中读取进来;我们还需要一个反射部件,负责加载Bean Class并且创建这个实例;创建实例之后,我们用一个Map来保存Bean的实例;最后我们提供一个getBean() 方法供外部使用。我们这个IoC容器就做好了。 注意:请根据上面内容,做到下面操作 1.根据内容重新编写内容,把新写的内容放到字段content,内容要有条理性,有结构性。内容使用markdown格式输出。content 中的 换成

图片

实现一个原始版本的IoC容器

目标

实现一个简单的IoC容器,管理Bean的两个属性:id和class。id用于给Bean一个别名,class表示要注入的类。Bean通过XML配置文件注入到框架中。

Bean属性

  • id:Bean的别名,用于简化记忆成本。

  • class:要注入的类的名称。

XML配置示例

下面是一个XML配置文件的示例,展示了如何定义Bean。

1
2
3
4
<beans>
  <bean id='aaaaaaa1' class='com.example.ExampleClass1'/>
  <bean id='aaaaaaa2' class='com.example.ExampleClass2'/>
</beans>

步骤

  1. 解析XML配置文件:读取XML文件,解析出其中的bean标签,获取id和class属性。

  2. 创建Bean实例:根据解析出的class属性,创建对应的类实例。

  3. 注册Bean:将创建的Bean实例注册到一个Map中,以id为键,Bean实例为值。

  4. 使用Bean:通过id获取对应的Bean实例,进行后续操作。

注意事项

  • 确保XML配置文件格式正确,id和class属性不为空。

  • 类实例化时,需要处理可能的异常,例如类找不到或实例化失败。

  • 考虑到后续可能的扩展,代码应保持一定的灵活性和可维护性。

1
2
3
4
<?xml version="1.0" encoding="UTF-8" ?>
<beans>
    <bean id = "xxxid" class = "com.minis.xxxclass"></bean>
</beans>

在准备阶段,我们需要创建一个新的Java项目,并导入dom4j-1.6.1.jar包。dom4j是一个库,它封装了多种操作XML文件的方法,这将简化我们处理XML文件中的属性的工作,并且为我们后续处理基于XML注入的Bean提供了便利。 尽管我们的目标是学习Spring框架,我们会尽量减少对第三方库的依赖,并通过手动编写代码来实现功能,以更深入地理解底层原理。我们鼓励你亲自动手实践,因为编程是一项技能,提高它的唯一途径就是通过实践。通过不断学习、思考和实践,你的编程技能将得到显著提升。

构建BeanDefinition

创建Java项目后,我们将在项目中创建一个名为com.minis的包,所有相关的程序代码都将放在这个包下。在这个包中,我们将创建第一个类,用于定义Bean,命名为BeanDefinition。在这个类中,我们将定义两个基本的属性:id和className。以下是BeanDefinition类的代码示例。

1
2
3
4
5
6
7
8
public class BeanDefinition {
    private String id;
    private String className;
    public BeanDefinition(String id, String className) {
        this.id = id;
        this.className = className;
    }
    //省略getter和setter

在面向对象的编程中,为了更好地封装数据和行为,我们通常会创建包含属性、构造方法以及getter和setter方法的类。对于一个简单的Java Bean来说,这意味着提供一个全参数的构造方法来初始化对象状态,并且为每个属性提供访问器(getter)和修改器(setter)方法,以允许外部代码读取或更改这些属性的值。

实现ClassPathXmlApplicationContext类

当涉及到通过Spring框架进行依赖注入时,XML配置文件是一个常见的选择,它定义了应用程序上下文中Bean的配置信息。要解析这样的XML配置并根据其内容初始化Bean实例,我们需要实现ClassPathXmlApplicationContext类。这个类的名字已经暗示了它的主要功能:从类路径下的指定XML文件加载配置信息,并据此构建应用上下文。 以下是实现ClassPathXmlApplicationContext类的基本步骤:

  • 定位资源:首先确定XML配置文件的位置。因为我们的类名中包含了ClassPath,所以我们将从类路径(如src/main/resources目录)查找XML文件。

  • 加载文档:使用DOM或其他方式将XML文件的内容加载到内存中,以便后续处理。

  • 解析XML:遍历XML文档结构,识别出代表不同Bean定义的元素及其属性。

  • 注册Bean:基于解析出来的信息,在应用上下文中注册相应的Bean。这可能包括设置Bean的作用域、生命周期回调等。

  • 实例化Bean:根据需要即时或者延迟地创建Bean实例,并处理它们之间的依赖关系。

  • 管理Bean的生命周期:确保正确地调用初始化和销毁方法。 通过以上步骤,我们可以构建一个基础版本的ClassPathXmlApplicationContext,它能够支持从XML配置文件中读取Bean的信息,并按照Spring的方式管理和初始化这些Bean。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class ClassPathXmlApplicationContext {
    private List<BeanDefinition> beanDefinitions = new ArrayList<>();
    private Map<String, Object> singletons = new HashMap<>();
    //构造器获取外部配置,解析出Bean的定义,形成内存映像
    public ClassPathXmlApplicationContext(String fileName) {
        this.readXml(fileName);
        this.instanceBeans();
    }
    private void readXml(String fileName) {
        SAXReader saxReader = new SAXReader();
        try {
            URL xmlPath =
this.getClass().getClassLoader().getResource(fileName);
            Document document = saxReader.read(xmlPath);
            Element rootElement = document.getRootElement();
            //对配置文件中的每一个<bean>,进行处理
            for (Element element : (List<Element>) rootElement.elements()) {
                //获取Bean的基本信息
                String beanID = element.attributeValue("id");
                String beanClassName = element.attributeValue("class");
                BeanDefinition beanDefinition = new BeanDefinition(beanID,
beanClassName);
                //将Bean的定义存放到beanDefinitions
                beanDefinitions.add(beanDefinition);
            }
        }
    }
    //利用反射创建Bean实例,并存储在singletons中
    private void instanceBeans() {
        for (BeanDefinition beanDefinition : beanDefinitions) {
            try {
                singletons.put(beanDefinition.getId(),
Class.forName(beanDefinition.getClassName()).newInstance());
            }
        }
    }
    //这是对外的一个方法,让外部程序从容器中获取Bean实例,会逐步演化成核心方法
    public Object getBean(String beanName) {
        return singletons.get(beanName);
    }
}

aaaaaa在解析和实例化Bean的过程中,ClassPathXmlApplicationContext扮演了核心角色。它通过定义唯一的构造函数来实现这一过程,主要执行两件事:读取XML配置文件(readXml)以及根据这些信息实例化Bean(instanceBeans)。

readXml 方法详解

  • 目标:将XML中的文本信息转换为内存中可以使用的数据结构,便于后续的Bean管理。

  • 步骤:

    1. 使用dom4j包提供的SAXReader对象创建一个读取器。
    2. 根据给定的XML文件路径加载文档,并获取根元素。
    3. 遍历根元素下的所有子节点,从中提取出每个Bean的关键属性(如id、class等)。
    4. 利用上述属性信息构建BeanDefinition对象,代表单个Bean的定义,并将其添加到BeanDefinitions列表中保存。

instanceBeans 方法详解

  • 目标:基于已有的Bean定义信息,使用反射技术创建具体的Bean实例。

  • 步骤:

    1. BeanDefinitions中取出每一个BeanDefinition对象。
    2. 利用Class.forName()方法依据类名字符串得到对应的Class对象。
    3. 将生成的具体类实例存入名为singletons的Map中,形成ID与实际对象之间的映射关系。

实现概述

当前阶段,ClassPathXmlApplicationContext不仅完成了对Bean信息的读取与实例化工作,还承担起了类似BeanFactory的角色,负责管理容器内的Bean们。通过维护beanDefinitionssingletons这两个关键集合,它能够有效地支持基本的依赖注入等功能。

功能验证

为了确保以上设计确实达到了预期效果,在com.minis目录下新增了一个test包,用于存放相应的测试代码。通过编写并运行测试案例,我们可以检查ClassPathXmlApplicationContext是否正确地实现了其功能。

1
2
3
public interface AService {
    void sayHello();
}

这里,我们定义了一个sayHello接口,该接口的实现是在控制台打印出“a service 1 say hello”这句话。

1
2
3
4
5
public class AServiceImpl implements AService {
    public void sayHello() {
        System.out.println("a service 1 say hello");
    }
}

我们将XML文件命名为beans.xml,注入AServiceImpl类,起个别名,为aservice。

1
2
3
4
<?xml version="1.0" encoding="UTF-8" ?>
<beans>
    <bean id = "aservice" class = "com.minis.test.AServiceImpl"></bean>
</beans>

除了测试代码,我们还需要启动类,定义main函数。

1
2
3
4
5
6
7
8
public class Test1 {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext ctx = new
ClassPathXmlApplicationContext("beans.xml");
        AService aService = (AService)ctx.getBean("aservice");
        aService.sayHello();
    }
}

构建最原始的IoC容器

在本节中,我们通过构建ClassPathXmlApplicationContext来实现一个基础的IoC容器。这个容器通过读取名为beans.xml的XML文件,获取Bean的定义,并利用getBean方法来获取并注入AService接口的实现类AServiceImpl。通过这种方式,我们可以在控制台输出a service 1 say hello,展示了IoC容器对Bean的基本管理能力。

BeanDefinition的引入

在实现IoC容器的过程中,我们引入了BeanDefinition的概念,它定义了Bean的属性和依赖关系。这使得容器能够理解如何创建和配置Bean。

ClassPathXmlApplicationContext的职责

尽管ClassPathXmlApplicationContext实现了基本的IoC功能,但它承担了过多的职责,违反了单一职责原则。因此,我们需要对其进行优化和扩展。

优化扩展:解耦ClassPathXmlApplicationContext

为了使ClassPathXmlApplicationContext更加模块化和易于扩展,我们计划将其分解为两个主要部分:

  1. 核心容器:负责Bean的创建和管理。

  2. 配置信息访问:负责从外部配置源(如XML文件)读取配置信息。 这种分解不仅有助于保持代码的清晰和模块化,而且也便于未来扩展到其他配置源,如Web或数据库。

项目代码结构的重构

为了使项目结构更加清晰,我们参考Spring的目录结构,对项目代码进行重构。这将有助于我们更好地组织代码,并为未来的扩展打下基础。

1
2
3
4
com.minis.beans;
com.minis.context;
com.minis.core;
com.minis.test;

定义BeansException

在正式开始解耦工作之前,我们先定义属于我们自己的异常处理类:BeansException。我们来看看异常处理类该如何定义。

1
2
3
4
5
public class BeansException extends Exception {
  public BeansException(String msg) {
    super(msg);
  }
}

可以看到,现在的异常处理类比较简单,它是直接调用父类(Exception)处理并抛出异常。有了这个基础的BeansException之后,后续我们可以根据实际情况对这个类进行拓展。

定义 BeanFactory

首先要拆出一个基础的容器来,刚才我们反复提到了 BeanFactory 这个词,现在我们正式引入BeanFactory这个接口,先让这个接口拥有两个特性:一是获取一个Bean(getBean),二是注册一个BeanDefinition(registerBeanDefinition)。你可以看一下它们的定义。

1
2
3
4
public interface BeanFactory {
    Object getBean(String beanName) throws BeansException;
    void registerBeanDefinition(BeanDefinition beanDefinition);
}

定义Resource

刚刚我们将BeanFactory的概念进行了抽象定义。接下来我们要定义Resource这个概念,我们把外部的配置信息都当成Resource(资源)来进行抽象,你可以看下相关接口。

1
2
public interface Resource extends Iterator<Object> {
}

aaaaaaa在当前的项目中,我们主要依赖于XML文件作为配置数据的来源。为了提高系统的灵活性和可扩展性,我们引入了Resource接口,这使得将来能够从多种来源获取配置信息,比如数据库或Web网络。 现在,我们的目标是进一步解耦代码,以便将XML文件的读取与解析逻辑从现有的ClassPathXmlApplicationContext类中分离出来。为此,我们将定义一个新的类——ClassPathXmlResource,它将负责处理所有与类路径下的XML资源相关的操作。

定义 ClassPathXmlResource

  1. 创建ClassPathXmlResource:该类将实现Resource接口,并专注于提供访问位于类路径下的XML文件的功能。

  2. 封装XML读取逻辑:在ClassPathXmlResource内部,我们需要封装用于打开并读取XML文件的方法。

  3. 支持解析:虽然ClassPathXmlResource本身不直接进行XML解析(这是BeanFactory的责任),但它应该能以一种对BeanFactory友好的方式提供原始XML内容。

  4. 测试覆盖:确保为ClassPathXmlResource编写足够的单元测试来验证其行为。 通过这样的设计,我们可以更容易地管理和维护与XML配置文件相关的功能,同时也为未来可能添加的新类型资源配置打下了基础。此外,这样做也符合面向对象的设计原则,即每个类都应该有单一职责。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public class ClassPathXmlResource implements Resource{
    Document document;
    Element rootElement;
    Iterator<Element> elementIterator;
    public ClassPathXmlResource(String fileName) {
        SAXReader saxReader = new SAXReader();
        URL xmlPath = this.getClass().getClassLoader().getResource(fileName);
        //将配置文件装载进来,生成一个迭代器,可以用于遍历
        try {
            this.document = saxReader.read(xmlPath);
            this.rootElement = document.getRootElement();
            this.elementIterator = this.rootElement.elementIterator();
        }
    }
    public boolean hasNext() {
        return this.elementIterator.hasNext();
    }
    public Object next() {
        return this.elementIterator.next();
    }
}

在处理XML文件时,dom4j库提供了极大的便利。它能够帮助我们将XML文件中的标签和属性转换成Java对象,从而简化了代码的编写。尽管我们也可以手动编写代码来解析XML文件,但为了减少重复劳动,我们选择使用这个第三方库。

dom4j的作用

dom4j是一个外部jar包,它使得读取和解析XML文件变得简单。通过这个库,我们可以轻松地将XML中的标签和参数映射到Java对象中。

XmlBeanDefinitionReader的作用

解析XML文件后,我们需要将这些解析后的数据转换成BeanDefinition对象,以便后续使用。XmlBeanDefinitionReader正是完成这一任务的工具。它将解析好的XML数据转换成我们需要的BeanDefinition,从而使得XML文件中定义的配置能够被应用程序所理解和使用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class XmlBeanDefinitionReader {
    BeanFactory beanFactory;
    public XmlBeanDefinitionReader(BeanFactory beanFactory) {
        this.beanFactory = beanFactory;
    }
    public void loadBeanDefinitions(Resource resource) {
        while (resource.hasNext()) {
            Element element = (Element) resource.next();
            String beanID = element.attributeValue("id");
            String beanClassName = element.attributeValue("class");
            BeanDefinition beanDefinition = new BeanDefinition(beanID, beanClassName);
            this.beanFactory.registerBeanDefinition(beanDefinition);
        }
    }
}

可以看到,在XmlBeanDefinitionReader中,有一个loadBeanDefinitions方法会把解析的XML内容转换成BeanDefinition,并加载到BeanFactory中。

BeanFactory功能扩展

首先,定义一个简单的BeanFactory实现类SimpleBeanFactory。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class SimpleBeanFactory implements BeanFactory{
    private List<BeanDefinition> beanDefinitions = new ArrayList<>();
    private List<String> beanNames = new ArrayList<>();
    private Map<String, Object> singletons = new HashMap<>();
    public SimpleBeanFactory() {
    }

    //getBean,容器的核心方法
    public Object getBean(String beanName) throws BeansException {
        //先尝试直接拿Bean实例
        Object singleton = singletons.get(beanName);
        //如果此时还没有这个Bean的实例,则获取它的定义来创建实例
        if (singleton == null) {
            int i = beanNames.indexOf(beanName);
            if (i == -1) {
                throw new BeansException();
            }
            else {
                //获取Bean的定义
                BeanDefinition beanDefinition = beanDefinitions.get(i);
                try {
                    singleton = Class.forName(beanDefinition.getClassName()).newInstance();
                }
                //注册Bean实例
                singletons.put(beanDefinition.getId(), singleton);
            }
        }
        return singleton;
    }

    public void registerBeanDefinition(BeanDefinition beanDefinition) {
        this.beanDefinitions.add(beanDefinition);
        this.beanNames.add(beanDefinition.getId());
    }
}

在Spring框架中,SimpleBeanFactory扮演着核心的角色,负责Bean的实例化和加载。通过将ClassPathXmlApplicationContext中与BeanDefinition相关的部分提取出来,ClassPathXmlApplicationContext变得更加简洁,其功能被重新分配给BeanFactory、Resource和Reader。以下是对这一变化的详细解析:

SimpleBeanFactory的作用

  • Bean实例化:SimpleBeanFactory负责将BeanDefinition转换为具体的Bean实例。

  • 加载到内存:它还负责将这些BeanDefinition加载到内存中,以便后续使用。

ClassPathXmlApplicationContext的变化

  • 功能简化:提取BeanDefinition相关功能后,ClassPathXmlApplicationContext成为一个“空壳子”,其核心功能被分解。

  • 集成者角色:尽管功能被分解,ClassPathXmlApplicationContext仍然扮演着集成者的角色,负责将不同的组件和功能整合在一起。

功能分配

  • BeanFactory:负责Bean的创建和管理。

  • Resource:处理资源的加载和访问。

  • Reader:负责读取和解析配置文件。

总结

通过这种设计,Spring框架能够更加灵活地处理Bean的生命周期和配置,同时也提高了代码的可维护性和可扩展性。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public class ClassPathXmlApplicationContext implements BeanFactory{
    BeanFactory beanFactory;
    //context负责整合容器的启动过程,读外部配置,解析Bean定义,创建BeanFactory
    public ClassPathXmlApplicationContext(String fileName) {
        Resource resource = new ClassPathXmlResource(fileName);
        BeanFactory beanFactory = new SimpleBeanFactory();
        XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory);
        reader.loadBeanDefinitions(resource);
        this.beanFactory = beanFactory;
    }
    //context再对外提供一个getBean,底下就是调用的BeanFactory对应的方法
    public Object getBean(String beanName) throws BeansException {
        return this.beanFactory.getBean(beanName);
    }
    public void registerBeanDefinition(BeanDefinition beanDefinition) {
        this.beanFactory.registerBeanDefinition(beanDefinition);
    }
}

在本次课程中,我们探讨了ClassPathXmlApplicationContext在实例化过程中的三个主要步骤,这为我们理解如何将XML配置转换为可管理的Bean对象提供了基础。下面是对这些步骤的结构化总结:

ClassPathXmlApplicationContext实例化过程

  1. 解析XML文件
  • 首先,ClassPathXmlApplicationContext会加载并解析指定路径下的XML配置文件。
  • 解析过程中识别出所有的bean定义以及它们之间的关系。
  1. 构建BeanDefinition
  • 根据解析到的信息,创建每个bean对应的BeanDefinition对象。
  • 这些BeanDefinition对象包含了关于如何创建特定bean的所有信息,比如类名、作用域等属性。
  1. 实例化与注入Bean
  • 接下来,使用之前构建好的BeanDefinition来创建实际的bean实例。
  • 创建完成后,这些bean将被添加到BeanFactory容器之中,同时根据需要完成依赖注入。

小结通过上述几个关键步骤,我们可以实现从XML配置文件到具体Java对象(即beans)的转化,并最终将这些对象置于IoC容器内进行统一管理。尽管整个流程看似简单,但它体现了非常重要的编程原则之一:单一职责原则。这意味着每个类或组件都应该专注于执行单一功能。遵循这一原则有助于提高代码的可维护性和扩展性。

aaaaaaa以上就是本节课关于MiniSpring框架核心部分——Bean和IoC初步搭建的知识点介绍。

IoC控制反转概念解析

在本节课中,我们学习了如何通过框架容器自动管理业务类对象,而无需手动创建。这种管理方式体现了IoC(控制反转)的核心思想。IoC的核心在于将对象的创建和依赖关系的管理从业务代码中分离出来,交由框架容器负责。以下是IoC概念的详细解析和代码体现:

1. 框架容器管理对象

  • 自动对象创建:框架容器自动创建业务类对象,无需手动new

  • Resource和BeanFactory

  • Resource:定义Bean的数据来源。

  • BeanFactory:负责Bean的容器化管理。

2. 功能解耦与容器结构

  • 功能解耦:通过将对象创建和依赖管理分离,容器结构更清晰。

  • 易于阅读和扩展:清晰的结构使得阅读和后续扩展更为方便。

3. IoC的“反转”含义

  • 控制权转移:IoC“反转”了对象创建和依赖管理的控制权,从业务代码转移到框架容器。

  • 代码体现:在代码中,这种“反转”体现在不再需要在业务逻辑中显式创建对象,而是通过框架容器来获取所需的对象。

4. IoC的扩展性和适用性

  • 基本功能的实现:虽然是一个简化的模型,但已经具备了IoC的基本功能。

  • 持续扩展:随着更多功能的添加,这个模型可以发展成为一个完整的框架。

5. 源代码参考

完整源代码可以在以下GitHub链接查看:YaleGuo/minis

课后思考题

  • IoC的“反转”:思考IoC的字面含义“控制反转”,它究竟反转了什么?又是如何在代码中体现的?欢迎在留言区讨论并分享这节课。 我们下节课再见!