实现一个简易的 Spring IoC 容器 - V2EX
deppwxq
V2EX    Java

实现一个简易的 Spring IoC 容器

  •  
  •   deppwxq
    deppwang Apr 19, 2020 3001 views
    This topic created in 2214 days ago, the information mentioned may be changed or developed.

    作者:DeppWang原文地址

    source:https://fernandofranzini.wordpress.com/

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

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

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

    下面正文开始。

    零、前言

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

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

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

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

    有了 Spring 框架,由框架来新建对象,管理对象,并处理对象之间的依赖,我们程序员再也不用 new 业务类对象了。我们来看看 Spring 框架是如何实现的吧。

    注:以下 Spring 框架简写为 Spring

    本节源码对应:v0

    一、实现「实例化 Bean 」

    首先,Spring 需要实例化类,将其转换为对象。在 Spring 中,我们管(业务)类叫 Bean,所以实例化类也可以称为实例化 Bean 。

    早期 Spring 需要借助 xml 配置文件来实现实例化 Bean,可以分为三步(配合源码 v1 阅读):

    1. 从 xml 配置文件获取 Bean 信息,如全限定名等,将其作为 BeanDefinition ( Bean 定义类)的属性
    2. 使用一个 Map 存放所有 BeanDefinition,此时 Spring 本质上是一个 Map,存放 BeanDefinition
    3. 当获取 Bean 实例时,通过类加载器,根据全限定名,得到其类对象,通过类对象利用反射创建 Bean 实例

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

    名词解释:

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

    本节源码对应:v1

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

    实现实例化 Bean 后,此时成员变量(引用)还为 null:

    此时需要通过一种方式实现,让引用指向实例,我们管这一步叫填充属性。

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

    抛开 Spring 不谈,在正常情况下,我们有两种方式实现依赖注入,1 、使用 Setter() 方法,2 、使用构造方法。使用 Setter() 方法如下:

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

    其实早期 Spring 也是通过这两种方式来实现依赖注入的。下面是 Spring 通过 xml 文件 + Setter() 来实现依赖注入的步骤(配合源码 v2 阅读):

    1. 给 PetStoreService 添加 Setter() 方法,并稍微修改一下 xml 配置文件,添加 <property>,代表对应 Setter() 方法。
    2. 从 xml 配置文件获取 Bean 的属性 <property>,存放到 BeanDefinition 的 propertyNames 中。
    3. 通过 propertyName 获取属性实例,利用反射,通过 Setter() 方法实现填充属性(依赖注入)

    基于构造函数实现依赖注入的方式跟 Setter() 方法差不多,感兴趣可以 Google 搜索查看。

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

    本节源码对应:v2

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

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

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

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

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

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

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

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

    我们将 BeanIocContainer 修改为 BeanFactory 接口,只提供 getBean() 方法。创建( IoC )容器由其子类自己实现。

    ApplicationContext 和 BeanFactory 的区别:ApplicationContext 初始化时就实例化所有 Bean,BeanFactory 用到时再实例化所用 Bean 。

    本节源码对应:v3

    三、实现「注解」

    前面使用 xml 配置文件的方式,实现了实例化 Bean 和依赖注入。这种方式比较麻烦,还容易出错。Spring 从 2.5<sup>ref</sup> 开始可使用注解替代 xml 配置文件。比如:

    1. 使用 @Component 注解代替 <bean>
    2. 使用 @Autowired 注解代替 <property>

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

    • 根据 component-scan 指定路径,找到路径下所有包含 @Component 注解的 Class 文件,作为 BeanDefinition
    • 如何判断 Class 是否有 @Component:利用字节码技术,获取 Class 文件中的元数据(注解等),判断元数据中是否有 @Componet

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

    • 通过反射,查看 Field 中是否有 @Autowired 类型的注解,有,则使用反射实现依赖注入

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

    我们可以使用 @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 查阅。

    名词解释:

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

    本节源码对应:v4

    四、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 IoC 容器,顺便说了一下 Spring Boot 原理。Spring 还有很多重要功能,如:管理 Bean 生命周期、AOP 的实现,等等。后续有机会再做一次分享。

    来个注解小结:

    • Spring 只实例化标识为 @Component 的组件(即业务类对象)
    • @Component 作为组件标识
    • @Autowired 用于判断是否需要依赖注入
    • @ComponentScan 指定组件扫描路径,不指定即为当前路径
    • @Configuration 代表配置类,作为入口
    • @EnableAutoConfiguration 实现加载配置类

    有的童鞋可能还会有这样的疑问:

    jdk jar 包、工具 jar 包的类是否需要注入容器?

    • 回答是不需要,因为容器只管理业务类,注入容器的类都有 @Component 注解。

    全文完。

    Supplement 1    Apr 24, 2020
    根据 v 友 guyeu 的提示,修改了部分内容,可以 [原文阅读]( https://depp.wang/2020/04/19/realize-a-simple-spring-ioc-container) 查看
    4 replies    2020-04-21 16:52:40 +08:00
    guyeu
        1
    guyeu  
       Apr 20, 2020
    1. @Component 和 @Bean 注解;
    2. 推荐使用构造方法的方式表示依赖注入;
    guyeu
        2
    guyeu  
       Apr 20, 2020   1
    并不是说 jdk jar 包、工具 jar 包的类不需要注入容器,一个常见的例子是使用 spring 管理线程池。一切需要实例化并被依赖的对象都可以托管给 spring,并通过依赖注入的方式提供给使用者。
    460881773
        3
    460881773  
       Apr 20, 2020
    不说了先标记一下
    deppwxq
        4
    deppwxq  
    OP
       Apr 21, 2020
    @guyeu 真知灼见,感谢,我修改一下。
    About     Help     Advertise     Blog     API     FAQ     Solana     2898 Online   Highest 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 66ms UTC 15:25 PVG 23:25 LAX 08:25 JFK 11:25
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86