作者:DeppWang原文地址

我通过实现一个简易的 Spring IoC 容器,算是真正入门了 Spring 框架。本文是对实现过程的一个总结提炼,需要配合源码阅读源码地址

结合本文和源码,你应该可以学到:Spring 的原理和 Spring Boot 的原理。

Spring 框架是 Java 开发的,Java 是面向对象的语言,所以 Spring 框架本身有大量的抽象、继承、多态。对于初学者来说,光是理清他们的逻辑就很麻烦,我摒弃了那些包装,只实现了最本质的功能。代码不是很严谨,但只为了理解 Spring 思想却够了。

下面正文开始。

零、Spring 的作用

在没有 Spring 框架的远古时代,我们业务逻辑一般长这样:

public class PetStoreService {
AccountDao accountDao = new AccountDao();
} public class AccountDao {
} PetStoreService petStoreService = new PetStoreService();

到处都是 new 关键字,需要开发人员显式的使用 new 关键字来创建对象(实例)。这样有很多弊端,如,创建的对象太多(多次创建多个对象),耦合性太强(默认 new),等等。

有个叫 Rod Johnson 老哥对此很不爽,就开发了一个叫 Spring 的框架,就是为了干掉 new 关键字(哈哈,我杜撰的,只是为了说明 Spring 的作用)。

有了 Spring 框架,由框架来新建对象,管理对象,并处理对象之间的依赖,我们程序员就可以专注于业务逻辑(专心搬砖),不用关心对象的创建了。我们来看看 Spring 框架是如何实现的吧。

注:以下 Spring 框架简写为 Spring

本节源码对应:v0

一、实现「实例化 Bean 」

首先,我们需要标记哪些类交给 Spring 管理,可以借助 xml 标记,将其标记为 <bean>

<!--petstore-v1.xml-->
<bean id="petStore" class="org.deppwang.litespring.v1.service.PetStoreService"></bean>
<bean id="accountDao" class="org.deppwang.litespring.v1.dao.AccountDao"></bean>

Spring 的第一步操作就是根据 xml 的标记来实例化类,在 Spring 中,我们管类叫 Bean,所以实例化类也可以称为实例化 Bean。

Spring 如何根据 xml 配置来实现实例化类?

大致可以分为三步(配合源码 v1 阅读):

  1. 从 xml 配置文件获取 Bean 信息:id、全限定名,将其作为 BeanDefinition(Bean 定义类)的属性

    BeanDefinition bd = new BeanDefinition(id, beanClassName);
  2. 使用一个 Map 存放所有 BeanDefinition,此时 Spring 本质上是一个 Map,存放 BeanDefinition

    private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>(64);
  3. 当获取 Bean 实例时,通过类加载器,根据全限定名,得到其类对象,通过类对象利用反射创建 Bean 实例

    return Thread.currentThread().getContextClassLoader().loadClass(bd.getBeanClassName()).newInstance();

关于类加载和反射,前者可以看看《深入理解 Java 虚拟机》第 7 章,后者可以看看 Spring 中的反射与反射的原理。本文只学习 Spring,这两个知识点不做深入讨论。

名词解释:

  • 全限定名:指编译后的 class 文件在 jar 包中的路径

本节源码对应:v1

二、实现「填充属性(依赖注入)」


public class PetStoreService {
AccountDao accountDao;
}

当不默认 new 时,实现实例化 Bean 后,此时成员变量(属性)还为 null:

此时需要通过一种方式实现让属性不为 null,我们管这一步叫填充属性。

当一个 Bean 的成员变量类型是另一个 Bean 时,我们可以说一个 Bean 依赖于另一个 Bean。所以填充属性,也可以称为依赖注入(Dependency Injection,简称 DI)。

抛开 Spring 不谈,在正常情况下,我们有两种方式实现依赖注入,1、使用构造方法;2、使用 Setter() 方法。一般使用构造方法(因为依赖可能有多个):

