likes
comments
collection
share

Spring IOC学习指南——一种混合思考方式

作者站长头像
站长
· 阅读数 6

如何学习Spring

Spring的重要性不言而喻,而Spring的基石是IOC容器。IOC,控制反转,是指将对象的管理等工作交给Spring容器。这在Java世界是一个太热门的话题了,也有太多太多资料和文档跟你分析Spring IOC的实现原理。但是对于初学者而言,首先,你应该至少看过一个demo,了解Spring的基本用法

比如:

ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
User user = (User)context.getBean("user");

或者:

@Configuration      //配置类
@ComponentScan("xxx")   // 以配置类所在的包为基础包来扫描,扫描该包及其子包中包含的类
public class SpringConfig {
}

或者:

        // 创建BeanFactory对象
        DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
        // 加载xml配置文件
        XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory);
        reader.loadBeanDefinitions("bean.xml");
        // 通过Spring容器获取对象,而不是自己new一个User对象
        User user = beanFactory.getBean(User.class);

然后就需要进一步理解Spring,为了让你深刻地认识Spring的IOC容器,一般有两种方式:

  • 一步到位,读源码
  • 手写一个Spring容器

这两种各有各的优劣:

  • 看源码时,你是否会:看到一行代码,但无法很快明白这行代码的作用,不知道它是否重要,是否值得花很多时间了解它?跳过吧,一会就看的云里雾里,不知所踪,迷失在源码中。
  • 而手写时,又觉得是在“重复造轮子”,尽管能帮助你提高写代码的水平,但十分有限,性价比不高,付出与回报不成正比,并且最终还是要拥抱源码,但手写的优势是:看源码的过程会非常轻松并且有趣。

这就像宏内核与微内核,想要两边都沾一点,那就用混合内核呗。

混合模式

因此,本文首先会「站在高处思考Spring IOC容器的设计」,并不会手写代码,但我们会知道:Spring IOC容器设计的大概思路,这会帮助你更好地理解源码。

其次,通过阅读源码验证你的思路,更具体地说:看看Spring IOC容器的设计和你的区别在哪里,有什么细节是你没有考虑到的。

当然,Spring是伟大的,一篇文章肯定讲不完,本文主要提供一些重要的引子和思路,深入细节的学习还需要读者自己花时间去阅读源码。

那么下面我们就正式开始了。

一、站在高处思考Spring IOC容器的设计

IOC容器最核心的功能

最核心的功能,无非是下面三点:

  • 创建对象
  • 管理对象
  • 销毁对象

1、创建对象

就这四个字,“创建对象”,我的第一反应是:工厂模式。工厂模式可以屏蔽掉创建对象的细节,这岂不是和IOC容器一拍即合?那我们也许可以用ObjectFactory作为顶层父接口,提供创建对象的方法。

怎么创建对象

谈到如何创建对象,又会想到:new构造方法,和反射。作为容器,我们不应该强制要求选用某种构造方法来进行创建,因此:我们需要一种机制能够选择类的一个构造方法来进行创建。

  • 用户应当可以通过注解修饰某个构造方法,来指定我们的选择
  • 在用户没有指定的情况下,我们也应该提供默认的实现,比如:选择无参构造

然后通过反射调用构造方法,对象就算创建完了。

创建对象的时机

一种是:IOC容器一启动,就创建对象;另一种是,当用户需要使用时,才创建对象。这很像懒加载,我们可以使用一个注解来表达用户是否希望对象需要用时才创建对象。

构造方法的参数从何而来

这就需要递归来解决了。也即我们创建这个对象,因为它的属性需要注入某个对象,因此必须先创建另一个对象。然后就像用户一样拿到这个对象注入即可。

2、管理对象

当用户需要使用对象时,我们该如何提供对象呢?我们假定这个方法为:getObject方法。

对象的标识

用户需要传递给容器某些信息,让我能够知道我要给用户的是哪个对象。因此我们需要一种手段唯一标识对象。

  • 我们可以给对象起名(唯一标识一个对象),getObject时传递名字即可
  • 用户传递class对象,这当然也是可以的

是否单例

实际上很多业务代码类,都是单例模式,但也有很多对象,每次使用时都需要一个新的。这就出现了不同,因此我们可以用一个注解来表达这个特性。比如我们获取对象时,对于单例,直接从单例池中取;否则,创建一个新的对象给用户。

那这就引出了一个问题:如何管理单例对象呢?

单例池

我们需要一个数据结构来维护所有的单例对象,当然也可能根据某些特性用不同的数据结构分别管理。为了保证取对象的高效,自然会想到HashMap。

属性注入

