手撸一个 Spring —— 1. 基于 xml

Spring 框架算是 Java 后端面试的重中之重的了。那么怎样才算真正吃透 Spring 了呢?

以自己是否能造出轮子来衡量学习的效果。 ——轮子哥

所以,我们就来实现一个类 Spring 的 IOC 容器,作为项目经验的同时,还能引导面试官询问相关的技术实现,可谓一举两得。

目前,整个框架已经实现的内容有:

  • 基于 xml 配置文件注入
  • 基本属性注入
  • 引用依赖注入
  • singleton 与 prototype 模式注入
  • 基于注解注入
  • 基于该框架的简单 Web 框架(类似于 SpringMVC)实现

本节首先实现一个基于 xml 配置文件注入依赖的容器,并解决各种依赖注入问题。

本节最终的代码为:https://github.com/CN-GuoZiyang/My-Spring-IOC/tree/82967670e52fe66ad55a6b2a539dbb4d48b46805

最终效果

我们首先来看看本节最后的效果,容器会首先读取以下配置文件,并注入容器:

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

    <bean id="helloWorldService" class="top.guoziyang.main.service.HelloWorldServiceImpl" scope="prototype">
        <property name="text" value="Hello World"></property>
    </bean>

    <bean id="wrapService" class="top.guoziyang.main.service.WrapService">
        <property name="helloWorldService" ref="helloWorldService"></property>
    </bean>

</beans>

这里注入了两个 bean,第一个 helloWorldService 是以 prototype 方式注入的,这个 bean 有一个 String类型的属性 text,在构造时应当注入 “Hello World” 字符串;第二个 bean 是 wrapService,以 singleton 方式注入,同时它有一个属性 helloWorldService ,指向了第一个 bean。

这个配置文件的格式类似于 Spring 的配置文件格式,这两个 bean 的实现如下:

package top.guoziyang.main.service;

public class HelloWorldServiceImpl implements HelloWorldService {
    private String text;
    @Override
    public void saySomething() {
        System.out.println(text);
    }
}
package top.guoziyang.main.service;

public class WrapService {
    private HelloWorldService helloWorldService;
    public void say() {
        helloWorldService.saySomething();
    }
}

ApplicationContext 全局上下文

ApplicationContext,即应用程序上下文,是 Spring 框架中最为核心的类,也是 Spring 的入口类。该接口继承自 BeanFactory 接口,实现了 BeanFactory(实例工厂)的所有功能,还支持资源访问(如 URL 和文件)、事务传播等功能。但是我们此时还是只实现其最核心的功能——实例工厂。

我们首先定义 ApplicationContext 接口:

package top.guoziyang.springframework.context;

/**
 * 应用程序上下文接口
 *
 * @author ziyang
 */
public interface ApplicationContext {
    Object getBean(Class clazz) throws Exception;
    Object getBean(String beanName) throws Exception;
}

这个接口表示程序有两种从 ApplicationContext 获取 bean 实例的方式:通过 Class 对象和通过实例名(一般就是类名)。

我们接着仿照 Spring,编写一个抽象类 AbstractApplicationContext,来实现 ApplicationContext 接口,实现一些通用的方法。注意,在 Spring 中,ApplicationContext 实现 BeanFactory 接口的方式,是在 ApplicationContext 对象的内部,保存了一个 BeanFactory 对象的实例,实质上类似一种代理模式。所以我们也给出一个类似的实现:

package top.guoziyang.springframework.context;

import top.guoziyang.springframework.factory.BeanFactory;

public abstract class AbstractApplicationContext implements ApplicationContext {

    BeanFactory beanFactory;

    @Override
    public Object getBean(Class clazz) throws Exception {
        return beanFactory.getBean(clazz);
    }
    @Override
    public Object getBean(String beanName) throws Exception {
        return beanFactory.getBean(beanName);
    }
}

那么现在,从 ApplicationContext 中取出对象的方法都实现完了,那么 ApplicationContext 具体实现类的工作,就是用某种方式读取配置,然后把对象信息存入到 BeanFactory 中,等待用户来取。

那么在我们编写 ApplicationContext 具体实现类之前,我们先来看看 BeanFactory 接口,这个实例工厂。

BeanFactory 实例工厂

BeanFactory,毫无疑问就是一个工厂,可以将 bean 的信息添加到其中,BeanFactory 就可以根据这些定义生成实例,在需要时可以通过两种 getBean 方法获取到这些实例。

BeanFactory 接口定义如下:

package top.guoziyang.springframework.factory;

import top.guoziyang.springframework.entity.BeanDefinition;

/**
 * Bean工厂接口
 *
 * @author ziyang
 */
