IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> 我的自定义Spring框架 | 自定义Spring IoC功能 -> 正文阅读

[Java知识库]我的自定义Spring框架 | 自定义Spring IoC功能

分析完与Spring IoC功能相关的接口之后,接下来我们就要来自定义Spring IoC功能了。

首先,我们先来看一下需求:现要对下面的配置文件进行解析,并自定义Spring框架的IoC功能对涉及到的对象进行管理。

<?xml version="1.0" encoding="UTF-8"?>
<beans>

    <bean id="userDao" class="com.meimeixia.dao.impl.UserDaoImpl">
        <property name="username" value="zhangsan"></property>
        <property name="password" value="123456"></property>
    </bean>

    <bean id="userService" class="com.meimeixia.service.impl.UserServiceImpl">
        <property name="userDao" ref="userDao"></property>
    </bean>

</beans>

要想完成这一需求,可不是件容易的事情,我们得分如下几部分来进行定义。

  1. 定义bean相关的pojo类。
  2. 定义注册表相关的类。
  3. 定义解析器相关的类。
  4. 定义IoC容器相关的类。

下面,我们先来定义bean相关的pojo类。

定义bean相关的pojo类

首先,我们先使用IDEA来创建一个Maven工程,工程名字你可以取为liayun_spring,然后再来创建相应的包,这里为了让大家清楚地看到我都创建了哪些包,干脆我把最终Maven工程的结构给大家展示出来吧!

在这里插入图片描述

PropertyValue类

在这一部分,我们会创建不同的类,第一个类就是PropertyValue,该类的作用就是用来封装bean的属性的。看一下一开始的配置文件,最终我们是要解析该配置文件的,所以严格来说,PropertyValue这个类是用来封装<bean>标签的<property>子标签中的属性的。由于<property>子标签中有name、ref、value等属性,因此PropertyValue类里面至少得有name、ref、value这三个属性。

package com.meimeixia.framework.beans;

/**
 * 用来封装bean标签下的property标签的属性,属性有这些:
 *      name属性
 *      ref属性
 *      value属性:给基本数据类型及String类型的数据赋值
 * @author liayun
 * @create 2021-09-20 9:41
 */
public class PropertyValue {

    private String name;
    private String ref;
    private String value;

    public PropertyValue() {

    }

    public PropertyValue(String name, String ref, String value) {
        this.name = name;
        this.ref = ref;
        this.value = value;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getRef() {
        return ref;
    }

    public void setRef(String ref) {
        this.ref = ref;
    }

    public String getValue() {
        return value;
    }

    public void setValue(String value) {
        this.value = value;
    }

}

MutablePropertyValues类

创建完PropertyValue类之后,接下来我们再来创建第二个类,那就是MutablePropertyValues。为什么要创建这个类呢?因为一个<bean>标签可以有多个<property>子标签,而每一个<property>子标签都会被封装成一个PropertyValue对象,对于多个PropertyValue对象我们就要进行存储以及管理了,所以在这里我们就要创建一个MutablePropertyValues类,用来存储并管理多个PropertyValue对象了。

注意,在创建该类时,我们需要用到迭代器模式,所以该类得去实现Iterable接口。至于为什么这儿要用到迭代器模式,我不说,相信大家也知道,因为MutablePropertyValues类是用来存储并管理多个PropertyValue对象的,所以它必须是可以迭代的。

package com.meimeixia.framework.beans;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
 * 用于存储和管理多个PropertyValue对象
 * @author liayun
 * @create 2021-09-20 10:15
 */
public class MutablePropertyValues implements Iterable<PropertyValue> {

    // 定义List集合对象,用来存储PropertyValue对象
    private final List<PropertyValue> propertyValueList; // 如果用final修饰的话,那么就意味着它只能被赋值一次

    // 以下构造方法是用来为以上成员变量赋值的
    public MutablePropertyValues() {
        this.propertyValueList = new ArrayList<PropertyValue>();
    }

    public MutablePropertyValues(List<PropertyValue> propertyValueList) {
        if (propertyValueList == null) {
            this.propertyValueList = new ArrayList<PropertyValue>();
        } else {
            this.propertyValueList = propertyValueList;
        }
    }

    // 获取所有的PropertyValue对象,当然,是以数组的形式返回
    public PropertyValue[] getPropertyValues() {
        // 将集合转换为数组并返回
        return propertyValueList.toArray(new PropertyValue[0]);
    }

    // 根据name属性值获取PropertyValue对象
    public PropertyValue getPropertyValue(String propertyName) {
        // 遍历集合对象
        for (PropertyValue propertyValue : propertyValueList) {
            if (propertyValue.getName().equals(propertyName)) {
                return propertyValue;
            }
        }
        return null;
    }

    // 判断集合是否为空
    public boolean isEmpty() {
        return propertyValueList.isEmpty();
    }