属性注入是非常重要的一个环节,我们必须提供足够丰富的注入方法,以满足一些复杂的,特殊的对象能够符合用户的预期。比如:

  • 基于 setxxx 的依赖注入
  • 基于字段的依赖注入

我们还需要支持从外部的配置文件读取属性并注入。

3、销毁对象

由于篇幅原因,此处就省略了,读者可以自己思考IOC容器该如何销毁对象。

IOC容器需要考虑的细节

即便不考虑编码细节,其它细节也是非常多的,比如:

  • 创建对象的顺序,Spring IOC容器通过@DependsOn指定对象的顺序
  • 哪些对象应该被容器管理,最经典的两个注解:@ComponentScan和@Component
  • 特殊的对象,这些Bean的运行时类型是动态的,而纯粹的IOC容器对这种动态的变化并不敏感,Spring提供了FactoryBean接口,使得我们可以在创建的时候再去实现其具体的功能。
  • Bean循环依赖问题,Spring使用三级缓存在一定程度上解决了这个问题
  • AOP相关,使用代理模式时,代理对象该不该被IOC容器感知?该。那么Spring IOC容器存储的应该是代理类,IOC容器也需要能够创建和管理代理对象,这依赖于PostProcessor后置处理,Spring提供的扩展点之一

而在阅读源码时,你可能会发现更多原本自己没想到的细节。

二、验证你的思路:读源码

当然在阅读源码之前,Bean是Spring中最核心的概念,了解Bean的概念是非常有必要的。

Spring Bean的概念

  • Java Bean:一个普通的 Java 对象(Plain Old Java Object,POJO)
  • Spring Bean:由 Spring 负责创建、配置和管理,的Java对象

总之一个Bean就是一个对象。

另外你应该注意到了,在我们的demo中有这样两行代码:

XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory);
reader.loadBeanDefinitions("bean.xml");

这就引出了另外一个非常重要的概念:什么是BeanDefinition?

BeanDefinition存在的意义

对于开发者而言,我们只是编写了一个类,然后丢给Spring IOC容器,容器完全不知道该如何管理这个类,因此Spring需要开发者给Spring容器传达一些信息,比如:

  • Bean被创建的时机
  • Bean是单例模式 or 每次希望使用这个Bean都创建一个全新的Bean
  • Bean具体该如何初始化,如何给Bean的字段赋值

而实际上Spring容器需要的信息远远比这更多。因此,Spring需要一个对象,来封装这些信息,从而能够符合用户预期地管理、创建、销毁Bean。这个对象就叫做:「BeanDefinition」。我们简单看一下BeanDefinition:

// BeanDefinition对应的class名
void setBeanClassName(@Nullable String var1);
String getBeanClassName();
// bean的作用域
void setScope(@Nullable String var1);
String getScope();
// bean是否懒加载
void setLazyInit(boolean var1);
boolean isLazyInit();
// bean是否单例 / 原型
boolean isSingleton();
boolean isPrototype();

有了BeanDefinition,IOC容器的主要工作就可以划分为两块:

  • 将用户定义的类转化为BeanDefinition
  • 根据BeanDefinition创建,管理,销毁Bean

该从哪开始阅读源码

仅从认识Spring IOC的原理来说,我并不建议初学者从ApplicationContext开始阅读源码。这就引出了一个常见面试题:ApplicationContext和BeanFactory有什么区别?

那网上八股文很多,下面谈这个问题,并不是标准答案,只是为了说明“读Spring源码,也许该从BeanFactory开始”这一个观点而已。

ApplicationContext和BeanFactory的区别

其实看名字我们就可以推断出来:BeanFactory,就是个Bean工厂,那它至少可以创建Bean吧,至于能不能管理,销毁,名字就不能体现了,需要我们看看源码。

而ApplicationContext提供了更多的功能,如何证明呢?ApplicationContext的属性中有一个beanFactory。

private DefaultListableBeanFactory beanFactory;

因此我们只需要稍微看几行源码,就会知道:ApplicationContext通过委托的方式将一部分功能交给beanFactory来做。

而ApplicationContext还包括一个可能初学者也听过的东西:Spring Event,事件机制,这是一种对观察者设计模式的优雅实现。这显然和一个纯粹的IOC容器没什么关系,至于其它譬如国际化的功能,就不提了。总之,ApplicationContext是一个集大成的接口,阅读它的启动流程,干扰因素太多。但我想你已经明白为什么我会说:你也许应该从BeanFactory开始阅读源码。

另外,推荐一篇个人觉得写的不错的源码分析文章:Spring IOC 容器源码分析

最后

本文主要提供一种学习Spring的思考方式,当然这就和系统设计一样,没有绝对的孰优孰劣,更该关注适不适合。但希望无论你是想手写Spring,还是读源码,本文都能够或多或少帮助到你。