public class PetStoreService {
private AccountDao accountDao;
public PetStoreService(AccountDao accountDao) {
this.accountDao = accountDao;
} public class AccountDao {
} PetStoreService petStore = new PetStoreService(new AccountDao()); // 将依赖 new AccountDao() 注入 petStore

Spring 也是通过这两种方式来实现依赖注入。

我们需要告诉 Spring,需要为类使用什么方式注入依赖,注入什么依赖。比如,我们想 Spring 使用构造函数的方式,注入 AccountDao 依赖。

    <bean id="petStore" class="org.deppwang.litespring.v2.service.PetStoreService">
<constructor-arg ref="accountDao"/>
</bean>

Spring 如何根据 <constructor-arg> 来实现依赖注入?

大致也分为 3 步(配合源码 v2 阅读):

  1. 从 xml 中获取构造函数的参数实例的 id,存放到 BeanDefinition 的 constructorArguments 中

    bd.getConstructorArgumentValues().add(argumentName);
  2. 通过反射得到 PetStoreService 所有的构造函数(Constructor 对象),找到参数跟 constructorArguments 一致的 Constructor 对象

    Constructor<?>[] candidates = beanClass.getDeclaredConstructors();
  3. 通过 constructorArguments 获取到所有参数实例,再利用反射,通过 Constructor 对象实现填充属性。

    return constructorToUse.newInstance(argsToUse);

基于 Setter() 方法实现依赖注入的方式跟构造方法差不多,源码中有实现,请看源码。

实际上,Spring 默认先尝试使用构造函数注入依赖,当没有配置 <constructor-arg> 时,使用 <property>

因为 Spring 实现了依赖注入,所以我们程序员没有了创建对象的控制权,所以也被称为控制反转(Inversion of Control,简称 IoC)。因为 Spring 使用 Map 管理 BeanDefinition,我们也可以将 Spring 称为 IoC 容器。

本节源码对应:v2

三、使用「单例模式、工厂方法模式」

前面两步实现了获取 Bean 实例时创建 Bean 实例,但 Bean 实例经常使用,不能每次都新创建。其实在 Spring 中,一个 Bean 只对应一个 Bean 实例,这需要使用单例模式。

单例模式:一个类有且只有一个实例

Spring 使用类对象创建 Bean 实例,是如何实现单例模式的?

Spring 其实使用一个 Map 存放所有 Bean 实例。创建时,先看 Map 中是否有 Bean 实例,没有就创建;获取时,直接从 Map 中获取。这种方式能保证一个类只有一个 Bean 实例。

private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(64);

因为存放 Bean 实例也是 Map,这是除 beanDefinitionMap 外,Spring 称为 IoC 容器的另一个原因。

我们将创建对象的控制权交给 Spring(BeanIocContainer.java),我们可以认为 BeanIocContainer.java 是一个创建对象的工厂(专门生产对象),也可以称为简单工厂。它实现了创建对象和使用对象分离。

Spring 为了使用不同的方式均可实现实例化 Bean,不能只是简单工厂,需要使用工厂方法模式。

工厂方法模式:定义一个用于创建对象的接口,让子类决定实例化哪一个类。Factory Method 使一个类的实例化延迟到其子类。来源:《Head First 设计模式》

简单的理解就是:将创建对象的方法抽象,作为一个工厂方法。

这里的「让子类决定实例化哪一个类」,也可以看成让子类决定如何实现实例化类。

我们可以把工厂方法模式理解为简单工厂的升级版,可通过子类实现多种方式创建对象,是一种简单工厂的「多态」。

早期 Spring 使用 Bean 的策略是用到时再实例化所用 Bean,杰出代表是 XmlBeanFactory。后期为了实现更多的功能,新增了 ApplicationContext,初始化时就实例化所有 Bean,两者都继承于 BeanFactory 接口。


实现:将 BeanIocContainer 修改为 BeanFactory 接口,只提供 getBean() 方法。实现不同的子类对应不同什么的方式实例化 Bean。

Spring 使用 **getBean() **作为工厂方法。getBean() 包含创建对象的方法。

本节源码对应:v3

四、实现「注解」

在业务开发中,如果每个业务类均设置构造函数,并且需要在 xml 中配置,那么就太繁琐,还容易出错。Spring 从 2.5ref 开始,新增了注解,可以使用注解来替代业务类的 xml 配置和构造函数。