    // 往集合里面添加PropertyValue对象。注意,该方法的返回值类型是MutablePropertyValues,目的是为了能实现链式编程
    public MutablePropertyValues addPropertyValue(PropertyValue pv) {
        // 判断集合中存储的PropertyValue对象是否和传递进来的重复了,如果重复了,那么就进行覆盖
        for (int i = 0; i < propertyValueList.size(); i++) {
            // 获取集合中每一个PropertyValue对象
            PropertyValue currentPv = propertyValueList.get(i);
            if (currentPv.getName().equals(pv.getName())) {
                propertyValueList.set(i, pv); // 如果重复了,那么就进行覆盖
                return this; // 返回当然对象(即MutablePropertyValues类型的对象),目的就是实现链式编程
            }
        }
        this.propertyValueList.add(pv); // 如果没有重复的,那么就直接添加进集合里面去
        return this; // 返回当然对象(即MutablePropertyValues类型的对象),目的就是实现链式编程
    }

    // 判断是否有指定name属性值的PropertyValue对象,有的话返回true,没有的话返回false
    public boolean contains(String propertyName) {
        return getPropertyValue(propertyName) != null;
    }

    // 获取迭代器对象
    @Override
    public Iterator<PropertyValue> iterator() {
        /*
         * 获取迭代器对象的这个方法应该如何来实现呢?
         *
         * 由于PropertyValue对象是存储在一开始定义的List集合里面的,所以这里我们直接调用其获取迭代器的方法(即iterator)即可。
         */
        return propertyValueList.iterator();
    }

}

相信大家可以看到,我们在以上MutablePropertyValues类中定义了很多方法,这些方法我也写了一些比较详细的注释,相信大家都能看懂。

BeanDefinition类

接下来,我们再来创建bean相关的pojo类里面的最后一个类,也是最重要的一个类,叫BeanDefinition。其实,之前我们在分析Spring IoC功能的相关接口时,就见过类似这玩意,只不过它是接口,而在这里我们是直接将其定义成类了,主要是为了简单,图省事。

BeanDefinition类主要是用来封装bean信息的,主要包含id(即bean对象的名称)、class(需要交由Spring管理的类的全类名)及子标签<property>中的数据。

package com.meimeixia.framework.beans;

/**
 * 用来封装bean标签数据,包含:
 *      id属性
 *      class属性
 *      property子标签中的数据
 * @author liayun
 * @create 2021-09-20 11:06
 */
public class BeanDefinition {

    private String id;
    private String className;
    private MutablePropertyValues propertyValues;

    public BeanDefinition() {
        this.propertyValues = new MutablePropertyValues();
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getClassName() {
        return className;
    }

    public void setClassName(String className) {
        this.className = className;
    }

    public MutablePropertyValues getPropertyValues() {
        return propertyValues;
    }

    public void setPropertyValues(MutablePropertyValues propertyValues) {
        this.propertyValues = propertyValues;
    }

}

可以看到,虽然这个类特别重要,但是创建起来还是比较简单的。

定义注册表相关的类

BeanDefinitionRegistry接口

定义完bean相关的pojo类之后,接下来我们就要来定义注册表相关的类了。当然,这一部分就不仅仅是有类了,还有接口,说白了,在这一部分,我们会创建一个接口及其子实现类。至于接口的话,我们就命名为BeanDefinitionRegistry了,下面我们就来看一下该接口需要定义哪些功能?

BeanDefinitionRegistry接口应定义注册表的相关操作,所以需要定义如下功能:

  • 注册BeanDefinition对象到注册表中。
  • 从注册表中删除指定名称的BeanDefinition对象。
  • 根据名称从注册表中获取BeanDefinition对象。
  • 判断注册表中是否包含指定名称的BeanDefinition对象。
  • 获取注册表中BeanDefinition对象的个数。
  • 获取注册表中所有的BeanDefinition对象的名称。

以上这些功能,相信大家应该很熟悉,因为之前我们在分析Spring源码里面的BeanDefinitionRegistry接口时就看到过,BeanDefinitionRegistry接口也定义了以上这些功能。

根据以上分析,我们创建出来的BeanDefinitionRegistry接口就应该是下面这个样子的。

package com.meimeixia.framework.beans.factory.support;

import com.meimeixia.framework.beans.BeanDefinition;

/**
 * 注册表对象所属接口
 * @author liayun
 * @create 2021-09-20 11:16
 */
public interface BeanDefinitionRegistry {
    // 注册BeanDefinition对象到注册表中
    void registerBeanDefinition(String beanName, BeanDefinition beanDefinition);

    // 从注册表中删除指定名称的BeanDefinition对象
    void removeBeanDefinition(String beanName) throws Exception;

    // 根据名称从注册表中获取BeanDefinition对象
    BeanDefinition getBeanDefinition(String beanName) throws Exception;

    boolean containsBeanDefinition(String beanName);

    int getBeanDefinitionCount();