public interface BeanFactory {

    Object getBean(String name) throws Exception;
    Object getBean(Class clazz) throws Exception;
    void registerBeanDefinition(String name, BeanDefinition beanDefinition) throws Exception;

}

除了 ApplicationContext 中也有到两种获取 bean 实例的方法以外,还定义了一个方法:void registerBeanDefinition(String name, BeanDefinition beanDefinition) throws Exception;,表示向工厂中注册 Bean 的定义,至于 BeanDefinition 的实现,后面再说。

根据名字来拿 Bean,显而易见是一个类似 Map 的结构,这里我们采用 ConcurrentHashMap 来存储这个结构以保证并发安全。那么这样,两个 getBean 的实现也就很显然了,仿照 Spring 的结构,我们还是先创建一个抽象类 AbstractBeanFactory 来实现 BeanFactory 接口:

package top.guoziyang.springframework.factory;

import top.guoziyang.springframework.entity.BeanDefinition;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public abstract class AbstractBeanFactory implements BeanFactory {

    ConcurrentHashMap<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>();

    @Override
    public Object getBean(String name) throws Exception {
        BeanDefinition beanDefinition = beanDefinitionMap.get(name);
        if(beanDefinition == null) return null;
        if(!beanDefinition.isSingleton() || beanDefinition.getBean() == null) {
            return doCreateBean(beanDefinition);
        } else {
            return beanDefinition.getBean();
        }
    }

    @Override
    public Object getBean(Class clazz) throws Exception {
        BeanDefinition beanDefinition = null;
        for(Map.Entry<String, BeanDefinition> entry : beanDefinitionMap.entrySet()) {
            Class tmpClass = entry.getValue().getBeanClass();
            if(tmpClass == clazz || clazz.isAssignableFrom(tmpClass)) {
                beanDefinition = entry.getValue();
            }
        }
        if(beanDefinition == null) {
            return null;
        }
        if(!beanDefinition.isSingleton() || beanDefinition.getBean() == null) {
            return doCreateBean(beanDefinition);
        } else {
            return beanDefinition.getBean();
        }
    }

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

    /**
     * 创建Bean实例
     * @param beanDefinition Bean定义对象
     * @return Bean实例对象
     * @throws Exception 可能出现的异常
     */
    abstract Object doCreateBean(BeanDefinition beanDefinition) throws Exception;

    public void populateBeans() throws Exception {
        for(Map.Entry<String, BeanDefinition> entry : beanDefinitionMap.entrySet()) {
            doCreateBean(entry.getValue());
        }
    }
}

这里被注册的 BeanDefinition,如果是 singleton 的 bean,在 BeanDefinition 对象生成时,就已经生成好了对应的实例,在 getBean 获取的时候就可以直接获取,否则就需要调用 doCreateBean 方法来即时创建一个实例。doCreateBean 方法是一个抽象方法,表示真正创建 Bean 实例对象的操作,留给具体的实现类来实现。

我们要实现的 BeanFactory,是一个可以自动注入属性的 BeanFactory,可以创建完成实例对象后,注入其中的属性,如果属性是一个对象引用,那么就再去创建那个被引用的实例对象,并递归地完成属性注入。在 Spring 中,这个实现类叫做 AutowiredCapableBeanFactory。于是,我们的 AutowiredCapableBeanFactory 的实现是这样的:

package top.guoziyang.springframework.factory;

import top.guoziyang.springframework.entity.BeanDefinition;
import top.guoziyang.springframework.entity.BeanReference;
import top.guoziyang.springframework.entity.PropertyValue;
import java.lang.reflect.Field;

public class AutowiredCapableBeanFactory extends AbstractBeanFactory {

    @Override
    Object doCreateBean(BeanDefinition beanDefinition) throws Exception {
        if(beanDefinition.isSingleton() && beanDefinition.getBean() != null) {
            return beanDefinition.getBean();
        }
        Object bean = beanDefinition.getBeanClass().newInstance();
        if(beanDefinition.isSingleton()) {
            beanDefinition.setBean(bean);
        }
        applyPropertyValues(bean, beanDefinition);
        return bean;
    }

    /**
     * 为新创建了bean注入属性
     * @param bean 待注入属性的bean
     * @param beanDefinition bean的定义
     * @throws Exception 反射异常
     */
    void applyPropertyValues(Object bean, BeanDefinition beanDefinition) throws Exception {
        for(PropertyValue propertyValue : beanDefinition.getPropertyValues().getPropertyValues()) {
            Field field = bean.getClass().getDeclaredField(propertyValue.getName());
            Object value = propertyValue.getValue();
            if(value instanceof BeanReference) {
                BeanReference beanReference = (BeanReference) propertyValue.getValue();
                BeanDefinition refDefinition = beanDefinitionMap.get(beanReference.getName());
                if(refDefinition.getBean() == null) {
                    value = doCreateBean(refDefinition);
                }
            }
            field.setAccessible(true);
            field.set(bean, value);
        }
    }
}