  • 使用 @Component 注解代替 <bean>
  • 使用 @Autowired 注解代替 <constructor-arg> + 构造函数。
@Component
public class PetStoreService {
@Autowired
private AccountDao accountDao;
}

Spring 如何根据注解来实现实例化 Bean 和依赖注入?或者说,这两个注解起了什么作用?

1、@Component 用于生成 BeanDefinition,实现原理(配合源码 v4 阅读):

  • 根据 <context:component-scan> 指定路径,找到路径下所有包含 @Component 注解的 Class 文件,作为 BeanDefinition

    String basePackagesStr = ele.attributeValue("base-package");
  • 如何判断 Class 是否有 @Component:利用字节码技术,获取 Class 文件中的元数据(注解),判断元数据中是否有 @Componet

    annotationMetadata.hasAnnotation(Component.class.getName())

2、@Autowired 用于依赖注入,实现原理(配合源码 v4 阅读):

  • 通过反射,得到所有的属性(Field 对象),查看 Field 对象中是否有 @Autowired 类型的注解,有,则使用反射实现依赖注入

    Field[] fields = bean.getClass().getDeclaredFields();
    field.set(bean, getBean(field.getName()));
  • 注意:使用 @Autowired 时,既没有使用构造方法,也没有使用 Setter() 方法

@Component + @Autowired 实现 Spring 对业务类的管理。被 @Component + @Autowired 修饰的业务类算是一种特殊的 Bean。

至此,我们还是在需要通过配置文件来实现组件扫描。有没有完全不使用配置文件的方式?有!

我们可以使用 @Configuration 替代配置文件,并使用 @ComponentScan 来替代配置文件的 <context:component-scan>

@Configuration // 将类标记为 @Configuration,代表这个类是相当于一个配置文件
@ComponentScan // ComponentScan 扫描 PetStoreConfig.class 所在路径及其所在路径所有子路径的文件
public class PetStoreConfig {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(PetStoreConfig.class);
PetStoreService userService = context.getBean(PetStoreService.class);
userService.getAccountDao();
}
}

使用注解其实跟使用 xml 配置文件一样,目的是将配置类作为入口,实现扫描组件,将其加载进 IoC 容器中的功能。

AnnotationConfigApplicationContext 是专为针对配置类的启动类。其实现机制,可以 Google 查阅。

前面说:在 Spring 中,我们管类叫 Bean。其实不完全正确,类要称为 Bean,需要满足一个条件:

  • 当有成员变量时,要么有 @Autowired 注解,要么有对应的构造函数或者 Setter() 方法

即可以被 Spring 管理的类,称为 Bean。

名词解释:

  • Component:组件
  • Autowired:自动装配

本节源码对应:v4

五、注解 @Bean

只要类是一个 Bean,就可以由 Spring 管理。

业务类为了减少配置,可使用 @Component + @Autowired 实现依赖注入。

将其他 Bean 注入容器时,虽然可以通过 xml 实现,但仍然比较麻烦,Spring 提供了一个注解 @Bean,当一个方法标记为 @Bean 时,它的返回值将被注入容器。

举个栗子:我们可以将一个线程池 Executor 实例注入容器,再 @Autowired 使用:

@Configuration
public class BeanConfig {
@Bean
public ExecutorService sendMessageExecutor() {
ExecutorService executor = new ThreadPoolExecutor(2, 2,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(1024), new ThreadPoolExecutor.AbortPolicy());
return executor;
}
} public class BeanTest {
@Autowired
ExecutorService service;
}

这如果用 xml 来实现,就比较麻烦了,还不直观。

注意:@Bean 需要和 @Configuration 一起使用

本节源码对应:v5

六、Spring Boot 原理

前面说到了 @Configuration 和 @ComponentScan,这就不得不提 Spring Boot。因为 Spring Boot 就使用了 @Configuration 和 @ComponentScan,你可以点开 @SpringBootApplication 看到。

我们发现,Spring Boot 启动时,并没有使用 AnnotationConfigApplicationContext 来指定启动某某 Config 类。这是因为它使用了 @EnableAutoConfiguration 注解。

