中级17 - Spring IoC容器原理与手写简单实现

一切开始之前,先了解下 JavaBean 是什么,它是一种标准和约定。
一个 JavaBean:

  • 所有的属性都是 private(通过 getter/setter 进行读写)
  • 有一个 public 无参数的构造器
  • 实现了 Serializable(序列化,注意 Serializable 接口没有方法也没有字段,更多的是一种规范和约定)

1. Spring 是什么

Srping 是自动化管理 Java 对象和其中依赖关系的容器。

  • Java世界中Web容器的事实标准
  • Spring 容器 - 一个 IoC 容器
    • Spring MVC - 基于 Spring 和 Servlet 的 Web 应用框架
      • Spring Boot - 集成度和自动化程度更高(内嵌了 Servlet 容器)

-let 词根,“小”的意思,servlet 小服务器(service + let)。
Servlet 将网络请求封装成对象,交给上层 WebApp(spring容器、mvc、boot)。
Servlet <-> HttpServletRequest/HttpServletResponse <-> WebApp。
常见的 servlet 有:

  • tomcat
  • jetty

没有 Spring 怎么办?
**

  • 一个 main 程序走天下:轻量简单,但是规模大了后难以维护。
  • 拆分并且手动管理:优点方便测试、共同开发、维护,但缺点还是规模更大后,依赖关系太复杂。

Spring 的出现解放了我们的双手。

2. Spring 最简单的用法

通过配置一种“上古”的 xml 来使用。
xml 配置文件中声明了两个 Bean, 而这两个 Bean 之间存在依赖关系,因此为 OrderService 中的成员属性 OrderDao 添加了注解,用于对 OrderDao 自动装配。

在这里,容器就是 BeanFactory。有了容器之后,向容器索要 Bean。

// maven 依赖
<!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.2.6.RELEASE</version>
</dependency>
package spring.demo;

public class OrderDao {
    public void select() {
        System.out.println("select!");
    }
}
package spring.demo;

import org.springframework.beans.factory.annotation.Autowired;

public class OrderService {

    @Autowired // 加了这个注解才会自动装配 OrderDao,否则 OrderService 对象中的 orderDao 是 null
    private OrderDao orderDao;

    public void doSomething() {
        orderDao.select();
    }
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">

    <context:annotation-config /> <!-- 这句不加的话,即使写了上面的注解也不生效 -->
    <!-- bean definitions here -->
    <bean id="orderDao" class="spring.demo.OrderDao"/>
    <bean id="orderService" class="spring.demo.OrderService"/>
</beans>
package spring.demo;

import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class SpringMain {
    public static void main(String[] args) {
        BeanFactory beanFactory = new ClassPathXmlApplicationContext("classpath:spring/config.xml");

        OrderService orderService = (OrderService)beanFactory.getBean("orderService");
        OrderDao orderDao = (OrderDao) beanFactory.getBean("orderDao");

        System.out.println(orderService);
        orderService.doSomething();
        System.out.println(orderDao);
    }
}

现在没有写 new,却拿到了 OrderService 对象(一个Bean),默认是单例模式,也就是 OrderService 对象中的 OrderDao 和再次通过 getBean 得到的 OrderDao 对象是同一个对象。


image.png
image.png

假如没有使用 Spring,那么可能要自己手动创建,涉及到的各种对象的各种 new。

3. Spring 容器核心概念

  • Bean

容器中的最小工作单元,通常为一个 Java 对象

  • BeanFactory/ApplicationContext

容器本身对应的 Java 对象

  • 依赖注入(DI)

容器负责注入所有的依赖

  • 控制反转(IoC)

用户将控制权交给了 Spring 容器来进行自动装配

4. 手写一个简单的 IoC 容器

  • 定义 Bean
  • 加载 Bean 的定义
  • 实例化 Bean
  • 查找依赖,实现依赖注入
  • 要什么 Bean 就设置成什么 Bean

目录结构:


image.png
image.png

使用方法是在字段是声明注解(部分代码不再罗列,这里仅供举例):

import org.springframework.beans.factory.annotation.Autowired;

public class OrderService {
    @Autowired private OrderDao orderDao;
    @Autowired private UserService userService;

    public void createOrder() {
        orderDao.createOrder(userService.getCurrentLoginUser());
    }
}

具体实现:

Java 从初代就支持了 .properties 格式的配置文件,该文件用来存储简单的基于 key-value pairs 的参数。该配置文件处于被编译的代码之外。