这里主要还是使用了反射来创建对象实例,原理比较简单,就不过多说明。doCreateBean 在创建实例最后,需要通过 applyPropertyValues 方法来注入属性,在注入属性过程中,如果发现某个 value 是 BeanReference 类型,即引用类型的,并且在之前没有被创建过,就需要再次调用 doCreateBean 方法来创建。这样就可以解决递归引用的问题。

说了这么多,BeanDefinition到底是什么呢,又从哪里来呢?

BeanDefinition 实例定义

BeanDefinition 的定义如下:

public class BeanDefinition {

    private Object bean;    // 实例化后的对象
    private Class beanClass;
    private String beanClassName;
    private Boolean singleton;    // 是否是单例模式
    private PropertyValues propertyValues;    // Bean的属性

}

PropertyValues 实际上是一个 List,表示一组属性的定义,内部存储的对象是 PropertyValue 对象,表示一个属性定义和其对应的注入属性:

public class PropertyValue {

    private final String name;
    private final Object value;

}

注意这里的 value,如果是引用其他对象的话,value 就是一个 BeanReference 实例,表示对一个对象的引用,而不是立即初始化,因为 BeanDefinition 是在读取配置文件时就被创建的,这时还没有任何 Bean 被初始化,BeanReference 仅仅是一个记录而已:

public class BeanReference {
    private String name;
    private Object bean;
}

回到正题,BeanDefinition 从哪里来?当前的实现是从 xml 文件中读取的。我们来定义一个读取器接口,它只有一个很简单的方法,就是从某个位置以某种方式读取 bean 的配置:

package top.guoziyang.springframework.reader;

/**
 * Bean定义读取
 *
 * @author ziyang
 */
public interface BeanDefinitionReader {

    void loadBeanDefinitions(String location) throws Exception;

}

我们可以定义一个抽象的读取器,来将一些通用代码抽取出来:

/**
 * BeanDefinitionReader实现的抽象类
 *
 * @author ziyang
 */
public abstract class AbstractBeanDefinitionReader implements BeanDefinitionReader {

    private Map<String, BeanDefinition> registry;

    private ResourceLoader resourceLoader;

    public AbstractBeanDefinitionReader(ResourceLoader resourceLoader) {
        this.registry = new HashMap<>();
        this.resourceLoader = resourceLoader;
    }

    public Map<String, BeanDefinition> getRegistry() {
        return registry;
    }

    public ResourceLoader getResourceLoader() {
        return resourceLoader;
    }
}

其中,registry 这个 Map,用来存储 Bean 的名称和 BeanDefinition 的映射。在完全读取完毕后会将 registry 返回出去,供 BeanFactory 使用。

这里的抽象出资源这个概念,定义接口 Resource:

package top.guoziyang.springframework.io;

import java.io.InputStream;

/**
 * 获取资源(配置文件)的接口
 *
 * @author ziyang
 */
public interface Resource {

    InputStream getInputStream() throws Exception;

}

各种资源最终都可以抽象成输入流。从文件读取的实现类就是 UrlResource:

package top.guoziyang.springframework.io;

import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;

/**
 * 通过Url的方式获取资源
 *
 * @author ziyang
 */
public class UrlResource implements Resource {

    private URL url;

    public UrlResource(URL url) {
        this.url = url;
    }

    public InputStream getInputStream() throws Exception {
        URLConnection urlConnection = url.openConnection();
        urlConnection.connect();
        return urlConnection.getInputStream();
    }
}

这里的 URL 是统一资源定位符,可以表示网络路径,也可以表示本地的文件路径。

最终的 ResourceLoader 就是用来实际加载使用,不过目前只实现了 xml 文件方式:

package top.guoziyang.springframework.io;

import java.net.URL;

/**
 * 资源加载器,通过路径加载资源
 *
 * @author ziyang
 */
public class ResourceLoader {

    public Resource getResource(String location) {
        URL url = this.getClass().getClassLoader().getResource(location);
        return new UrlResource(url);
    }

}

最终,最后的具体实现类实现了对配置文件的读取,由于我们读取的是 xml 配置文件,所以我们的实现类名叫 XmlBeanDefinitionReader,使用 Java 内置的 xml 解析器,可以将其解析为 Document,具体的解析过程较长,不贴代码了,文件参考这里