    String[] getBeanDefinitionNames();
}

BeanDefinitionRegistry接口创建完毕之后,接下来我们就要来创建它的子实现类了,注意,这里我们只创建一个子实现类。

SimpleBeanDefinitionRegistry类

我们在创建该类时,要让该类去实现BeanDefinitionRegistry接口,并去重写它里面所有的抽象方法。注意,在该类里面我们还得定义一个Map集合,让其作为注册表容器。

package com.meimeixia.framework.beans.factory.support;

import com.meimeixia.framework.beans.BeanDefinition;

import java.util.HashMap;
import java.util.Map;

/**
 * 注册表接口的子实现类
 * @author liayun
 * @create 2021-09-20 11:23
 */
public class SimpleBeanDefinitionRegistry implements BeanDefinitionRegistry {
    // 定义一个Map集合,用来存储BeanDefinition对象
    private Map<String, BeanDefinition> beanDefinitionMap = new HashMap<String, BeanDefinition>(); // 注意,在这里我们选择创建的是双列集合,因为我们不仅要存储BeanDefinition对象,还要存储其名称

    @Override
    public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition) {
        beanDefinitionMap.put(beanName, beanDefinition);
    }

    @Override
    public void removeBeanDefinition(String beanName) throws Exception {
        beanDefinitionMap.remove(beanName);
    }

    @Override
    public BeanDefinition getBeanDefinition(String beanName) throws Exception {
        return beanDefinitionMap.get(beanName);
    }

    @Override
    public boolean containsBeanDefinition(String beanName) {
        return beanDefinitionMap.containsKey(beanName);
    }

    @Override
    public int getBeanDefinitionCount() {
        return beanDefinitionMap.size();
    }

    @Override
    public String[] getBeanDefinitionNames() {
        return beanDefinitionMap.keySet().toArray(new String[0]);
    }
}

定义解析器相关的类

接下来,我们就来定义解析器相关的接口和类。相信大家也都知道了,我们均是参照Spring里面的接口和类来定义的,所以在这一部分我们就将接口命名为BeanDefinitionReader,既然它是一个接口的话,那么它里面定义的便是最基本的功能规范了。还有,我们还得为该接口创建一个子实现类,名字不妨就叫做XmlBeanDefinitionReader。

为什么我们还要创建BeanDefinitionReader接口呢?之前我带着大家分析Spring IoC功能相关的接口时,你也看到了,针对于不同的配置文件,Spring会提供不同的子类来进行解析,例如,解析properties格式的配置文件用的是PropertiesBeanDefinitionReader类,解析XML格式的配置文件用的是XmlBeanDefinitionReader。大家要是能够去看一下BeanDefinitionReader接口的继承体系的话,你会发现这俩类都是其子实现类。当然了,这里我们在自定义Spring IoC功能时,只会针对XML格式的配置文件来创建解析类。

BeanDefinitionReader接口

由于BeanDefinitionReader是用来解析配置文件并在注册表中注册bean的信息的,所以我们应在它里面定义如下两个规范。

  1. 获取注册表的功能,让外界可以通过该对象获取注册表对象。
  2. 加载配置文件,并注册bean数据。

根据以上分析,我们创建出来的eanDefinitionReader接口就应该是下面这个样子的。

package com.meimeixia.framework.beans.factory.support;

/**
 * 用来解析配置文件的,而且该接口只是定义了规范,具体的应由子类来实现
 * @author liayun
 * @create 2021-09-20 11:49
 */
public interface BeanDefinitionReader {
    // 获取注册表对象
    BeanDefinitionRegistry getRegistry();

    // 加载配置文件,并在注册表中进行注册
    void loadBeanDefinitions(String configLocation) throws Exception;
}

XmlBeanDefinitionReader类

BeanDefinitionReader接口创建完毕之后,接下来我们就来创建其子实现类,名字上面我也说了,就叫XmlBeanDefinitionReader。

相信大家也知道了,XmlBeanDefinitionReader类是专门用来解析XML格式的配置文件的,而且创建该类时,不用我说,大家都应该知道该类得实现BeanDefinitionReader接口并去重写它里面的两个功能,如下所示。

package com.meimeixia.framework.beans.factory.xml;

import com.meimeixia.framework.beans.BeanDefinition;
import com.meimeixia.framework.beans.MutablePropertyValues;
import com.meimeixia.framework.beans.PropertyValue;
import com.meimeixia.framework.beans.factory.support.BeanDefinitionReader;
import com.meimeixia.framework.beans.factory.support.BeanDefinitionRegistry;
import com.meimeixia.framework.beans.factory.support.SimpleBeanDefinitionRegistry;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

import java.io.InputStream;
import java.util.List;

/**
 * 针对XML格式的配置文件进行解析的类
 * @author liayun
 * @create 2021-09-20 11:58
 */
public class XmlBeanDefinitionReader implements BeanDefinitionReader {
    /*
     * 声明注册表对象
     *
     * 为什么要在成员变量位置处声明注册表对象呢?大家不妨来想一下,XmlBeanDefinitionReader对象(即解析器)是
     * 专门用来解析XML格式的配置文件的,解析完之后,自然是会将配置文件里面的<bean>标签封装成BeanDefinition对
     * 象,那么这些BeanDefinition对象是存放在哪呢?是不是就是注册到了注册表对象里面呀?所以,我们就在这个位置声
     * 明了一个注册表对象。
     */
    private BeanDefinitionRegistry registry;

