Spring源码系列 - IOC和DI

对于一个系统的使用者来说,源码就是一个黑盒,不需要关心太多. 正如陀螺仪之于手表,集成电路之于CPU一样,仅仅需要了解这个系统可以做什么,以及这个系统不可以做什么就可以了.

  • 背景
  • IOC
  • DI

背景

为什么需要读源码?

搞清楚目的和动机,往往比做什么和怎么做更重要.
先说结论

  • 对于我们大多数人来说,不需要读源码或者说读了没什么用. 换言之,我们在读任何源码之前,要问一个问题,为什么要读它?

如果我们需要读源码,基本上就是下面两个大的理由

  • 为了源码而源码
    • 因为别人看了,我们也要看
    • 为了装X或面试
    • ….
  • 为了解决某个问题
    • 代码运行的时候遇到报错,需要跟踪源码,这种情况很少见,如果出现这种情况,只能说明这个框架是做的还是不够好.
    • 吸取源码的一些思想和设计思路,应用到自己的项目中

读框架源码的基本策略

  • 将框架拆解成模块,找到最核心的几个功能或者模块
  • 聚焦一个模块,两种角度
    • 使用者(黑盒)角度
      • 需要了解哪些最少必要概念
      • 引入哪些包
      • 需要配置哪些参数
      • 在代码层面需要添加什么东西
      • 别的框架中是否有类似的模块?
    • 开发者(白盒)角度
      • 需要了解哪些最少必要概念
      • 设计原则,模式
      • 模块的workflow是什么,牵涉到时序图,类图,流程图等,这些图都是从不同的角度来描述这个模块.
      • 与别的框架相比,模块有什么相同和不同

读源码和读书的类似

读源码和读一本武侠小说或者宏观经济学没有大的区别,因为源码和书本质上都是信息,读源码就是萃取信息的过程。
不同的阅读目的,需要用不同的姿势。这里的目的就预设为:吸取源码的一些思想和设计思路,应用到自己的项目中

  • 小说的主题和源码的初衷
    一本书肯定有一个或者某几个主题,比如歌颂爱情,追求真理。 框架的源码也是的,就Spring来说是为了简化Java开发 - 说起来是简单的,但实现起来并不简单。

  • 小说的线索和源码的设计原则和策略
    就拿天龙八部来说,它的主线可以是侠义,恩仇,爱情。然后爱情这块就分为三个主人公的分线。Spring源码也可以认为有几条主线:IOC,AOP,POJO, 模版. 这些是Spring的起点和基石,贯穿了Spring的方方面面.

  • 小说的结构和源码的的模块设计
    小说可以分为几个章节,每个章节大概讲了什么,每个章节的编排顺序是什么。同理,源码可以分为几个模块,模块之间的关系是什么?

  • 小说的细节和代码的细节
    里面会有一些观点,然后为了证明这些观点会有事实和逻辑。同理,源码模块里的一些方法会有一些技术细节,比如if,else, try, catch, synchronized, 某个数据结构,或者某个事件的的触发. 所以在这里我们可以得出一个结论:如果读源码二话不说闷着头就跳进入到方法的细节里,这是不对的,是错误的方向.

  • 看书读后感和看源码读后感
    小时候记得往往要写个什么观后感,现在想想这个事还是挺重要的。看一本书,可以看到作者传递的思想和价值主张(value proposal),得到某个启发,某个思维模型或框架,或者某个精彩的案例和论证,或者优雅的遣词造句,这些是对写作文有帮助的. 看源码也是,举个命名方面的例子,Spring在定位Bean的过程中涉及到AbstractRefreshableConfigAplicationContext类, 这个类名就很长,由5个单词组成, 一般而言,项目中很少取这么长名字来给类命名. 所以Spring的作者传递了这样一个思想:能清楚的表达设计意图是最重要的,其次才是长不长的问题。

  • 心态
    如果用大口吃烤串的心态去看书和读源码,很显然是没有效果和收益的。现在大多数人比较浮躁,看东西讲究快和多,讲究快本身没有问题,但快了之后,质量就会下降。所以看经典框架的核心模块,要以喝茶的心态来看,也就是《思考,快与慢》里提到的”系统2”.

  • 重要的的东西
    根据二八法则,一本书里只有20%的东西是重要的,看书不是越多越好,而是看少而精的书,那么源码也不例外,不需要阅读很多框架的源码,也不需要阅读一个框架的所有源码,那如何找到这20%?这是另外一个话题

IOC

IOC想要讲的故事

IOC - Inversion of Control, 中文是控制反转, 不管是中文还是英文,从字面上很难理解它的意思, 这与我随便说个词语”依赖变换“没什么区别。
让我们切换一个角度,看看在代码层面IOC意味着什么. 现在有类A和类B,传统的做法是类A依赖于类B,IOC的做法是,A不要依赖于B, 要依赖于B的抽象AbstractB. 比较一下前后的变化

  • A -> 具体的B
  • A -> AbstractB -> (某种方式指向) 具体B

这种变化让人想到了什么?AbstractB像是一个中介,隔离了A和具体的B. AbstractB侧重描述做什么,具体的B侧重于描述怎么做。
所以可以得出一个结论: IOC真正的内涵是将做什么(what)和怎么做(how)隔离开了, 可以认为做什么 是某个企业几十年不会变的核心概念和流程, 怎么做 为了达成某件事的手段是多样的,现在世界本身也是如此,付款的方式有现金和手机支付。所以如果一个复杂的系统没有基于IOC, 那么这个系统没有未来,如果一个系统基于IOC,这个复杂的系统才有演化的可能.