Spring Boot 利用了 @EnableAutoConfiguration 来自动加载标识为 @Configuration 的配置类到容器中。Spring Boot 还可以将需要自动加载的配置类放在 spring.factories 中,Spring Boot 将自动加载 spring.factories 中的配置类。spring.factories 需放置于META-INF 下。

如 Spring Boot 项目启动时,autocofigure 包中将自动加载到容器的(部分)配置类如下:

以上也是 Spring Boot 的原理。

在 Spring Boot 中,我们引入的 jar 包都有一个字段,starter,我们叫 starter 包。

标识为 starter(启动器)是因为引入这些包时,我们不用设置额外操作,它能被自动装配,starter 包一般都包含自己的 spring.factories。如 spring-cloud-starter-eureka-server:

如 druid-spring-boot-starter:

有时候我们还需要自定义 starter 包,比如在 Spring Cloud 中,当某个应用要调用另一个应用的代码时,要么调用方使用 Feign(HTTP),要么将被调用方自定义为 starter 包,让调用方依赖引用,再 @Autowired 使用。此时需要在被调用方设置配置类和 spring.factories:

@Configuration
@ComponentScan
public class ProviderAppConfiguration {
} // spring.factories
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.amy.cloud.amycloudact.ProviderAppConfiguration

当然,你也可以把这两个文件放在调用方(此时要指定扫描路径),但一般放在被调用方。ps:如果你两个应用的 base-package 路径一样,那么可以不用这一步。

说了 Spring Boot,那么在 Spring MVC,如何将引入 jar 包的组件注入容器?

  • 跟扫描本项目包一样,在 xml ,增加引入 jar 包的扫描路径:
<context:component-scan base-package="引入 jar 包的 base-package" />
...

嗯,本节没有源码

七、结语

Spring 的原理离不开两个关键词:反射和注解。

  • 反射:在创建 Bean 实例和依赖注入是都需要使用反射。
  • 注解:使用注解可大大提升代码可阅读性,降低复杂度。注解本质上是作为一个标识,获取注解时需要使用字节码技术。

现在我们一般很少使用 xml 来设置 bean,但了解了 xml 可以更好的理解 Spring 注解的原理。

来个注解小结:

  • @Component 作为组件标识,代表需要 Spring 管理
  • @Autowired 用于判断是否需要依赖注入,代替构造函数
  • @ComponentScan 指定组件扫描路径,不指定即为当前路径
  • @Configuration 代表配置类,作为 Spring 寻找需要被管理 Bean 的入口
  • @Bean 实现将任意 Bean 注入容器
  • @EnableAutoConfiguration 实现自动加载配置类到容器

以上实现了一个简易的 Spring IoC 容器,顺便说了一下 Spring Boot 原理。Spring 还有很多重要功能,如:处理对象之间的依赖、管理 Bean 生命周期、AOP 的实现,等等。后续有机会再做一次分享。

全文完。