    public XmlBeanDefinitionReader() {
        registry = new SimpleBeanDefinitionRegistry();
    }

    /**
     * 获取注册表对象
     * @return 直接返回成员注册表对象
     */
    @Override
    public BeanDefinitionRegistry getRegistry() {
        return registry;
    }

    /**
     * 加载配置文件,并在注册表中进行注册
     * @param configLocation 类路径下配置文件的路径
     * @throws Exception
     */
    @Override
    public void loadBeanDefinitions(String configLocation) throws Exception {
        // 使用dom4j进行XML配置文件的解析
        SAXReader reader = new SAXReader();
        // 获取类路径下的配置文件。注意,这里我们只实现类路径下的配置文件的加载
        InputStream is = XmlBeanDefinitionReader.class.getClassLoader().getResourceAsStream(configLocation);
        Document document = reader.read(is);
        // 根据Document对象获取根标签对象(根标签很明显就是<beans>标签)
        Element rootElement = document.getRootElement();
        // 获取根标签下所有的<bean>子标签对象
        List<Element> beanElements = rootElement.elements("bean");
        // 遍历集合
        for (Element beanElement : beanElements) {
            // 获取id属性
            String id = beanElement.attributeValue("id");
            // 获取class属性
            String className = beanElement.attributeValue("class");

            // 将id属性和class属性封装到BeanDefinition对象中
            // 1. 创建BeanDefinition对象
            BeanDefinition beanDefinition = new BeanDefinition();
            beanDefinition.setId(id);
            beanDefinition.setClassName(className);

            // 创建MutablePropertyValues对象
            MutablePropertyValues mutablePropertyValues = new MutablePropertyValues();

            // 获取<bean>标签下所有的<property>子标签对象
            List<Element> propertyElements = beanElement.elements("property");
            for (Element propertyElement : propertyElements) {
                String name = propertyElement.attributeValue("name");
                String ref = propertyElement.attributeValue("ref");
                String value = propertyElement.attributeValue("value");
                PropertyValue propertyValue = new PropertyValue(name, ref, value);
                mutablePropertyValues.addPropertyValue(propertyValue);
            }
            // 将MutablePropertyValues对象封装到BeanDefinition对象中
            beanDefinition.setPropertyValues(mutablePropertyValues);

            // 将BeanDefinition对象注册到注册表中
            registry.registerBeanDefinition(id, beanDefinition);
        }
    }
}

注意,由于我们要在XmlBeanDefinitionReader类的loadBeanDefinitions方法中使用dom4j进行XML配置文件的解析,所以我们应在工程的pom.xml文件里面导入对应的jar包的坐标,如下所示。

<dependency>
    <groupId>dom4j</groupId>
    <artifactId>dom4j</artifactId>
    <version>1.6.1</version>
</dependency>

定义IoC容器相关的类

定义完解析器相关的接口和类之后,接下来我们来定义IoC容器相关的接口和类。大家要注意了,这一部分是我们自定义Spring IoC功能最核心的部分,而且在这一部分我们需要定义如下这些接口和类。

BeanFactory接口

在该接口中我们需要定义IoC容器的统一规范,即获取bean对象的方法。

package com.meimeixia.framework.beans.factory;

/**
 * IoC容器父接口
 * @author liayun
 * @create 2021-09-20 17:34
 */
public interface BeanFactory {
    // 根据bean对象的名称获取bean对象
    Object getBean(String name) throws Exception;
    
    // 根据bean对象的名称获取bean对象,并进行类型转换
    <T> T getBean(String name, Class<? extends T> clazz) throws Exception;
}

ApplicationContext接口

BeanFactory接口创建完毕之后,接下来我们来创建它的一个子接口,这里我们就取名为ApplicationContext了。

之前我带领着大家分析Spring IoC功能相关的接口时,相信大家也知道了ApplicationContext属于非延时加载,也就是说(使用者)在创建容器对象的时候,就会去加载配置文件,并实例化bean对象,最终将其存储在容器里面。

这里,我也不废话了,直接给出ApplicationContext接口的代码,如下所示。

package com.meimeixia.framework.context;

import com.meimeixia.framework.beans.factory.BeanFactory;

/**
 * 定义非延时加载功能
 * @author liayun
 * @create 2021-09-20 17:39
 */
public interface ApplicationContext extends BeanFactory {
    // 进行配置文件加载并进行对象创建
    void refresh() throws Exception;
}

可以看到,我们在创建ApplicationContext接口时,在它里面只定义了一个refresh方法,该方法主要完成以下两个功能。