对于IOC,我们有这样的肖像刻画

  • 将做什么和怎么做隔离出来了 - 总感觉做开发的天天无脑的念叨隔离就可以了….
  • 符合依赖倒置原则
  • 符合好莱坞原则 - 体现在对象实例化的过程中

对于开发者来说,IOC在框架层面是标配,就好比汽车有个ABS系统一样,如果现在买辆车,如果没有防抱死系统,那真的是很奇怪.

有哪些框架支持IOC功能?

  • Spring算一个 - Spring提供了很多功能,我们可以只使用其中的IOC功能
  • .net的Autofac
  • HK2 - 轻量级的IOC框架,
  • Guice - Google出品
  • 如果没有上述框架,只能手写了

使用者的角度

一个简单的例子

// 创建IOC容器
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("defaultconstruct/user.xml");
// 实例化Bean,包含依赖注入的过程
User user = (User) context.getBean("user");

有哪些框架支持IOC功能?

  • .net的Autofac
  • HK2 - 轻量级的IOC框架,
  • Guice - Google出品
  • 如果没有上述框架,只能手写了

开发者的角度

Spring IOC容器创建的大体过程(细节可见IOC时序图)

  • 定位 - 设置Bean的路径,创建BeanFactory.
  • 加载 - 读取BeanDefinition文件.
  • 注册 - 解析BeanDefinition,并放入IOC容器当中.

核心方法

IOC 时序图

avatar

补充

DI

DI和IOC的关系

IOC更多的是描述了做什么(要隔离规范和实现), DI是IOC的一种实现方式。 此外DL是IOC的另外一种实现方式

使用者的角度

warmup: 一个简单的依赖注入例子

  • 定义Bean

    @Configuration
    public class RedisConfig {
        @Primary /* 在装配的时候,可能会匹配多个Bean, Primary代表了这个Bean会被优先选择 */
        @Bean(name = "redisTemplate")
        public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
            RedisTemplate<String, Object> template = new RedisTemplate<>();
            return template;
        }
    }
  • 注入Bean

    @Component
    public final class AppRedisCacheManager implements IGlobalCache {
        private RedisTemplate<String, Object> redisTemplate;
    
        /* 注入RedisTemplate实例 */
        /* Autowired是Spring特有的注解,如果想让项目不局限于Spring可以使用Inject注解 */
        @Autowired
        public AppRedisCacheManager(RedisTemplate redisTemplate){
            this.redisTemplate = redisTemplate;
        }
    }

依赖注入的方式

上面的例子是典型的基于注解的自动注入。我看过不少介绍依赖注入的文章,有说有三种,有的说有五种. 按照我自己的理解,依赖注入有两大方式

  • 手动注入 - 必须显示的指定实例A的引用是B,一般是在XML里面定义或者JavaConfig里定义。
  • 自动注入 - 不需要显示的指定实例A的引用是是B, 典型的形式是基于注解。 自动注入更加通用的内涵是:如果基于不同的条件,加载不同的Bean
    • 有哪些注解可以加载Bean?
      • @Autowired - Spring特有的注解,默认按类型查找,如果找不到,按照名字查找.
        • Autowire加载Bean可能失败的原因
          • 被加载的Bean没有加上@Service相关的注解
          • 加了@Service的注解,但没有被@ComponentScan扫描。

            在Springboot中, @SpringBootApplication 注解包含了@ComponentScan, 这就是为什么在Spring Boot应用中看不到ComponentScan注解的原因

          • Spring MVC 的filter不能加载某个Bean, 因为在这个时候Bean还没有被初始化. 解决的办法是拿到ApplycationContext 然后调用getBean方法.
          • 循环依赖
        • Autowired可以注入到构造器,字段,属性,参数
      • @Resouce - Spring特有的注解,默认按名字查找,找不到,按类型查找,如果按类型找到多个,再按照@Qualifier筛选
      • @Inject - 非Spring特有的注解,
    • 有哪些注解可以让Bean被加载?
      • @Component - 作用在类上, 用来描述通用的Bean, 业务内涵不是特别强.
      • @Controller - 作用在类上,与Spring mvc强相关,不能用别的注解替换, 内部会包含路由等信息
      • @Service - 作用在类上, 一般情况下从Repository拿数据然后提供数据给Controller消费,
      • @Repository - 作用在类上, 语义上来说与持久化相关,对应DAO, 不能用别的注解替换, 内部会包含SQL语句的信息.
      • @Bean - Spring 3.0引入,和@Configuration一起工作. 如果存在第三方插件,很显然无法使用@Component注解的,所以可以在某个方法A上使用Bean注解,然后在方法A的内部返回实例.

一些额外发现的问题

  • ApplycationContext的getBean和@Autowired的区别是什么?

其他

  • 依赖查找是什么?
  • 规范
    • JSR-330
      • Spring 从3.0开始支持JSR-330规范
      • @Inject和Spring的@Autowired等价
      • @Named和Spring的@Component等价
    • JSR-250

开发者的角度

DI 时序图

avatar

未来