回到 ApplicationContext

这就是完整的,一个 Bean 从配置文件到被实例化的过程。首先从文件中被读取解析成 BeanDefinition,接着解析 BeanDefinition 实例化成实际的对象。那么,最开头的 ApplicationContext 的具体实现类所要做的,就很简单了,只需要创建一个 BeanDefinitionReader 读取配置文件,并且将读取到的配置存到 BeanFactory 中,并且由 BeanFactory 创建对应的实例对象即可。由于我们是读取 xml 文件,那么这个 ApplicationContext 的实现类,就叫 ClassPathXmlApplicationContext。

public class ClassPathXmlApplicationContext extends AbstractApplicationContext {

    private final Object startupShutdownMonitor = new Object();
    private String location;

    public ClassPathXmlApplicationContext(String location) throws Exception {
        super();
        this.location = location;
        refresh();
    }

    public void refresh() throws Exception {
        synchronized (startupShutdownMonitor) {
            AbstractBeanFactory beanFactory = obtainBeanFactory();
            prepareBeanFactory(beanFactory);
            this.beanFactory = beanFactory;
        }
    }

    private void prepareBeanFactory(AbstractBeanFactory beanFactory) throws Exception {
        beanFactory.populateBeans();
    }

    private AbstractBeanFactory obtainBeanFactory() throws Exception {
        XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(new ResourceLoader());
        beanDefinitionReader.loadBeanDefinitions(location);
        AbstractBeanFactory beanFactory = new AutowiredCapableBeanFactory();
        for (Map.Entry<String, BeanDefinition> beanDefinitionEntry : beanDefinitionReader.getRegistry().entrySet()) {
            beanFactory.registerBeanDefinition(beanDefinitionEntry.getKey(), beanDefinitionEntry.getValue());
        }
        return beanFactory;
    }

}

在构建的时候,就会直接调用 refresh 方法,用于刷新 bean,其实就是第一次从文件中读取并解析注入。refresh 方法加了一个 startupShutdownMonitor 锁,来保证并发安全。obtainBeanFactory 方法负责读取并解析文件生成 BeanDefinition,并形成 beanFactory。prepareBeanFactory 方法用于将根据 BeanDefinition 实例化对象,其实就是调用了 beanFactory 的 populateBeans 方法。

测试结果

让我们编写一些测试代码,看看效果:

public class Main {

    public static void main(String[] args) throws Exception {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("application.xml");
        WrapService wrapService = (WrapService) applicationContext.getBean("wrapService");
        wrapService.say();
        HelloWorldService helloWorldService = (HelloWorldService) applicationContext.getBean("helloWorldService");
        HelloWorldService helloWorldService2 = (HelloWorldService) applicationContext.getBean("helloWorldService");
        System.out.println("prototype验证:" + (helloWorldService == helloWorldService2));
        WrapService wrapService2 = (WrapService) applicationContext.getBean("wrapService");
        System.out.println("singleton验证:" + (wrapService == wrapService2));
    }

}

运行结果如下:

Hello World
prototype验证:false
singleton验证:true

这里验证了一下 prototype 和 singleton,这里首先获取了两次 HelloWorldService 的实例,由于这个 Bean 在配置文件中被标为 prototype,所以两次获取到的都不是同一个对象,使用等号比较时得到了 false。而后面获取的 wrapService,和第一次获取的 WrapService 比较,由于是 singleton 的,所以使用等号比较时返回 true。

下一节我们将实现基于注解注入的方式,以摆脱 xml 配置文件。

#春招##Java##Spring#
全部评论
我的天 人均rpc+spring 疯狂的卷😂
点赞 回复
分享
发布于 2021-02-25 14:21
感谢参与【创作者计划2期·技术干货场】!欢迎更多牛油来写干货,瓜分总计20000元奖励!!技术干货场活动链接:https://www.nowcoder.com/link/czz2jsghtlq(参与奖马克杯将于每周五结算,敬请期待~)
点赞 回复
分享
发布于 2021-02-25 17:10
秋招专场
校招火热招聘中
官网直投
我有点想写一个关于springboot扫描器的文章
点赞 回复
分享
发布于 2021-02-26 11:05
太卷了,另外楼主建议一下spring该怎么浅到深的学习呀😂😂😂
点赞 回复
分享
发布于 2021-03-14 03:54
牛逼
点赞 回复
分享
发布于 2021-03-27 23:39
滴滴 设置BeanDefinition属性的时候在读取xml的时候没有设置Beanclass 修改后的如图
点赞 回复
分享
发布于 2022-10-23 23:24 广东

相关推荐

18 60 评论
分享
牛客网
牛客企业服务