  • 加载配置文件。
  • 根据注册表中的BeanDefinition对象封装的数据进行bean对象的创建。

AbstractApplicationContext类

ApplicationContext接口创建完毕之后,接下来我们来创建它的一个子实现类,这里我们就取名为AbstractApplicationContext了。

那么,AbstractApplicationContext类有什么特点以及作用呢?

  • 作为ApplicationContext接口的子类,所以该类也是非延时加载,也就是立即加载,这样,我们就需要在该类中定义一个Map集合,以作为bean对象存储的容器。

  • 声明BeanDefinitionReader类型的变量,用来进行XML配置文件的解析,这是符合单一职责原则的。也就是说,如果你要去解析配置文件的话,那么就不要在该类里面去自己实现了,而是直接调用解析器的方法进行解析就行了。

    当然了,BeanDefinitionReader类型的对象创建应交由子类去实现,因为只有子类明确到底会创建BeanDefinitionReader哪个子实现类对象。

明确了AbstractApplicationContext类具有的以上特点之后,相信大家不难创建出该类,如下所示。

package com.meimeixia.framework.context.support;

import com.meimeixia.framework.beans.factory.support.BeanDefinitionReader;
import com.meimeixia.framework.beans.factory.support.BeanDefinitionRegistry;
import com.meimeixia.framework.context.ApplicationContext;

import java.util.HashMap;
import java.util.Map;

/**
 * ApplicationContext接口的子实现类,用于立即加载
 * @author liayun
 * @create 2021-09-20 18:07
 */
public abstract class AbstractApplicationContext implements ApplicationContext {
    // 声明解析器变量
    protected BeanDefinitionReader beanDefinitionReader; // 注意,这里我们只是声明解析器变量而已,具体的对象应交由子类去创建。
                                                         // 而且,为了让子类更好的去访问,我们将会使用protected来修饰!

    // 定义用于存储bean对象的Map容器。也是为了让子类更好的去访问,我们同样会使用protected来修饰!
    protected Map<String, Object> singletonObjects = new HashMap<String, Object>(); // 注意,这里我们不考虑线程安全问题,直接创建一个HashMap对象就可以了

    // 声明配置文件类路径的变量。也是为了让子类更好的去访问,我们同样会使用protected来修饰!
    protected String configLocation;

    @Override
    public void refresh() throws Exception {
        // 加载BeanDefinition对象。加载BeanDefinition对象,我们只需要去调用解析器里面的方法即可
        beanDefinitionReader.loadBeanDefinitions(configLocation);
        // 初始化bean对象,也就是创建bean对象
        finishBeanInitialization();
    }

    /**
     * bean对象的初始化
     *
     * 不妨我们来思考一个问题,就是如果我们要进行bean对象的初始化,那么应该获取哪个东东呢?
     * 很显然,就是BeanDefinition对象,因为BeanDefinition对象里面记录了bean的相关信
     * 息,只有拿到这些信息,你才能去创建对象,所以我们要去获取BeanDefinition对象。而
     * BeanDefinition对象又是被注册在注册表里面的,所以首先我们还得先去获取对应的注册
     * 表对象!
     */
    private void finishBeanInitialization() throws Exception {
        // 获取注册表对象
        BeanDefinitionRegistry registry = beanDefinitionReader.getRegistry();

        // 获取BeanDefinition对象
        String[] beanNames = registry.getBeanDefinitionNames();
        for (String beanName : beanNames) {
            // 进行bean的初始化
            getBean(beanName);
        }
    }
}

注意:该类finishBeanInitialization方法中调用getBean方法使用到了模板方法模式。

ClassPathXmlApplicationContext类

接下来,我们来定义IoC容器接口的具体子实现类,这个子实现类的名字不妨就取名为ClassPathXmlApplicationContext,通过该类的类名我们就能知道该类主要是加载类路径下的XML格式的配置文件,并进行bean对象的创建的

接下来,我们就来看一下该类具体要完成的功能主要有哪些?

  • 在构造方法中,创建BeanDefinitionReader对象。因为我们只是在父类中声明了一个BeanDefinitionReader类型的变量而已,而该BeanDefinitionReader类型的对象的创建应交由具体的子类来实现。
  • 在构造方法中,调用refresh方法,用于进行配置文件加载、创建bean对象并存储到容器中。
  • 重写父接口中的getBean方法,并实现依赖注入(DI)操作。

根据以上分析,我们创建出来的ClassPathXmlApplicationContext类就应该是下面这个样子的。

package com.meimeixia.framework.context.support;

import com.meimeixia.framework.beans.BeanDefinition;
import com.meimeixia.framework.beans.MutablePropertyValues;
import com.meimeixia.framework.beans.PropertyValue;
import com.meimeixia.framework.beans.factory.support.BeanDefinitionRegistry;
import com.meimeixia.framework.beans.factory.xml.XmlBeanDefinitionReader;
import com.meimeixia.framework.utils.StringUtils;

import java.lang.reflect.Method;

/**
 * IoC容器具体的子实现类:用于加载类路径下的XML格式的配置文件
 * @author liayun
 * @create 2021-09-20 19:00
 */
public class ClassPathXmlApplicationContext extends AbstractApplicationContext {