造轮子:实现一个简易的 Spring IoC 容器的更多相关文章

  1. 曹工说Tomcat4:利用 Digester 手撸一个轻量的 Spring IOC容器

    一.前言 一共8个类,撸一个IOC容器.当然,我们是很轻量级的,但能够满足基本需求.想想典型的 Spring 项目,是不是就是各种Service/DAO/Controller,大家互相注入,就组装成了 ...

  2. 手写一个最简单的IOC容器,从而了解spring的核心原理

    从事开发工作多年,spring源码没有特意去看过.但是相关技术原理倒是背了不少,毕竟面试的那关还是得过啊! 正所谓面试造火箭,工作拧螺丝.下面实现一个最简单的ioc容器,供大家参考. 1.最终结果 2 ...

  3. Summer——从头开始写一个简易的Spring框架

    Summer--从头开始写一个简易的Spring框架                ​ 参考Spring框架实现一个简易类似的Java框架.计划陆续实现IOC.AOP.以及数据访问模块和事务控制模块. ...

  4. spring揭密学习笔记(3)-spring ioc容器:Spring的IoC容器之BeanFactory

    1. Spring的IoC容器和IoC Service Provider的关系 Spring的IoC容器和IoC Service Provider所提供的服务之间存在一定的交集,二者的关系如图4-1所 ...

  5. IoC原理-使用反射/Emit来实现一个最简单的IoC容器

    从Unity到Spring.Net,到Ninject,几年来陆陆续续用过几个IoC框架.虽然会用,但也没有一直仔细的研究过IoC实现的过程.最近花了点时间,下了Ninject的源码,研究了一番,颇有收 ...

  6. Spring IoC容器的初始化过程

    Spring IoC容器的初始化包括 BeanDefinition的Resource定位.载入和注册 这三个基本的过程.IoC容器的初始化过程不包含Bean依赖注入的实现.Bean依赖的注入一般会发生 ...

  7. 【Spring】非Spring IOC容器下获取Spring IOC上下文的环境

    前言 在Spring Web项目中,有些特殊的时候需要在非Spring IOC容器下获取Spring IOC容器的上下文环境,比如获取某个bean. 版本说明 声明POM文件,指定需引入的JAR. & ...

  8. 对Spring IoC容器实现的结构分析

    本文的目标:从实现的角度来认识SpringIoC容器. 观察的角度:从外部接口,内部实现,组成部分,执行过程四个方面来认识SpringIoC容器. 本文的风格:首先列出SpringIoC的外部接口及内 ...

  9. spring IOC容器实例化Bean的方式与RequestContextListener应用

    spring IOC容器实例化Bean的方式有: singleton 在spring IOC容器中仅存在一个Bean实例,Bean以单实例的方式存在. prototype 每次从容器中调用Bean时, ...

随机推荐

  1. priority_queue 中存放pair时,自定义排序的写法

    struct cmp {template <typename T, typename U> bool operator()(T const &left, U const & ...

  2. Android课程设计——博学谷1.0

    本文讲述了如何应用大三下学期智能移动终端开发技术课程所学知识,完成包含服务器端.客户端程序的应用——博学谷登录模块的开发,结合java语言基本知识,例如:字符串.列表.类.数据库读写等,设计.实现一个 ...

  3. 升级 nop 4.1 Incorrect syntax near 'OFFSET'. Invalid usage of the option NEXT in the FETCH statement.

    Incorrect syntax near 'OFFSET'.  Invalid usage of the option NEXT in the FETCH statement. nop.web 项目 ...

  4. 终极解决方案之——Centos7由于误删或更新python导致 No module named yum

    之前由于不懂yum和python之间的关系,因为一直在学python3,看到系统里/usr/lib下的python2我就直接删了,结果... 可能还有人是因为python升级的原因,即系统自带的pyt ...

  5. GANs和低效映射

    生成对抗网络(GANs)被誉为生成艺术领域的下一纪元,这是有充分理由的.新技术一直是艺术的驱动因素,从颜料的发明到照相机再到Photoshop-GAN是自然而然的.例如,考虑下面的图片,由埃尔加马勒发 ...

  6. 密钥对格式转换:JKS到PEM

    此处脚本用途:Tomcat的JKS转换成Nginx的PEM格式. #!/bin/bash export JKS=$1 export PASS=$2 NAME=$(basename "$JKS ...

  7. mac主机无法访问虚拟机中的Ubuntu运行的web服务

    第一点: 检查主机和虚拟机之间是否连通: 在mac主机中ping 虚拟机ip 虚拟机ip可以在虚拟机命令行中输入 ifconfig查看 第二点: 如果不能ping通,改变虚拟机的网络连接方式为:桥接模 ...

  8. 谈谈flex布局实现水平垂直居中

    我们在这要谈的是用flex布局来实现水平和垂直居中.随着移动互联网的发展,对于网页布局来说要求越来越高,而传统的布局方案对于实现特殊布局非常不方便,比如垂直居中.所以09年,W3C 提出了一种新的方案 ...

  9. 展示html/javascript/css------Live-Server

    Live-server简介 这是一款带有热加载功能的小型开发服务器.用它来展示你的HTML / JavaScript / CSS,但不能用于部署最终的网站. 官网地址:https://www.npmj ...

  10. python 报错:a bytes-like object is required, not 'str'

    核心代码: def ipPools(numPage): headers = randomHeads() url = 'http://www.xicidaili.com/nn/' saveFsvFile ...