  1. 先写一个简单的 beans.properties 配置文件,定义了 Bean 的名字和对应的实现类:
# Bean 名字 和 Bean 的全限定类名
orderDao=com.github.hcsp.ioc.OrderDao
userDao=com.github.hcsp.ioc.UserDao
userService=com.github.hcsp.ioc.UserService
orderService=com.github.hcsp.ioc.OrderService
  1. MyIoCContainer 容器:
import org.springframework.beans.factory.annotation.Autowired;

import java.io.IOException;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class MyIoCContainer {
    // 实现一个简单的IoC容器,使得:
    // 1. 从beans.properties里加载bean定义
    // 2. 自动扫描bean中的@Autowired注解并完成依赖注入

    // 定义一个容器! 存放 bean 的名字到 bean 实例对象的映射
    private Map<String, Object> beans = new HashMap<>();

    /**
     * 依赖注入
     *
     * @param beanInstance bean 的 实例
     */
    private void dependencyInject(Object beanInstance) {
        // 拿到带有 @AutoWired 注解的 fields
        List<Field> fieldsToBeAutoWired = Stream.of(beanInstance.getClass().getDeclaredFields())
                .filter(field -> field.getAnnotation(Autowired.class) != null)
                .collect(Collectors.toList());
        // 为当前 bean 对象的需要依赖的字段注入依赖(设置字段值)
        fieldsToBeAutoWired.forEach(field -> {
            String fieldName = field.getName(); // 加了 @AutoWired 的字段名即是所要依赖的 bean 的名字
            Object dependencyBeanInstance = beans.get(fieldName); // 所依赖的 bean 实例
            try {
                field.setAccessible(true); // 设置为 true 用来压制针对被反射对象的访问检查
                field.set(beanInstance, dependencyBeanInstance); // 从而可以在这里设置当前 bean 的私有字段
            } catch (IllegalAccessException e) {
                throw new RuntimeException(e);
            }
        });
    }

    /**
     * 启动该容器
     */
    public void start() {
        // bean 的初始化

        Properties properties = new Properties();

        // 从 InputStream 中读取属性列表(键值对)
        try {
            properties.load(MyIoCContainer.class.getResourceAsStream("/beans.properties"));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        System.out.println(properties);

        properties.forEach((beanName, beanClassName) -> {
            try {
                // 通过反射拿到 bean 的实例并放入容器中
                Class<?> klass = Class.forName((String) beanClassName);
                Object beanInstance = klass.getConstructor().newInstance();
                beans.put((String) beanName, beanInstance);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        });

        // 使用反射,处理依赖关系,注入依赖
        beans.forEach((beanName, beanInstance) -> dependencyInject(beanInstance));
    }

    /**
     * 从容器中获取一个bean
     *
     * @param beanName bean 的名字
     * @return 返回 bean 的实例
     */
    public Object getBean(String beanName) {
        return beans.get(beanName);
    }

    public static void main(String[] args) {

        MyIoCContainer container = new MyIoCContainer();
        container.start();
        OrderService orderService = (OrderService) container.getBean("orderService");
        orderService.createOrder();
    }
}

即使是两个依赖字段存在循环依赖也没关系,因为在创造期间,会先各自创建出实例对象。相关依赖字段此时是 null,不影响 bean 的创建。而创建完之后,再回头对字段注入所依赖的 bean。

当然,以上只是个简单的实现,实际的 Spring 中还可以在构造器上使用 @Autowired 注解,而不推荐在私有字段上使用。这样即使不是用 Spring,比如写测试代码的时候,还是可以方便的 new 一个实例,否则,私有字段还要通过反射一顿操作才能创建实例进行测试。

另外,实际应用中 @Autowired 也不推荐使用了,更推荐 @Inject 注解,这样除了 Spring 之外,还可能受到其他类似框架的识别。

所谓的 Spring 只不过是在以上基础上扩充了无穷无尽的功能,比如支持 xml 中配置依赖,支持构造器注入(调用哪个构造器,传递哪些参数)等等,从一个很简单的思想不断扩充成庞大的体系。

5. Spring 启动过程浅析

  • 在 xml 里中定义 Bean
  • BeanDefinition 的加载和解析
  • Bean 的实例化和依赖注入
  • 对外提供服务

建议 debug Spring 源码的时候,要带着目前来进行,不要在无关细节里浪费太多时间。


image.png
image.png

image.png
image.png

image.png
image.png

6. 参考

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,919评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,567评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,316评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,294评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,318评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,245评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,120评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,964评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,376评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,592评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,764评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,460评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,070评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,697评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,846评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,819评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,665评论 2 354