    /**
     * 提供一个有参构造,该有参构造需要传入配置文件的类路径
     * @param configLocation
     */
    public ClassPathXmlApplicationContext(String configLocation) {
        this.configLocation = configLocation;
        // 构建解析器对象
        this.beanDefinitionReader = new XmlBeanDefinitionReader(); // 注意,我们现在是已经规定了要加载的就是XML格式的配置文件,所以这里我们创建的是XmlBeanDefinitionReader对象
        try {
            this.refresh();
        } catch (Exception e) {
            // 这儿我们不做任何处理啊!
        }
    }

    // 根据bean对象的名称获取bean对象
    @Override
    public Object getBean(String name) throws Exception {
        // 判断对象容器中是否包含指定名称的bean对象,若包含,则直接返回即可,若不包含,则还需要自行创建
        Object obj = singletonObjects.get(name);
        if (obj != null) { // 对象容器中确实包含指定名称的bean对象,所以直接获取到之后直接进行返回
            return obj;
        }

        // 对象容器中并没有包含指定名称的bean对象,所以我们还需要自行创建
        // 获取BeanDefinition对象,因为它里面记录了bean的相关信息
        BeanDefinitionRegistry registry = beanDefinitionReader.getRegistry();
        BeanDefinition beanDefinition = registry.getBeanDefinition(name);
        // 获取bean信息中的className,也就是全类名
        String className = beanDefinition.getClassName();
        // 通过反射创建对象
        Class<?> clazz = Class.forName(className);
        Object beanObj = clazz.newInstance(); // 我们的目的就是为了获取这个bean对象

        // 进行依赖注入操作
        MutablePropertyValues propertyValues = beanDefinition.getPropertyValues();
        // 由于MutablePropertyValues类使用到了迭代器模式,所以我们就可以使用迭代器去遍历了
        for (PropertyValue propertyValue : propertyValues) { // 如果你能使用迭代器去遍历的话,那么就意味着你也可以使用增强for循环去遍历
            // 获取name属性的值
            String propertyName = propertyValue.getName();
            // 获取value属性的值
            String value = propertyValue.getValue();
            // 获取ref属性的值
            String ref = propertyValue.getRef();
            // 这里大家一定要注意,<property>标签里面的value属性和ref属性只能存在一个!
            if (ref != null && !"".equals(ref)) {
                // 获取依赖的bean对象
                Object bean = getBean(ref); // 这里涉及到递归操作
                /*
                 * 拿到name属性的值之后,我们就要去拼接对应的set方法名了。
                 *
                 * 为什么要去拼接对应的set方法呢?因为<property>标签里面的name
                 * 属性值是要和类中的set方法相对应的属性名保持一致的!我相信使用过
                 * Spring框架的童鞋应该都知道这一点。
                 */
                String methodName = StringUtils.getSetterMethodByFieldName(propertyName);
                // 获取所有的方法对象
                Method[] methods = clazz.getMethods();
                for (Method method : methods) {
                    if (methodName.equals(method.getName())) {
                        // 通过反射执行该set方法
                        method.invoke(beanObj, bean);
                    }
                }
            }

            if (value != null && !"".equals(value)) {
                // 拼接set方法名
                String methodName = StringUtils.getSetterMethodByFieldName(propertyName);
                // 获取Method对象
                Method method = clazz.getMethod(methodName, String.class);
                method.invoke(beanObj, value);
            }
        }

        /*
         * 在返回beanObj对象之前,将该对象存储到Map容器中。
         *
         * 为什么要存放在Map容器里面呢?因为如果你不存放到Map容器里面的话,
         * 那么下一次你从Map容器里面去获取bean对象时,肯定是获取不到的,获
         * 取不到的话,就意味着你还需要再去重新创建一遍,显然这就很愚蠢了!
         */
        singletonObjects.put(name, beanObj);
        return beanObj;
    }

    @Override
    public <T> T getBean(String name, Class<? extends T> clazz) throws Exception {
        Object bean = getBean(name);
        if (bean == null) {
            return null;
        }
        return clazz.cast(bean); // 该cast方法就是用来进行强制类型转换的
    }

}

由于以上类的getBean方法中需要根据<property>标签的name属性值拼接set方法名,所以在这里我们就专门创建了一个工具类,即StringUtils,如下所示。

package com.meimeixia.framework.utils;

/**
 * 工具类
 * @author liayun
 * @create 2021-09-20 19:31
 */
public class StringUtils {

    private StringUtils() {

    }

    // 拼接set方法名,例如userDao ---> setUserDao
    public static String getSetterMethodByFieldName(String fieldName) {
        String methodName = "set" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1);
        return methodName;
    }

}

测试

至此,咱们自己定义的Spring IoC功能就已经实现了。实现了之后,到底能不能使用还是一个大大的疑问,所以我们还得进行测试。

大家一定要记住,在测试之前,我们得先将我们的Maven工程安装到本地仓库里面,因为安装到本地仓库里面的话,其他的工程就可以引入了。那么,如何将我们的Maven工程安装到本地仓库中呢?

首先,确保Maven工程的打包方式是jar,如下图所示。

在这里插入图片描述

然后,按照下图来安装Maven工程,一切尽在不言中。

在这里插入图片描述

当在控制台中看到BUILD SUCCESS时,就代表安装成功了,反正我是安装成功了,因为我在本地仓库里面是能找到打包成的jar文件的,如下图所示。

在这里插入图片描述

接下来,我们就要在之前的Maven工程(即spring_demo)中引入该jar文件并使用它了。

首先,在spring_demo工程的pom.xml文件里面注掉之前引入的Spring框架的坐标,然后引入以上jar文件的坐标,如下所示。

<!--<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.3.9</version>
</dependency>-->
<dependency>
    <groupId>com.meimeixia</groupId>
    <artifactId>liayun_spring</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

然后,按照下图来刷新一下spring_demo工程,以确保工程引入了以上jar文件。很多人都会忘了这一步,切记切记!

在这里插入图片描述

以上jar文件的坐标引入完毕之后,我们就要来稍微修改一下UserController类的代码了,其实也并不需要修改,只是将导入的类改变一下就行,如下所示,看到没有,导入的尽是以上jar文件中的类和接口,这些都是我们自个定义的。

package com.meimeixia.controller;

import com.meimeixia.framework.context.ApplicationContext;
import com.meimeixia.framework.context.support.ClassPathXmlApplicationContext;
import com.meimeixia.service.UserService;

/**
 * @author liayun
 * @create 2021-09-19 18:41
 */
public class UserController {
    public static void main(String[] args) throws Exception {
        // 1. 创建Spring的容器对象
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
        // BeanFactory beanFactory = new XmlBeanFactory(new ClassPathResource("applicationContext.xml"));
        // 2. 从容器对象中获取UserService对象
        UserService userService = applicationContext.getBean("userService", UserService.class);
        // 3. 调用UserService对象的方法进行业务逻辑处理
        userService.add();
    }
}

现在我们试着来加载一下如下的XML配置文件,看一下咱们自定义的Spring IoC功能好不好使?

<?xml version="1.0" encoding="UTF-8"?>
<beans>

    <bean id="userDao" class="com.meimeixia.dao.impl.UserDaoImpl"></bean>

    <bean id="userService" class="com.meimeixia.service.impl.UserServiceImpl">
        <property name="userDao" ref="userDao"></property>
    </bean>

</beans>

此时,运行以上UserController类的代码,打印结果如下图所示,可以看到确实是我们所想要的结果。

在这里插入图片描述

当然,我们现在还需要去验证另外一个问题,就是我们自己定义的Spring IoC容器到底是立即加载的呢,还是延时加载的?

为了验证这一点,我们在UserController类的如下代码处打上了一个断点,然后以Debug的方式来运行UserController类的代码。

在这里插入图片描述

此时,程序肯定是要停留在断点处的,然后我们按下F6键让程序再往下运行一行代码,当你切换到控制台时,你会发现现在控制台有打印结果了,如下图所示。

在这里插入图片描述

这也就是说在实例化容器对象后就会自动实例化bean,以此验证了我们自己定义的Spring IoC容器就是属于立即加载的。

接下来,我们再来做一个测试,就是给UserDaoImpl对象里面去注入String类型的数据。

首先,在UserDaoImpl类里面声明如下两个成员变量,注意了,我们这里是通过setter方法来注入的,所以我们还得为这俩成员变量提供对应的setter方法。

package com.meimeixia.dao.impl;

import com.meimeixia.dao.UserDao;

/**
 * 数据访问层实现类
 * @author liayun
 * @create 2021-09-19 18:29
 */
public class UserDaoImpl implements UserDao {

    private String username;
    private String password;

    public void setUsername(String username) {
        this.username = username;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public UserDaoImpl() {
        System.out.println("userDao被创建了");
    }

    @Override
    public void add() {
        // System.out.println("UserDao...");
        System.out.println("UserDao..." + username + "==" + password);
    }

}

然后,我们试着来加载一下如下的XML配置文件,看一下咱们自定义的Spring IoC功能还好不好使?

<?xml version="1.0" encoding="UTF-8"?>
<beans>

    <bean id="userDao" class="com.meimeixia.dao.impl.UserDaoImpl">
        <property name="username" value="zhangsan"></property>
        <property name="password" value="123456"></property>
    </bean>

    <bean id="userService" class="com.meimeixia.service.impl.UserServiceImpl">
        <property name="userDao" ref="userDao"></property>
    </bean>

</beans>

最后,我们再来运行以上UserController类的代码,打印结果如下图所示,可以看到对于String类型的数据,我们自定义的Spring IoC容器也是能够进行注入的。

在这里插入图片描述

至此,我们就测试完毕了我们自定义的Spring IoC功能,还是挺好使的,是不是啊!大家也不妨去使用一下,用完之后,你会发现我们自定义的Spring IoC功能基本上还是可以的,至少是可以进行依赖管理的,不仅能注入bean对象,还能注入String类型的数据。

总结

接下来,我们对自定义Spring IoC功能进行一个总结。

首先,我们来看一下我们自定义的Spring IoC功能里面使用到了哪些设计模式?总共用到了如下几个设计模式:

  • 工厂模式。

    这里面使用的是工厂模式 + 配置文件的方式,其作用是可以提高程序的扩展性以及灵活性。

    我在这里还是要多说一嘴,大家可以看到,在配置文件里面我们配了多个bean,并且我们在配置文件里面也体现出来了bean和bean之间的依赖关系。明确了这点之后,咱们来看一下UserServiceImpl这个实现类的代码,如下图所示,可以看到该类得依赖于UserDao子实现类对象,但是在该类里面并没有体现出来它到底要依赖的是哪个子实现类对象,很明显,这就是面向接口编程,作用是能够提高程序的扩展性。后期,如果我们想要去更换UserDao子实现类的话,那么只需要去修改配置文件即可,工程中的代码是不需要进行任何修改的。这便是使用工厂模式 + 配置文件的方式的好处。

    在这里插入图片描述

  • 单例模式。

    Spring IoC管理的bean对象都是单例的,此处的单例并不是通过构造器进行单例的控制的,而是Spring框架对每一个bean只创建了一个对象。

    上面这句话什么意思啊?我来给大家解释解释,之前我为大家讲解过创建型模式里面的单例设计模式,想必大家应该都知道单例设计模式该怎么写吧!无非就是私有构造方法并提供一个对象让外界去使用,是不是啊!所以,我们都是通过私有构造器的这种方式来进行单例的控制的。

    但是,此处我们是通过Spring框架对每一个bean只创建一个对象来实现单例的。有些童鞋就会想了,为什么会是这样的呢?这是因为别人在使用我们自己定义的Spring IoC功能时,我们不可能去要求用户在定义他自己的类时使用单例设计模式。

  • 模板方法模式。

    相信大家都看到了,AbstractApplicationContext类中的finishBeanInitialization方法(即完成bean对象的初始化)调用了子类的getBean方法,很显然,这使用的便是模板方法模式。

    为什么要去调用子类中的getBean方法呢?这是因为getBean方法的实现和环境息息相关,例如对不同的配置文件来说,获取bean对象的方式就是不一样的,因此getBean方法就应交由具体的子类去实现,这同样也能提高程序的扩展性和灵活性。

  • 迭代器模式。

    大家也都看到了,对于MutablePropertyValues类的定义,我们使用到了迭代器模式。为什么在定义该类时要使用迭代器模式呢?因为该类存储并管理着PropertyValue对象,也就意味我们可以将其理解为是一个容器了,既然是容器的话,那么我们就可以给该容器提供一种遍历方式。

以上就是我们自定义的Spring IoC功能里面使用到的设计模式。不过,这里我要说明的一点是,Spring框架其实使用到了很多设计模式,如AOP使用到了代理模式,选择JDK代理或者CGLIB代理使用到了策略模式,除此之外,还用到了适配器模式、装饰者模式、观察者模式等。而我们这里只是实现了一个简易的Spring IoC功能,并没有一一使用这些设计模式,大家要明确这一点。

然后,我得告诉大家的是我们自定义的Spring IoC功能符合大部分设计原则,这一点我在编写代码的过程中就已经给大家稍微说了一下,当然了,你要是想体会得更深一点,那你还得自己去实现一下你自个的Spring IoC功能。

最后,我得给大家透个底,我们自定义的Spring框架的整个设计和Spring框架的设计还是有一定的出入的,因为Spring框架的底层是很复杂的,它进行了很深入的封装,并且对外提供了很好的扩展性,大家需要明确这一点。

至于,我们为何要自定义Spring IoC功能,我想是有以下几个目的。

  • 了解Spring底层对对象的大体管理机制。
  • 了解设计模式在具体的开发中的使用。这个目的才是我们最终的真正目的,因为我们学完23种设计模式之后,就要在具体的开发中去用一用这些设计模式了,看一下它们到底是怎么去使用的。
  • 以后如果你要学习Spring源码的话,那么通过该案例的实现,就可以降低你学习Spring源码的入门成本了。
  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2021-10-19 11:44:20  更:2021-10-19 11:44:47 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 21:05:19-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码