cherish

返朴归真


  • Home

  • Archives

  • Tags

  • Categories

ZAB协议分析和ZooKeeper的leader选举机制

Posted on 2020-07-26 | In 分布式算法&理论 , 分布式事务和数据一致性 |
Words count in article: 4.2k | Reading time ≈ 14

前言

ZAB协议是专门为zookeeper实现分布式协调功能而设计。zookeeper主要是根据ZAB协议是实现分布式系统数据一致性。

zookeeper根据ZAB协议建立了主备模型完成zookeeper集群中数据的同步。这里所说的主备系统架构模型是指,在zookeeper集群中,只有一台leader负责处理外部客户端的事物请求(或写操作),然后leader服务器将客户端的写操作数据同步到所有的follower节点中。

1 ZAB协议内容

  • 所有事务请求必须由一个全局唯一的服务器来协调处理,这样的服务器被称为leader服务器,而余下的其他服务器则成为follower服务器。
  • leader服务器负责将一个客户端事务请求转换成一个事务proposal,并将该proposal分发给集群中所有的follower服务器。之后leader服务器需要等待所有follower服务器的反馈,一旦超过半数的follower服务器进行了正确的反馈后,那么leader就会自己先commit这个事务,并再次向所有的follower服务器分发commit消息,要求其将前一个proposal进行提交。

ZAB有两种基本的模式:崩溃恢复和消息广播。

  • 当整个服务框架启动过程中或Leader服务器出现网络中断、崩溃退出与重启等异常情况时,ZAB协议就会进入恢复模式并选举产生新的Leader服务器。

  • 当选举产生了新的Leader服务器,同时集群中已经有过半的机器与该Leader服务器完成了状态同步之后,ZAB协议就会退出恢复模式,那么整个服务框架就可以进入消息广播模式。其中,所谓的状态同步是指数据同步,用来保证集群中存在过半的机器能够与leader服务器的数据状态保持一致。

  • Leader选举算法不仅仅需要让Leader自身知道已经被选举为Leader,同时还需要让集群中的所有其他机器也能够快速地感知到选举产生的新的Leader服务器。

  • 当一台同样遵守ZAB协议的服务器启动并加入集群后,如果已经存在leader,那么它会自觉的找到leader,与其进行数据同步,然后一起参与消息广播。

  • 如果follower服务器接收到客户端的事务请求,那么他们会将这个事务请求转发给leader服务器。

  • 当Leader服务器出现崩溃或者机器重启、集群中已经不存在过半的服务器与Leader服务器保持正常通信时,那么在重新开始新的一轮的原子广播事务操作之前,所有进程首先会使用崩溃恢复协议来使彼此到达一致状态,于是整个ZAB流程就会从消息广播模式进入到崩溃恢复模式。

  • 一个机器要成为leader,要获得过半机器的支持,而由于每台机器都可能崩溃,因此整个过程可能出现多个leader,一个机器也可能多次成为leader。

2 消息广播

  • ZAB协议的消息广播过程使用原子广播协议,类似于一个二阶段提交过程,针对客户端的事务请求,Leader服务器会为其生成对应的事务Proposal,并将其发送给集群中其余所有的机器,然后再分别收集各自的选票,最后进行事务提交。

  • 此处ZAB的二阶段提交和一般的二阶段提交略有不同,ZAB移除了二阶段提交中的事务中断的逻辑,follower服务器要么正常反馈,要么抛弃leader。好处是我们不需要等待所有follower都反馈响应才能提交事务,坏处是集群无法处理leader崩溃而带来的数据不一致的问题。后者需要崩溃恢复模式来解决这个问题。

  • 整个消息广播协议是基于具有FIFO特性的TCP协议来进行网络通信的,因此能够很容易保证消息广播过程中消息接受与发送的顺序性。

  • 整个消息广播过程中,Leader服务器会为每个事务生成对应的Proposal来进行广播,并且在广播事务Proposal之前,Leader服务器会先为这个Proposal分配一个全局单调递增的唯一ID,称之为事务ID(ZXID),由于ZAB协议需要保证每个消息严格的因果关系,因此必须将每个事务Proposal按照其ZXID的先后顺序来进行排序和处理。

  • 在广播过程中,leader会为每一个follower分配一个单独的队列,然后将需要广播的事务proposal依次放入,并且根据FIFO策略进行消息发送。每个follower接收到proposal之后,都会首先将其以事务日志的形式写入本地磁盘,写入成功后反馈leader一个ack响应。当leader收到超过半数的follower的ack响应之后,就会广播一个commit消息给所有follower以通知其进行事务提交,同时leader自身也完成事务的提交。每个follower在接收到commit之后,也会完成对事务的提交。

  • 在广播过程中,如果follower接收到proposal之后记录事务日志失败,或者proposal丢失。紧接着不久后,它直接接到了这个proposal的commit,那么follower就会向leader发送请求重新申请这个任务,leader会再次发送proposal和commit。

关于 ZXID
  zxid,也就是事务id,它是全局唯一且递增的,为了保证事务的顺序一致性,zookeeper采用了递增的事务id号(zxid)来标识事务。所有的提议(proposal)都在被提出的时候加上了 zxid。实现中zxid是一个 64 位的数字,它高32 位是epoch(ZAB协议通过epoch编号来区分Leader周期变化的策略)用来标识leader关系是否改变,每次一个leader被选出来,它都会有一个新的epoch=(原来的 epoch+1),标识当前属于那个 leader 的统治时期。低 32 位用于递增计数。

3 崩溃恢复

  • 当整个服务框架启动过程中或Leader服务器出现网络中断、崩溃退出与重启等异常情况无法与半数以上的follower联系时,ZAB协议就会进入恢复模式。

3.1 崩溃恢复下的两种情况和所要保证的特性

  1. ZAB协议需要确保那些已经在Leader服务器上提交的事务最终被所有服务器都提交。

    • 如果leader在崩溃前发出了proposal1,proposal2,commit1(proposal1的commit),proposal3,commit2(说明leader自己已经commit了proposal2),那么ZAB需要确保恢复后proposal2在所有服务器上都被提交成功,否则会出现不一致。
  2. ZAB协议需要确保丢弃那些只在Leader服务器上被提出的事务

    • 如果leader服务器A在崩溃前发出了proposal1,proposal2,commit1(proposal1的commit),proposal3,commit2,并且执行了commit3(说明leader自己已经commit了proposal3),但是commit3还未发出,leader便宕机了,那么ZAB需要确保恢复后,A重新加入集群(大概率不是leader了)后,要舍弃proposal3这个事务。

      3.2 进入崩溃恢复模式的流程

  3. 当leader出现问题,zab协议进入崩溃恢复模式,并且选举出新的leader。当新的leader选举出来以后,如果集群中已经有过半机器完成了leader服务器的状态同(数据同步),退出崩溃恢复,进入消息广播模式。

  1. 当新的机器加入到集群中的时候,如果已经存在leader服务器,那么新加入的服务器就会自觉进入崩溃恢复模式,找到leader进行数据同步。

3.2.1 leader选举算法

zookeeper提供了三种选举方式:

  • LeaderElection
  • AuthFastLeaderElection
  • FastLeaderElection (最新默认)
    默认的算法是FastLeaderElection,所以这里主要分析它的选举机制。

选举过程中,有四个概念我们需要明确一下:

  1. Serverid:服务器ID
    • 比如有三台服务器,编号分别是1,2,3。
  2. Zxid:数据ID
    • 事务id,你也可以理解为当前服务器中存放的最大数据ID。
  3. Epoch:逻辑时钟
    • 在投票过程中,每投完一次票这个数据就会增加,然后与接收到的其它服务器返回的投票信息中的数值相比,根据不同的值做出不同的判断。
  4. Server状态:选举状态
  • LOOKING,竞选状态。
  • FOLLOWING,随从状态,同步leader状态,参与投票。
  • OBSERVING,观察状态,同步leader状态,不参与投票。
  • LEADING,领导者状态。

zk在选举投票的时候,他们的票上,就带有上述这四个信息。

根据前文的描述,我们知道选举成功的leader应该要满足如下特性:

  • 能够确保提交已经被Leader提交的事务的Proposal,同时丢弃已经被跳过的事务Proposal。
  • 如果让Leader选举算法能够保证新选举出来的Leader服务器拥有集群所有机器中最高编号(ZXID最大)的事务Proposal,那么就可以保证这个新选举出来的Leader一定具有所有已经提交的更改。
  • 更为重要的是,如果让具有最高编号事务的Proposal机器成为Leader,就可以省去Leader服务器查询Proposal的提交和丢弃工作这一步骤了。

所以zk的leader选举的判断优先级是(后文我们称为rules judging):

  1. 优先检查ZXID。ZXID比较大的服务器优先作为Leader
  2. 如果ZXID相同,那么就比较Serverid。Serverid较大的服务器作为Leader服务器。

选举步骤如下:

  1. Server刚启动(宕机恢复或者刚启动)准备加入集群,此时读取自身的zxid等信息。

  2. 所有Server加入集群时都会推荐自己为leader,然后将(leader id 、 zixd 、 epoch)作为广播信息,广播到集群中所有的服务器(Server)。然后等待集群中的服务器返回信息。

  3. 收到集群中其他服务器返回的信息,此时要分为两类:该服务器处于looking状态,或者其他状态。

    1. 服务器处于looking状态
      1. 首先判断逻辑时钟Epoch:
        • 如果接收到Epoch大于自己目前的逻辑时钟(说明自己所保存的逻辑时钟落伍了)。更新本机逻辑时钟Epoch,同时 Clear其他服务发送来的选举数据(这些数据已经OUT了)。然后根据rules judging判断是否需要更新当前自己的选举情况(一开始选择的leader id是自己)。然后再将自身最新的选举结果(也就是上面提到的三种数据(leader Serverid,Zxid,Epoch)广播给其他server)
        • 如果接收到的Epoch小于目前的逻辑时钟。说明对方处于一个比较OUT的选举轮数,这时只需要将自己的 (leader Serverid,Zxid,Epoch)发送给他即可。
        • 如果接收到的Epoch等于目前的逻辑时钟。再根据判断规则,将自身的最新选举结果广播给其他 server。
      2. 如果Server接收到了其他所有服务器的选举信息,那么则根据这些选举信息确定自己的状态(Following,Leading),结束Looking,退出选举。
      3. 即使没有收到所有服务器的选举信息,也可以判断一下根据以上过程之后最新的选举leader是不是得到了超过半数以上服务器的支持,如果是则尝试接受最新数据,倘若没有最新的数据到来,说明大家都已经默认了这个结果,同样也设置角色退出选举过程。
    2. 服务器处于其他状态(Following, Leading)
      1. 如果逻辑时钟Epoch相同,将该数据保存到recvset,如果所接收服务器宣称自己是leader,那么将判断是不是有半数以上的服务器选举它,如果是则设置选举状态退出选举过程
      2. 否则这是一条与当前逻辑时钟不符合的消息,那么说明在另一个选举过程中已经有了选举结果,于是将该选举结果加入到outofelection集合中,再根据outofelection来判断是否可以结束选举,如果可以也是保存逻辑时钟,设置选举状态,退出选举过程。

3.2.2 数据同步

完成Leader选举后,在正式开始工作前,Leader服务器首先会确认日志中的所有Proposal是否都已经被集群中的过半机器提交了,即是否完成了数据同步。

基于上文讲到的两种情况,数据同步会有不同的处理:


  • 同步事务的提交:
    • leader为每一个follower都准备一个队列,并将那些没有被各follower同步的事务以proposal消息的形式逐个发送给follower,并在每个proposal消息后面紧跟一个commit消息表示该事务已经被leader提交。等到某个follower同步了所有之前尚未同步的事务并将其成功应用到本地数据库,leader会将该follower加入到可用follower列表中。

  • 处理丢弃的事务
    • 下面分析ZAB协议如何处理需要丢弃的事务Proposal的,ZXID是一个64位的数字,其中低32位可以看做是一个简单的单调递增的计数器,针对客户端的每一个事务请求,Leader服务器在产生一个新的事务Proposal时,都会对该计数器进行加1操作;而高32位则代表了Leader周期的epoch编号,每当选举产生一个新的Leader时,就会从这个Leader上取出其本地日志中最大事务Proposal的ZXID,并解析出epoch值,然后加1,之后以该编号作为新的epoch,低32位从0来开始生成新的ZXID。
    • ZAB协议通过epoch号来区分Leader周期变化的策略,能够有效地避免不同的Leader服务器错误地使用不同的ZXID编号提出不一样的事务Proposal的异常情况。当一个包含了上一个Leader周期中尚未提交过的事务Proposal的服务器启动时,其肯定无法成为Leader,因为当前集群中一定包含了一个Quorum(过半)集合,该集合中的机器一定包含了更高epoch的事务的Proposal,因此这台机器的事务Proposal并非最高,也就无法成为Leader。
    • 当这台机器以follower身份连上leader之后,leader会根据自己最后被提交的proposal来和这台机器的proposal作比较,发现有不一致的,则以leader为准,明确需要舍弃的事务后,leader会要求该台机器进行回滚操作,回滚到某个被半数机器执行的最新的事务版本。

epoch:可以理解为当前集群所处的年代或者周期,每个leader就像皇帝,都有自己的年号,所以每次改朝换代,leader 变更之后,都会在前一个年代的基础上加 1。这样就算旧的 leader 崩溃恢复之后,也没有人听他的了,因为 follower 只听从当前年代的leader的命令。

4 ZAB和paxos的联系和区别

4.1 联系

  1. 都存在一个类似于Leader进程的角色,由其负责协调多个Follower进程的运行。
  2. Leader进程都会等待超过半数的Follower做出正确的反馈后,才会将一个提议进行提交。
  3. 在ZAB协议中,每个Proposal中都包含了一个epoch值,用来代表当前的Leader周期,在Paxos算法中,同样存在这样的一个标识,名字为Ballot。

4.2 区别

  1. Paxos算法中,新选举产生的主进程会进行两个阶段的工作,第一阶段称为读阶段,新的主进程和其他进程通信来收集主进程提出的提议,并将它们提交。第二阶段称为写阶段,当前主进程开始提出自己的提议。
  2. ZAB协议在Paxos基础上添加了同步阶段,此时,新的Leader会确保存在过半的Follower已经提交了之前的Leader周期中的所有事务Proposal。
  3. ZAB协议主要用于构建一个高可用的分布式数据主备系统,而Paxos算法则用于构建一个分布式的一致性状态机系统。

一致性hash算法

Posted on 2020-07-26 | In 分布式相关 , 负载均衡算法 |
Words count in article: 1.9k | Reading time ≈ 6

前言

在解决分布式系统中负载均衡问题的时候,我们可以使用Hash算法让固定的一部分请求落到同一台服务器上,这样每台服务器固定处理一部分请求(并维护这些请求的信息),进而起到负载均衡的作用。

但是普通的hash取模算法伸缩性很差,当新增或者下线服务器机器时候,用户id与服务器的映射关系会大量失效。这在分布式缓存系统中,是非常严重的问题。

例如我们原先有10台服务器,故而hash取模我们一般会这么算:hash(key)%10,从而得到一个在0-9之间的余数,确定请求由哪个服务器处理。

此时如果我们上线新服务器,或下线旧服务器,都会使服务器数量发生改变,这时候不论是hash(key)%11还是hash(key)%9,都会使近乎所有的key的hash取模结果和原先不一样,进而引发问题。比如缓存场景中的负载均衡,如果遇到这种情况,会使短时间内近乎所有的key失效,进而引发缓存雪崩。

为了解决这个问题,使得分布式系统可以自由且无顾虑的增减服务器,我们引入了一致性hash算法,利用hash环对其原本的hash取模算法进行了改进。

1 一致性hash算法

一致性哈希算法在1997年由麻省理工学院提出,是一种特殊的哈希算法,目的是解决分布式缓存的问题。在移除或者添加一个服务器时,能够尽可能小地改变已存在的服务请求与处理请求服务器之间的映射关系。一致性哈希解决了简单哈希算法在分布式哈希表( Distributed Hash Table,DHT) 中存在的动态伸缩等问题。

一致性hash算法主要用于解决cache miss问题

一致性哈希算法是在哈希算法基础上提出的,在动态变化的分布式环境中,哈希算法应该满足的几个条件:

  1. 平衡性
    • 是指hash的结果应该平均分配到各个节点,这样从算法上解决了负载均衡问题。
  2. 单调性
    • 是指在新增或者删减节点时,不影响系统正常运行。
  3. 分散性
    • 是指数据应该分散地存放在分布式集群中的各个节点(节点自己可以有备份),不必每个节点都存储所有的数据。

1.1 算法概述

为了能直观的理解一致性hash原理,这里结合一个简单的例子来讲解,假设有4台服务器,地址为ip1,ip2,ip3,ip4。

一致性hash是首先计算四个ip地址对应的hash值
hash(ip1),hash(ip2),hash(ip3),hash(ip3),计算出来的hash值是0~最大正整数(2^32)之间的一个值,这四个值在一致性hash环上呈现如下图:

hash环上顺时针从整数0开始,一直到最大正整数,我们根据四个ip计算的hash值肯定会落到这个hash环上的某一个点,至此我们把服务器的四个ip映射到了一致性hash环。

当用户在客户端进行请求时候,首先根据hash(userId)计算路由规则,然后看hash值落到了hash环的那个地方,根据hash值在hash环上的位置顺时针找距离最近的ip作为路由ip。

如上图可知,user1和user2由ip2的服务器处理,user3由ip3服务器处理,以此类推

0~2^32的区间导致了hash值数量远超过服务器数量,使得hash碰撞的概率降到了极低。

1.2 上线服务器

当新增一个ip5的服务器后,一致性hash环大致如下图:

根据顺时针规则可知之前user5的请求应该被ip5服务器处理,现在被新增的ip5服务器处理,其他用户的请求处理服务器不变,也就是说,新增服务器顺时针方向最近的服务器的一部分请求会被新增的服务器所替代。

1.3 下线服务器

当ip2的服务器挂了的时候,一致性hash环大致如下图:

根据顺时针规则可知user1,user2的请求会被服务器ip3进行处理,而其它用户的请求对应的处理服务器不变,也就是只有之前被ip2处理的一部分用户的映射关系被破坏了,并且其负责处理的请求被顺时针下一个节点委托处理。

2 一致性hash倾斜问题

一致性hash可以做到每个服务器都进行处理请求,但是不能保证每个服务器处理的请求的数量大致相同,如下图:

服务器ip1,ip2,ip3经过hash后落到了一致性hash环上,从图中hash值分布可知ip1会负责处理大概80%的请求,而ip2和ip3则只会负责处理大概20%的请求,虽然三个机器都在处理请求,但是明显每个机器的负载不均衡,这样称为一致性hash的倾斜,我们可以使用设置虚拟节点的方式解决这个问题。

2.1 设置虚拟节点

当服务器节点比较少的时候会出现上节所说的一致性hash倾斜的问题,一个解决方法是多加机器,但是加机器是有成本的,那么就加虚拟节点,比如上面三个机器,每个机器引入1个虚拟节点后的一致性hash环的图如下:

其中ip1-1是ip1的虚拟节点,ip2-1是ip2的虚拟节点,ip3-1是ip3的虚拟节点。命中ip3-1的请求,则会被导向到ip3服务器。

可知当物理机器数目为M,虚拟节点为N的时候,实际hash环上节点个数为M*N。比如当客户端计算的hash值处于ip2和ip3或者处于ip2-1和ip3-1之间时候使用ip3服务器进行处理。

当然,我们很难得到一个完美均衡的一致性hash环,但理论上虚拟节点数量的增加,和一致性hash环的均衡性呈正相关。在实际应用中,通常将虚拟节点数设置为32甚至更大,因此即使很少的服务节点也能做到相对均匀的数据分布。

3 一致性hash的优点

  1. 可扩展性。

    • 一致性哈希算法保证了增加或减少服务器时,数据存储的改变最少,相比传统哈希算法大大节省了数据移动的开销
  2. 更好地适应数据的快速增长。

    • 采用一致性哈希算法分布数据,当数据不断增长时,部分虚拟节点中可能包含很多数据、造成数据在虚拟节点上分布不均衡,此时可以将包含数据多的虚拟节点分裂,这种分裂仅仅是将原有的虚拟节点一分为二、不需要对全部的数据进行重新哈希和划分。
    • 虚拟节点分裂后,如果物理服务器的负载仍然不均衡,只需在服务器之间调整部分虚拟节点的存储分布。这样可以随数据的增长而动态的扩展物理服务器的数量,且代价远比传统哈希算法重新分布所有数据要小很多

Spring IoC源码详解

Posted on 2020-07-15 | In 框架 , Spring |
Words count in article: 15.8k | Reading time ≈ 77

本文中,所有spring framework源码,均采用5.0.x版本

在《Spring IoC概念分析》一文中,我们对Spring IoC的前置概念和整体流程有了一个初步的了解,在本文中,我们会解读源码,力图将spring IoC的逻辑扁平化,可视化。

1. Spring IoC容器的启动

applicationContext是Spring的核心,Context我们通常解释为上下文环境,我想用“容器”来表述它更容易理解一些,ApplicationContext则是“应用的容器”了;在Web应用中,我们会用到WebApplicationContext,WebApplicationContext继承自ApplicationContext;

以企业级java项目最常用的web项目为例;我们知道,在web项目中,Spring启动是在web.xml配置监听器,如下所示:

1
2
3
4
<!-- 配置Spring上下文监听器 -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

可以看到,ContextLoaderListener类是spring IoC容器启动的核心,也是整个Spring框架的启动入口:

ContextLoaderListener类实现了Tomcat容器的ServletContextListener接口,所以它与普通的Servlet监听是一样的。同样是重写到两个方法:

  • contextInitialized()方法在web容器初始化时执行
  • contextDestroyed()方法在容器销毁时执行。

WEB容器启动时会触发初始化事件,ContextLoaderListener监听到这个事件,其contextInitialized()方法会被调用,在这个方法中Spring会初始化一个root上下文,即WebApplicationContext。

WebApplicationContext是一个接口,其实际默认实现类是XmlWebApplicationContext。(重点关注红色箭头指示的两条继承/实现关系)

这个就是Spring IOC的容器,其对应bean定义的配置信息由web.xml中的context-param来指定:

1
2
3
4
5
6
<!-- 配置Spring配置文件路径 -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:applicationContext.xmlclasspath*:applicationContext-shiro.xml
</param-value>
</context-param>

在ContextLoaderListener类中,只是实现了ServletContextListener提供的到两个方法,Spring启动主要的逻辑在父类ContextLoader的方法initWebApplicationContext实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class ContextLoaderListener extends ContextLoader implements ServletContextListener {

public ContextLoaderListener() {
}

public ContextLoaderListener(WebApplicationContext context) {
super(context);
}

/**
* Initialize the root web application context.
*/
@Override
public void contextInitialized(ServletContextEvent event) {
initWebApplicationContext(event.getServletContext());
}

/**
* Close the root web application context.
*/
@Override
public void contextDestroyed(ServletContextEvent event) {
closeWebApplicationContext(event.getServletContext());
ContextCleanupListener.cleanupAttributes(event.getServletContext());
}
}

ContextLoaderListener的作用就是启动web容器时自动装配ApplicationContext的配置信息。更细化一点讲,Spring的启动过程其实就是Spring IOC容器的启动过程。

1.1 ContextLoader剖析

上文说过,Spring启动主要的逻辑在父类ContextLoader的方法initWebApplicationContext实现。所以我们有必要重点看下ContextLoader类:

这里面,最核心的方法是initWebApplicationContext(),它被ContextLoaderListene的contextInitialized()调用,负责spring容器的初始化。

注意,该图代码非5.0.x版本,但核心逻辑变化不大,可以参考。

1.2 Context的生成门面

在这个方法中,入参ServletContext是由web容器监听器(ContextLoaderListener)提供。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
/**
* Initialize Spring's web application context for the given servlet context,
* using the application context provided at construction time, or creating a new one
* according to the "{@link #CONTEXT_CLASS_PARAM contextClass}" and
* "{@link #CONFIG_LOCATION_PARAM contextConfigLocation}" context-params.
*
* 使用构造时提供的应用程序上下文,或者根据“ {@link #CONTEXT_CLASS_PARAM contextClass}”
* 和“ {@link #CONFIG_LOCATION_PARAM contextConfigLocation}”上下文参数,
* 为给定的servlet上下文初始化Spring的Web应用程序上下文。
*
* @param servletContext current servlet context
* @return the new WebApplicationContext
* @see #ContextLoader(WebApplicationContext)
* @see #CONTEXT_CLASS_PARAM
* @see #CONFIG_LOCATION_PARAM
*/

public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
//判断给定的ServletContext是否已经存在WebApplicationContext,如果存在则抛出异常。
if (servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null) {
throw new IllegalStateException(
"Cannot initialize context because there is already a root application context present - " +
"check whether you have multiple ContextLoader* definitions in your web.xml!");
}

Log logger = LogFactory.getLog(ContextLoader.class);
servletContext.log("Initializing Spring root WebApplicationContext");
if (logger.isInfoEnabled()) {
logger.info("Root WebApplicationContext: initialization started");
}
long startTime = System.currentTimeMillis();

try {
// Store context in local instance variable, to guarantee that
// it is available on ServletContext shutdown.
//判断ContextLoader的context属性是否为空,为空表示WebApplicationContext还不存在。
if (this.context == null) {
//不存在则创建WebApplicationContext,该方法也是核心方法,我们后面详解。
this.context = createWebApplicationContext(servletContext);
}
//WebApplicationContext默认实现是XmlWebApplicationContext,
//XmlWebApplicationContext是ConfigurableWebApplicationContext子类
//所以该判断基本上都是正确的。
if (this.context instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
if (!cwac.isActive()) {//这个context还未执行refresh方法。
// The context has not yet been refreshed -> provide services such as
// setting the parent context, setting the application context id, etc
if (cwac.getParent() == null) {
// The context instance was injected without an explicit parent ->
// determine parent for root web application context, if any.
//通过loadParentContext()方法为其设置父上下文。
ApplicationContext parent = loadParentContext(servletContext);
cwac.setParent(parent);
}
//通过configureAndRefreshWebApplicationContext为根上下文构建bean工厂和bean对象。
configureAndRefreshWebApplicationContext(cwac, servletContext);
}
}

//将ApplicationContext放入ServletContext中,其key为
//WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);

ClassLoader ccl = Thread.currentThread().getContextClassLoader();
if (ccl == ContextLoader.class.getClassLoader()) {//如果当前线程的类加载器和ContextLoader类加载器一致
//那么创建的ApplicationContext赋值给ContextLoader.currentContext
//即在ContextLoader类被加载的进程中,如果创建了context的话,context赋值currentContext,方便取用
//而不是放在全局静态常量Map存起来。
currentContext = this.context;
}
else if (ccl != null) {//否则
//将ApplicationContext放入ContextLoader的全局静态常量Map中
//其中key为:Thread.currentThread().getContextClassLoader()即当前线程类加载器
//正常的context创建流程,走这里。
currentContextPerThread.put(ccl, this.context);
}

if (logger.isDebugEnabled()) {
logger.debug("Published root WebApplicationContext as ServletContext attribute with name [" +
WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE + "]");
}
if (logger.isInfoEnabled()) {
long elapsedTime = System.currentTimeMillis() - startTime;
logger.info("Root WebApplicationContext: initialization completed in " + elapsedTime + " ms");
}

return this.context;
}
catch (RuntimeException ex) {
logger.error("Context initialization failed", ex);
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex);
throw ex;
}
catch (Error err) {
logger.error("Context initialization failed", err);
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, err);
throw err;
}
}

归纳一下:

  • 首先判断servlectContext中是否已经存在根上下文,如果存在,则抛出异常;
  • 否则通过createWebApplicationContext方法创建新的根上下文。
  • 然后通过loadParentContext()方法为其设置父上下文。
  • 再通过configureAndRefreshWebApplicationContext为根上下文构建bean工厂和bean对象。
  • 最后把上下文存入servletContext,并且存入currentContextPerThread。
  • 至此初始化过程完毕,接下来可以获取WebApplicationContext,进而用getBean(“bean name”)得到bean。

1.3 创建Context

我们刚刚看到,initWebApplicationContext方法主要调用createWebApplicationContext方法来创建上下文:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* Instantiate the root WebApplicationContext for this loader, either the
* default context class or a custom context class if specified.
* <p>This implementation expects custom contexts to implement the
* {@link ConfigurableWebApplicationContext} interface.
* Can be overridden in subclasses.
*
* 实例化此加载程序的根WebApplicationContext(默认上下文类或自定义上下文类(如果已指定))。
* <p>此实现期望自定义上下文实现{@link ConfigurableWebApplicationContext}接口。 可以在子类中覆盖。
*
* <p>In addition, {@link #customizeContext} gets called prior to refreshing the
* context, allowing subclasses to perform custom modifications to the context.
* @param sc current servlet context
* @return the root WebApplicationContext
* @see ConfigurableWebApplicationContext
*/
protected WebApplicationContext createWebApplicationContext(ServletContext sc) {
//从web.xml配置的contextClass参数中获取上下文的实现类,如果contextClass为空,则使用默认的。
//下文有说明
Class<?> contextClass = determineContextClass(sc);
//根上下文的实现类必须是ConfigurableWebApplicationContext的子类,否则抛出异常
if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
throw new ApplicationContextException("Custom context class [" + contextClass.getName() +
"] is not of type [" + ConfigurableWebApplicationContext.class.getName() + "]");
}
//BeanUtils.instantiateClass工具方法,通过反射得到contextClass的构造方法,根据类名创建类
return (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
}

归纳一下:

  • createWebApplicationContext方法使用determineContextClass方法,从web.xml配置的contextClass参数中,获取要创建的context的具体实现类,如果没有指定,则默认使用XmlWebApplicationContext为其实现类。
  • 限制根上下文的实现类必须是ConfigurableWebApplicationContext的子类。
  • 使用BeanUtils.instantiateClass工具方法,根据类名创建类实例。

1.4 确定Context实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/**
* Return the WebApplicationContext implementation class to use, either the
* default XmlWebApplicationContext or a custom context class if specified.
*
* 返回要使用的WebApplicationContext实现类,
* 如果指定,则为自定义上下文类。否则默认为XmlWebApplicationContext
*
* @param servletContext current servlet context
* @return the WebApplicationContext implementation class to use
* @see #CONTEXT_CLASS_PARAM
* @see org.springframework.web.context.support.XmlWebApplicationContext
*/
protected Class<?> determineContextClass(ServletContext servletContext) {
//从web.xml获得参数contextClass,在一般的web项目中,此参数为null,即没有指定。
//(spring源生的XmlWebApplicationContext就挺好,没必要重复造轮子)
String contextClassName = servletContext.getInitParameter(CONTEXT_CLASS_PARAM);
if (contextClassName != null) {
try {
//如果指定了,返回类名
return ClassUtils.forName(contextClassName, ClassUtils.getDefaultClassLoader());
}
catch (ClassNotFoundException ex) {
throw new ApplicationContextException(
"Failed to load custom context class [" + contextClassName + "]", ex);
}
}
else {
//获得根上下文WebApplicationContext的默认实现类的类名,defaultStrategies是Properties类型,
//在CotnextLoader类开头static语句块中初始化
//获取当前包下面的ContextLoader.properties文件,文件内容是:
//org.springframework.web.context.WebApplicationContext=org.springframework.web.context.support.XmlWebApplicationContext
contextClassName = defaultStrategies.getProperty(WebApplicationContext.class.getName());
try {
return ClassUtils.forName(contextClassName, ContextLoader.class.getClassLoader());
}
catch (ClassNotFoundException ex) {
throw new ApplicationContextException(
"Failed to load default context class [" + contextClassName + "]", ex);
}
}
}

String contextClassName = servletContext.getInitParameter(CONTEXT_CLASS_PARAM);方法获取的,就是web.xml中配置指定的IoC容器类型:

1
2
3
4
<context-param>
<param-name>contextClass</param-name>
<param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
</context-param>

截止到这里为止,WebApplicationContext的实例就已经创建出来了。然后逻辑往下走,到了configureAndRefreshWebApplicationContext(cwac, servletContext)方法,该方法为根上下文构建bean工厂和bean对象。

1.5 配置和刷新Context

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
// The application context id is still set to its original default value
// -> assign a more useful id based on available information
//如果web.xml中配置了context的id,那么读取配置的id并使用。
String idParam = sc.getInitParameter(CONTEXT_ID_PARAM);
if (idParam != null) {
wac.setId(idParam);
}
else {//如果没有配置context的id,则使用默认的id。
// Generate default id...
wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
ObjectUtils.getDisplayString(sc.getContextPath()));
}
}
//把ServletContext塞入WebApplicationContext
wac.setServletContext(sc);
//读取web.xml中的<param-name>contextConfigLocation</param-name>的value值
//获取上下文的配置xml地址和文件名。
String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM);
if (configLocationParam != null) {
wac.setConfigLocation(configLocationParam);
}

// The wac environment's #initPropertySources will be called in any case when the context
// is refreshed; do it eagerly here to ensure servlet property sources are in place for
// use in any post-processing or initialization that occurs below prior to #refresh
ConfigurableEnvironment env = wac.getEnvironment();
if (env instanceof ConfigurableWebEnvironment) {
((ConfigurableWebEnvironment) env).initPropertySources(sc, null);
}

//customizeContext方法的功能是:
//对从web.xml的<context-param>中读取globalInitializerClasses和contextInitializerClasses参数
//获取配置的ApplicationContextInitializer实现类,然后对他们的实现类依次回调他们的initialize方法。

//ApplicationContextInitializer是Spring框架原有的接口,这个接口的主要作用就是
//在ConfigurableApplicationContext类型(或者子类型)的ApplicationContext做refresh之前
//允许我们对ConfiurableApplicationContext的实例做进一步的设置和处理。例如,根据上下文环境注册属性源或激活概要文件。
//ApplicationContextInitializer支持Order注解,表示执行顺序,越小越早执行;
customizeContext(sc, wac);

//核心代码,开始容器的初始化
wac.refresh();
}

String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM);方法获取的,就是web.xml中配置指定的IoC容器配置文件:

1
2
3
4
5
6
<!-- 配置Spring配置文件路径 -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:applicationContext.xmlclasspath*:applicationContext-shiro.xml
</param-value>
</context-param>

我们可以看到,容器的初始化逻辑,都在wac.refresh();中。

1.6 阶段性流程图

2 refresh()核心方法

我们之前说过,WebApplicationContext方法的默认实现是XmlWebApplicationContext,而XmlWebApplicationContext的refresh()方法,继承自它的父类AbstractApplicationContext。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
@Override
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// Prepare this context for refreshing.
//刷新预处理,和主流程关系不大,就是保存了容器的启动时间,启动标志等,同时给容器设置同步标识
prepareRefresh();

// Tell the subclass to refresh the internal bean factory.
//该方法调用链路,会启动Bean定义资源文件的载入方法loadBeanDefinitions方法
//值得下文讨论。
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

// Prepare the bean factory for use in this context.
//为BeanFactory配置容器特性,例如类加载器、事件处理器等
//还是一些准备工作,添加了两个后置处理器:ApplicationContextAwareProcessor,ApplicationListenerDetector
//还设置了 忽略自动装配 和 允许自动装配 的接口,如果不存在某个bean的时候,spring就自动注册singleton bean
//还设置了bean表达式解析器 等
prepareBeanFactory(beanFactory);

try {
// Allows post-processing of the bean factory in context subclasses.
//这是一个空方法,给子类继承用,执行自定义的BeanPost事件
postProcessBeanFactory(beanFactory);

// Invoke factory processors registered as beans in the context.
//调用所有注册的BeanFactoryPostProcessor的Bean
//执行自定义的BeanFactoryProcessor和内置的BeanFactoryProcessor
invokeBeanFactoryPostProcessors(beanFactory);

// Register bean processors that intercept bean creation.
// 注册BeanPostProcessor
registerBeanPostProcessors(beanFactory);

// Initialize message source for this context.
//初始化信息源,和国际化相关.
initMessageSource();

// Initialize event multicaster for this context.
//初始化容器事件传播器.
initApplicationEventMulticaster();

// Initialize other special beans in specific context subclasses.
// 空方法,给子类做定制
//调用子类的某些特殊Bean初始化方法
onRefresh();

// Check for listener beans and register them.
//为事件传播器注册事件监听器.
registerListeners();

// Instantiate all remaining (non-lazy-init) singletons.
//初始化所有剩余的,非懒加载的单例Bean
finishBeanFactoryInitialization(beanFactory);

// Last step: publish corresponding event.
//初始化容器的生命周期事件处理器,并发布容器的生命周期事件
finishRefresh();
}

catch (BeansException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Exception encountered during context initialization - " +
"cancelling refresh attempt: " + ex);
}

// Destroy already created singletons to avoid dangling resources.
//销毁已创建的Bean
destroyBeans();

// Reset 'active' flag.
//取消refresh操作,重置容器的同步标识。
cancelRefresh(ex);

// Propagate exception to caller.
throw ex;
}

finally {
// Reset common introspection caches in Spring's core, since we
// might not ever need metadata for singleton beans anymore...
//重设公共缓存
resetCommonCaches();
}
}
}

3 加载xml文件的beanDefinition

refresh()方法调用了obtainFreshBeanFactory()方法,在这个方法中,会发起IoC容器对配置文件的读取,并将其加载为beanDefinition,不过在了解beanDefinition加载过程之前,我们有需要了解一些前置知识点。

3.1 Resource资源文件框架

详见文章本博客《Spring Resource资源文件体系》;

3.2 加载前的准备工作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* Tell the subclass to refresh the internal bean factory.
* 让子类refresh内部bean工厂。
* @return the fresh BeanFactory instance
* @see #refreshBeanFactory()
* @see #getBeanFactory()
*/
protected ConfigurableListableBeanFactory obtainFreshBeanFactory() {
//核心方法在这,拆开看看
refreshBeanFactory();
ConfigurableListableBeanFactory beanFactory = getBeanFactory();
if (logger.isDebugEnabled()) {
logger.debug("Bean factory for " + getDisplayName() + ": " + beanFactory);
}
return beanFactory;
}

refreshBeanFactory()方法的实现在AbstractRefreshableApplicationContext类中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* This implementation performs an actual refresh of this context's underlying
* bean factory, shutting down the previous bean factory (if any) and
* initializing a fresh bean factory for the next phase of the context's lifecycle.
* 此实现对该上下文的基础bean工厂执行实际的刷新,
* 关闭前一个bean工厂(如果有),并为上下文生命周期的下一阶段初始化一个新的bean工厂。
*/
@Override
protected final void refreshBeanFactory() throws BeansException {
if (hasBeanFactory()) {//关闭前一个bean工厂(如果有)
destroyBeans();
closeBeanFactory();
}
try {
//初始化一个新的bean工厂
DefaultListableBeanFactory beanFactory = createBeanFactory();
beanFactory.setSerializationId(getId());
customizeBeanFactory(beanFactory);
//加载BeanDefinitions,核心方法,加载配置信息
loadBeanDefinitions(beanFactory);
synchronized (this.beanFactoryMonitor) {
this.beanFactory = beanFactory;
}
}
catch (IOException ex) {
throw new ApplicationContextException("I/O error parsing bean definition source for " + getDisplayName(), ex);
}
}

3.3 设置和获取Reader工具类

前文说过,XmlWebApplicationContext类是默认的Context类,所以默认情况下,loadBeanDefinitions(DefaultListableBeanFactory)调用的是XmlWebApplicationContext类中实现的loadBeanDefinitions(DefaultListableBeanFactory);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Override
protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException {
// Create a new XmlBeanDefinitionReader for the given BeanFactory.
//初始化XmlBeanDefinitionReader,Reader是将配置文件转为beanDefinition文件的主要工具类。
XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory);

// Configure the bean definition reader with this context's
// resource loading environment.
beanDefinitionReader.setEnvironment(getEnvironment());
//注意这里,将this,也就是XmlWebApplicationContext本身,作为XmlBeanDefinitionReader的ResourceLoader。
//XmlWebApplicationContext是实现了ResourceLoader接口的,并且它是ResourcePatternResolver接口的子类。
beanDefinitionReader.setResourceLoader(this);
beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this));

// Allow a subclass to provide custom initialization of the reader,
// then proceed with actually loading the bean definitions.
//空方法,给自定义的context类一个变更Reader类的回调机会
initBeanDefinitionReader(beanDefinitionReader);
//核心方法,继续往下看
loadBeanDefinitions(beanDefinitionReader);
}

loadBeanDefinitions(beanDefinitionReader)方法调用的是loadBeanDefinitions(XmlBeanDefinitionReader)方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* Load the bean definitions with the given XmlBeanDefinitionReader.
* <p>The lifecycle of the bean factory is handled by the refreshBeanFactory method;
* therefore this method is just supposed to load and/or register bean definitions.
* <p>Delegates to a ResourcePatternResolver for resolving location patterns
* into Resource instances.

* 使用给定的XmlBeanDefinitionReader加载Bean定义。beanFactory的生命周期由refreshBeanFactory方法处理;
* 因此该方法仅应加载 或 注册Bean定义。委托ResourcePatternResolver将位置模式解析为Resource实例。

* @throws IOException if the required XML document isn't found
* @see #refreshBeanFactory
* @see #getConfigLocations
* @see #getResources
* @see #getResourcePatternResolver
*/
protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws IOException {
//获取web.xml配置的configLocations,即配置文件的路径
String[] configLocations = getConfigLocations();
if (configLocations != null) {
for (String configLocation : configLocations) {
//遍历得到的configLocation路径,依次调用reader的loadBeanDefinitions方法解析每个路径
reader.loadBeanDefinitions(configLocation);
}
}
}

3.4 通配/完整路径解析策略

核心逻辑又进入了reader.loadBeanDefinitions(configLocation)方法,这个方法,XmlBeanDefinitionReader自己没有实现,是继承自AbstractBeanDefinitionReader的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
/**
* Load bean definitions from the specified resource location.
* <p>The location can also be a location pattern, provided that the
* ResourceLoader of this bean definition reader is a ResourcePatternResolver.
* @param location the resource location, to be loaded with the ResourceLoader
* (or ResourcePatternResolver) of this bean definition reader
* @param actualResources a Set to be filled with the actual Resource objects
* that have been resolved during the loading process. May be {@code null}
* to indicate that the caller is not interested in those Resource objects.
* @return the number of bean definitions found
* @throws BeanDefinitionStoreException in case of loading or parsing errors
* @see #getResourceLoader()
* @see #loadBeanDefinitions(org.springframework.core.io.Resource)
* @see #loadBeanDefinitions(org.springframework.core.io.Resource[])
*/
public int loadBeanDefinitions(String location, @Nullable Set<Resource> actualResources)
throws BeanDefinitionStoreException {
//获取ResourceLoader,ResourceLoader是用来确定location用哪种resource策略的逻辑封装。
//当前类XmlBeanDefinitionReader,在XmlWebApplicationContext类的loadBeanDefinitions(DefaultListableBeanFactory)方法中
//就已经指定了XmlBeanDefinitionReader的ResourceLoader。
//XmlBeanDefinitionReader的ResourceLoader就是XmlWebApplicationContext
ResourceLoader resourceLoader = getResourceLoader();
if (resourceLoader == null) {
throw new BeanDefinitionStoreException(
"Cannot import bean definitions from location [" + location + "]:
no ResourceLoader available");
}
//如果该reader注入的ResourceLoader是ResourcePatternResolver的子类。
//也就是支持解析配置路径是通配符形式的location。
//在web应用场景中,ResourceLoader已经指定了是XmlWebApplicationContext
//而XmlWebApplicationContext是ResourcePatternResolver的子类,所以判断必然为真
if (resourceLoader instanceof ResourcePatternResolver) {
// Resource pattern matching available.
try {
//那么,一个带通配符的路径,是可能返回多个Resource的
Resource[] resources = ((ResourcePatternResolver) resourceLoader).getResources(location);
int loadCount = loadBeanDefinitions(resources);
if (actualResources != null) {
for (Resource resource : resources) {
actualResources.add(resource);
}
}
if (logger.isDebugEnabled()) {
logger.debug("Loaded " + loadCount + " bean definitions
from location pattern [" + location + "]");
}
return loadCount;
}
catch (IOException ex) {
throw new BeanDefinitionStoreException(
"Could not resolve bean definition resource
pattern [" + location + "]", ex);
}
}
else {
// Can only load single resources by absolute URL.
//否则,则l该reader注入的ResourceLoader只支持解析配置路径是完整路径形式
//那么一个绝对路径,只会取回来一个resource
Resource resource = resourceLoader.getResource(location);
int loadCount = loadBeanDefinitions(resource);
if (actualResources != null) {
actualResources.add(resource);
}
if (logger.isDebugEnabled()) {
logger.debug("Loaded " + loadCount + " bean definitions
from location [" + location + "]");
}
return loadCount;
}
}

这里的两个分支,即resourceLoader支持解析location为通配符形式的和resourceLoader支持解析location为完整路径形式的,二者分别调用了:

  1. resourceLoader支持解析location为通配符形式的
    • PathMatchingResourcePatternResolver#getResources(String locationPattern)
    • AbstractBeanDefinitionReader#loadBeanDefinitions(Resource… resources)方法
  2. resourceLoader支持解析location为完整路径形式的
    • DefaultResourceLoader#getResources(String locationPattern)
    • XmlBeanDefinitionReader#loadBeanDefinitions(Resource resource)

而AbstractBeanDefinitionReader#loadBeanDefinitions(Resource… resources)方法的逻辑很简单:

1
2
3
4
5
6
7
8
9
10
@Override
public int loadBeanDefinitions(Resource... resources) throws BeanDefinitionStoreException {
Assert.notNull(resources, "Resource array must not be null");
int counter = 0;
for (Resource resource : resources) {
//遍历Resource,依次调用loadBeanDefinitions(Resource resource)
counter += loadBeanDefinitions(resource);
}
return counter;
}

所以最后殊途同归,最后都是落在了XmlBeanDefinitionReader#loadBeanDefinitions(Resource resource)方法上。

3.5 编码设置和循环依赖检测

XmlBeanDefinitionReader的loadBeanDefinitions(Resource resource)重写方法,主要就是加载xml文件配置的beanDefinition:

1
2
3
4
5
6
7
8
9
10
11
/**
* Load bean definitions from the specified XML file.
* @param resource the resource descriptor for the XML file
* @return the number of bean definitions found
* @throws BeanDefinitionStoreException in case of loading or parsing errors
*/
@Override
public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {
//对传进来的Resource又进行了一次封装,变成了编码后的Resource
return loadBeanDefinitions(new EncodedResource(resource));
}

可以看到,resource在这里被封装为了EncodedResource,我们继续往下看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/**
* Load bean definitions from the specified XML file.
* @param encodedResource the resource descriptor for the XML file,
* allowing to specify an encoding to use for parsing the file
* @return the number of bean definitions found
* @throws BeanDefinitionStoreException in case of loading or parsing errors
*/
public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
Assert.notNull(encodedResource, "EncodedResource must not be null");
if (logger.isInfoEnabled()) {
logger.info("Loading XML bean definitions from " + encodedResource);
}
//resourcesCurrentlyBeingLoaded是一个ThreadLocal,里面存放Resource包装类EncodedResource的set集合
Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();
if (currentResources == null) {//如果set不存在,new一个
currentResources = new HashSet<>(4);
this.resourcesCurrentlyBeingLoaded.set(currentResources);
}
//如果set中已有这个元素则返回false,进入该条件抛出异常
if (!currentResources.add(encodedResource)) {
//只有当前线程重复加载了某个资源,这里才会抛出异常
//用来检测是否循环加载某个Resource,如果是,提醒需要检查导入的definitions
throw new BeanDefinitionStoreException(
"Detected cyclic loading of " + encodedResource + " - check your import definitions!");
}
try {
//获取封装的InputStream
InputStream inputStream = encodedResource.getResource().getInputStream();
try {
InputSource inputSource = new InputSource(inputStream);
//没有设置编码集,跳过
if (encodedResource.getEncoding() != null) {
inputSource.setEncoding(encodedResource.getEncoding());
}
return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
}
finally {
inputStream.close();
}
}
catch (IOException ex) {
throw new BeanDefinitionStoreException(
"IOException parsing XML document from " + encodedResource.getResource(), ex);
}
finally {
currentResources.remove(encodedResource);
if (currentResources.isEmpty()) {
this.resourcesCurrentlyBeingLoaded.remove();
}
}
}

我们看到,核心逻辑,又进入了doLoadBeanDefinitions方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
* Actually load bean definitions from the specified XML file.
* @param inputSource the SAX InputSource to read from
* @param resource the resource descriptor for the XML file
* @return the number of bean definitions found
* @throws BeanDefinitionStoreException in case of loading or parsing errors
* @see #doLoadDocument
* @see #registerBeanDefinitions
*/
protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
throws BeanDefinitionStoreException {
try {
//根据不同的xml约束(dtd,xsd等),将xml文件生成对应的文档对象
//这个方法里面涉及xml的解析,不赘述,简单来说:
//检测解析传入的xml文件(也就是resource)时该用哪种验证方式
//如果这个文件有DOCTYPE声明,那么就用DTD验证,否则就使用XSD验证模式。
//使用标准JAXP配置XML解析器,加载InputSource的Document对象,然后返回一个新的DOM对象
//注意,这个Document对象,是W3C定义的标准XML对象,跟spring无关。
Document doc = doLoadDocument(inputSource, resource);
//核心方法,beanDefinitions的注册
return registerBeanDefinitions(doc, resource);
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (SAXParseException ex) {
throw new XmlBeanDefinitionStoreException(resource.getDescription(),
"Line " + ex.getLineNumber() + " in XML document from " + resource + " is invalid", ex);
}
catch (SAXException ex) {
throw new XmlBeanDefinitionStoreException(resource.getDescription(),
"XML document from " + resource + " is invalid", ex);
}
catch (ParserConfigurationException ex) {
throw new BeanDefinitionStoreException(resource.getDescription(),
"Parser configuration exception parsing XML from " + resource, ex);
}
catch (IOException ex) {
throw new BeanDefinitionStoreException(resource.getDescription(),
"IOException parsing XML document from " + resource, ex);
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(resource.getDescription(),
"Unexpected exception parsing XML document from " + resource, ex);
}
}

OK,两个核心方法,doLoadDocument和registerBeanDefinitions方法,前者负责解析xml文件,后者负责注册BeanDefinitions,我们分别来分析。先看doLoadDocument()方法

3.6 解析XML文件

此方法在XmlBeanDefinitionReader类中

1
2
3
4
5
6
7
8
9
10
11
/**
* Actually load the specified document using the configured DocumentLoader.
* @param inputSource the SAX InputSource to read from --从中读取的SAX输入源
* @param resource the resource descriptor for the XML file --xml文件的资源描述符
* @return the DOM Document DOM文档对象
*
* 使用配置好的DocumentLoader文档加载器加载指定的文档
*/
protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception {
return this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler,
getValidationModeForResource(resource), isNamespaceAware());

3.6.1 参数1

上文中的getEntityResolver() 方法返回 XmlBeanDefinitionReader 类的 entityResolver 属性。

entityResolver 属性在 loadBeanDefinitions(DefaultListableBeanFactory beanFactory) 方法中被赋值。

beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this));

和resourceLoader一样,this拿的是XmlWebApplicationContext实例,我们再来看下ResourceEntityResolver的构造方法

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* Create a ResourceEntityResolver for the specified ResourceLoader
* (usually, an ApplicationContext).
* @param resourceLoader the ResourceLoader (or ApplicationContext)
* to load XML entity includes with
*
* 为指定的ResourceLoade(通常是应用上下文)r创建一个ResourceEntityResolver
*/
public ResourceEntityResolver(ResourceLoader resourceLoader) {
super(resourceLoader.getClassLoader());
//此处解析器拿到了上下文的引用
this.resourceLoader = resourceLoader;
}

调用了父类构造,再跟进一层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* Create a new DelegatingEntityResolver that delegates to
* a default {@link BeansDtdResolver} and a default {@link PluggableSchemaResolver}.
* <p>Configures the {@link PluggableSchemaResolver} with the supplied
* {@link ClassLoader}.
* @param classLoader the ClassLoader to use for loading
* (can be {@code null}) to use the default ClassLoader)
*/
public DelegatingEntityResolver(ClassLoader classLoader) {
//这两个解析器和约束的类型有关,DTD
this.dtdResolver = new BeansDtdResolver();
//可插拔的Schema解析器,拿的上下文的类加载器
this.schemaResolver = new PluggableSchemaResolver(classLoader);
}

中间的this.errorHandler参数可忽略。

3.6.2 参数2

ok,然后是getValidationModeForResource(resource)入参。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* Gets the validation mode for the specified {@link Resource}. If no explicit
* validation mode has been configured then the validation mode is
* {@link #detectValidationMode detected}.
* <p>Override this method if you would like full control over the validation
* mode, even when something other than {@link #VALIDATION_AUTO} was set.
*
* 通过给定Resource给出验证模式。如果没有明确配置验证模式,那么调用detectValidationMode方法去检测。
*/
protected int getValidationModeForResource(Resource resource) {
//默认自动验证,为1
int validationModeToUse = getValidationMode();
//如果有给出具体验证方式,则返回结果
if (validationModeToUse != VALIDATION_AUTO) {
return validationModeToUse;
}
//检测验证模式,进入这个方法
int detectedMode = detectValidationMode(resource);
if (detectedMode != VALIDATION_AUTO) {
return detectedMode;
}
// Hmm, we didn't get a clear indication... Let's assume XSD,
// since apparently no DTD declaration has been found up until
// detection stopped (before finding the document's root tag).
// 如果实在不能判断验证模式是那种就使用XSD方式,
// 因为检测完后还是没有发现DTD模式的声明(在查找document的根标签之前)。
// 值为3
return VALIDATION_XSD;
}

getValidationModeForResource的核心方法是detectValidationMode(),我们继续:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/**
* Detects which kind of validation to perform on the XML file identified
* by the supplied {@link Resource}. If the file has a {@code DOCTYPE}
* definition then DTD validation is used otherwise XSD validation is assumed.
* <p>Override this method if you would like to customize resolution
* of the {@link #VALIDATION_AUTO} mode.
*
* 检测执行xml文件时该用哪种验证方式,这个xml由Resource对象提供
* 如果这个文件有DOCTYPE声明,那么就用DTD验证,否则就默认使用XSD。
* 如果你想要自定义自动验证模式的解决方式,你可以覆盖这个方法
*/
protected int detectValidationMode(Resource resource) {
//默认false
if (resource.isOpen()) {
throw new BeanDefinitionStoreException(
"Passed-in Resource [" + resource + "] contains an open stream: " +
"cannot determine validation mode automatically. Either pass in a Resource " +
"that is able to create fresh streams, or explicitly specify the validationMode " +
"on your XmlBeanDefinitionReader instance.");
}
InputStream inputStream;
try {
inputStream = resource.getInputStream();
}
catch (IOException ex) {
throw new BeanDefinitionStoreException(
"Unable to determine validation mode for [" + resource + "]: cannot open InputStream. " +
"Did you attempt to load directly from a SAX InputSource without specifying the " +
"validationMode on your XmlBeanDefinitionReader instance?", ex);
}

try {
//XmlBeanDefinitionReader的validationModeDetector属性有默认实现XmlValidationModeDetector
//核心方法,接下来进入这个方法看下
return this.validationModeDetector.detectValidationMode(inputStream);
}
catch (IOException ex) {
throw new BeanDefinitionStoreException("Unable to determine validation mode for [" +
resource + "]: an error occurred whilst reading from the InputStream.", ex);
}
}

所以来看validationModeDetector调用的detectValidationMode方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/**
* Detect the validation mode for the XML document in the supplied {@link InputStream}.
* Note that the supplied {@link InputStream} is closed by this method before returning.
*
* 在提供的InputStream中检测XML文档的验证模式
* 注意,提供的InputStream在这个方法return之前会被关闭
*/
public int detectValidationMode(InputStream inputStream) throws IOException {
// Peek into the file to look for DOCTYPE.
// 查找文件的DOCTYPE
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
try {
boolean isDtdValidated = false;
String content;
while ((content = reader.readLine()) != null) {
//读一行字符串就干掉字符串里面的注释,如果全是注释全干掉
//主要为了剥离注释,因为非注释内容要么是DOCTYPE声明要么是文档的根元素对象
content = consumeCommentTokens(content);
//剥离注释后完全没内容就继续循环
if (this.inComment || !StringUtils.hasText(content)) {
continue;
}
//有DOCTYPE声明,就跳出去
if (hasDoctype(content)) {
isDtdValidated = true;
break;
}
//注释不能进去。开头是"<",后面第一个字符是字母,就进入。
//比如'<beans xmlns="http://www.springframework.org/schema/beans"'
//进去后跳出循环
if (hasOpeningTag(content)) {
// End of meaningful data...
break;
}
}
//当遍历到名称空间了也就是"<beans xmlns=...>"还没有DOCTYPE声明,
//那么就判定他为XSD验证
return (isDtdValidated ? VALIDATION_DTD : VALIDATION_XSD);
}
catch (CharConversionException ex) {
// Choked on some character encoding...
// Leave the decision up to the caller.
return VALIDATION_AUTO;
}
finally {
//关流
reader.close();
}
}

3.6.3 解析的核心逻辑

讲完核心的两个入参后进入正主,loadDocument方法。

1
2
return this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler,
getValidationModeForResource(resource), isNamespaceAware());

documentLoader属性的默认实现是DefaultDocumentLoader;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* Load the {@link Document} at the supplied {@link InputSource} using the standard JAXP-configured
* XML parser.
*
* 使用标准JAXP配置XML解析器加载InputSource的Document对象
*/
@Override
public Document loadDocument(InputSource inputSource, EntityResolver entityResolver,
ErrorHandler errorHandler, int validationMode, boolean namespaceAware) throws Exception {

//创建文档构建器工厂对象,并初始化一些属性
//如果验证模式为XSD,那么强制支持XML名称空间,并加上schema属性
DocumentBuilderFactory factory = createDocumentBuilderFactory(validationMode, namespaceAware);
if (logger.isDebugEnabled()) {
logger.debug("Using JAXP provider [" + factory.getClass().getName() + "]");
}

//创建一个JAXP文档构建器
DocumentBuilder builder = createDocumentBuilder(factory, entityResolver, errorHandler);

//按照XML文档解析给定inputSource的内容,然后返回一个新的DOM对象
return builder.parse(inputSource);
}

最后一步,DocumentBuilder的默认实现是DocumentBuilderImpl,这个是jdk里面的xml解析器了,不再赘述。

至此,我们拿到了Document对象

3.7 准备注册BeanDefinitions

两个核心方法,看完了doLoadDocument方法,我们再来看registerBeanDefinitions方法,后者负责注册BeanDefinitions,我们分别来分析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* Register the bean definitions contained in the given DOM document.
* Called by {@code loadBeanDefinitions}.
* <p>Creates a new instance of the parser class and invokes
* {@code registerBeanDefinitions} on it.
* 注册包含在给定DOM文档中的bean定义。由{@code loadBeanDefinitions}调用。<p>创建解析器类的新实例,并在其上调用{@code registerBeanDefinitions}。
*
* 注册包含在给定DOM文档对象中的 bean definition
* 被loadBeanDefinitions方法所调用
* 解析class后创建一个新的实例,并调用registerBeanDefinitions方法
*/
public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
//其实就是new了一个DefaultBeanDefinitionDocumentReader工具类。
BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
//getRegistry()方法拿的是bean工厂对象,beanDefinition注册在工厂中
//这个方法就是返回已经被注册在工厂中的beanDefinitions数量
int countBefore = getRegistry().getBeanDefinitionCount();
//核心方法
//createReaderContext创建了XmlReaderContext对象
//XmlReaderContext对象是BeanDefinition读取过程中传递的上下文,封装相关的的配置和状态
documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
//返回上文这个核心方法真正注册在工厂中的beanDefinition数量
return getRegistry().getBeanDefinitionCount() - countBefore;
}

核心方法documentReader.registerBeanDefinitions实现在DefaultBeanDefinitionDocumentReader类中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 这个方法在刚创建的DefaultBeanDefinitionDocumentReader中
*
* This implementation parses bean definitions according to the "spring-beans" XSD
* (or DTD, historically).
* <p>Opens a DOM Document; then initializes the default settings
* specified at the {@code <beans/>} level; then parses the contained bean definitions.
*
* 根据“spring-beans"的XSD(或者DTD)去解析bean definition
* 打开一个DOM文档,然后初始化在<beans/>层级上指定的默认设置,然后解析包含在其中的bean definitions
*/
@Override
public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {
//入参时创建的XmlReaderContext对象
this.readerContext = readerContext;
logger.debug("Loading bean definitions");
//拿到了xml文档对象的根元素
Element root = doc.getDocumentElement();
//进入这个方法进行查看
doRegisterBeanDefinitions(root);
}

再看DefaultBeanDefinitionDocumentReader类的doRegisterBeanDefinitions方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/**
* Register each bean definition within the given root {@code <beans/>} element.
*
* 从<beans />配置中注册每一个bean,如果有嵌套的beans,那么递归执行这个方法。
*/
protected void doRegisterBeanDefinitions(Element root) {
// Any nested <beans> elements will cause recursion in this method. In
// order to propagate and preserve <beans> default-* attributes correctly,
// keep track of the current (parent) delegate, which may be null. Create
// the new (child) delegate with a reference to the parent for fallback purposes,
// then ultimately reset this.delegate back to its original (parent) reference.
// this behavior emulates a stack of delegates without actually necessitating one.

// delegate可以理解为配置参数/属性,传递的上下文,封装相关的的配置和状态等信息的集合对象
// 任何被嵌套的<beans>元素都会导致此方法的递归。为了正确的传播和保存delegate内的信息,所以这里需要做delegate交接
// 它可能为null
// 为了能够回退,新的(子)delegate具有父的引用,最终会重置this.delegate回到它的初始(父)引用。
BeanDefinitionParserDelegate parent = this.delegate;

//重要方法,创建一个新的代理,继承父delegate,并初始化一些默认值
this.delegate = createDelegate(getReaderContext(), root, parent);

//默认明明空间是"http://www.springframework.org/schema/beans"
if (this.delegate.isDefaultNamespace(root)) {
String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);

//如果当前层级的<beans />配置有profile属性,那么处理这个属性。不是很重要不赘述
if (StringUtils.hasText(profileSpec)) {
String[] specifiedProfiles = StringUtils.tokenizeToStringArray(
profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {
if (logger.isInfoEnabled()) {
logger.info("Skipped XML bean definition file due to specified profiles [" + profileSpec +
"] not matching: " + getReaderContext().getResource());
}
return;
}
}
}

//xml预处理,子类没有重写里面就是空实现
preProcessXml(root);

//重要方法,生成BeanDefinition,并注册在工厂中
parseBeanDefinitions(root, this.delegate);

//xml后处理,子类没有重写里面就是空实现
postProcessXml(root);

this.delegate = parent;
}

好,这里两个核心方法,我们一个一个来

3.8 生成代理和传递基础配置

先看DefaultBeanDefinitionDocumentReader类的createDelegate方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
protected BeanDefinitionParserDelegate createDelegate(
XmlReaderContext readerContext, Element root, @Nullable BeanDefinitionParserDelegate parentDelegate) {
//生成用来解析XML bean definition的有状态Delegate类,用来被主解析器和其他扩展使用。
//上下文readerContext继承在其内部
BeanDefinitionParserDelegate delegate = new BeanDefinitionParserDelegate(readerContext);
//给新生成的delegate做继承和赋初值
delegate.initDefaults(root, parentDelegate);
return delegate;
}


/**
* Initialize the default lazy-init, autowire, dependency check settings,
* init-method, destroy-method and merge settings. Support nested 'beans'
* element use cases by falling back to the given parent in case the
* defaults are not explicitly set locally.
*
* 用默认的值填充DocumentDefaultsDefinition实例
* 通过使用parentDefaults(父代理的default属性),来解决嵌套的'beans'元素情况,以防默认值在局部设定不明确
*/
public void initDefaults(Element root, BeanDefinitionParserDelegate parent) {

//重要方法,构造当前delegate的默认配置信息封装类,为其赋初值。
//this.defaults,也就是当前delegate的默认配置信息封装类,它的类型是DocumentDefaultsDefinition
//如果有父delegate,也要让新生成的defaults继承父delegate的defaults
populateDefaults(this.defaults, (parent != null ? parent.defaults : null), root);

//默认没做任何实现
this.readerContext.fireDefaultsRegistered(this.defaults);
}


/**
* Populate the given DocumentDefaultsDefinition instance with the default lazy-init,
* autowire, dependency check settings, init-method, destroy-method and merge settings.
* Support nested 'beans' element use cases by falling back to {@code parentDefaults}
* in case the defaults are not explicitly set locally.
* 用默认的值填充DocumentDefaultsDefinition实例
* 通过使用parentDefaults(父代理的default属性),来解决嵌套的'beans'元素情况,以防默认值在局部设定不明确
* @param defaults the defaults to populate
* @param parentDefaults the parent BeanDefinitionParserDelegate (if any) defaults to fall back to
* @param root the root element of the current bean definition document (or nested beans element)
*/
protected void populateDefaults(DocumentDefaultsDefinition defaults, @Nullable DocumentDefaultsDefinition parentDefaults, Element root) {
//根元素上如果没有设定值,则返回"default"字符串
String lazyInit = root.getAttribute(DEFAULT_LAZY_INIT_ATTRIBUTE);
//如果为"default",先看parentDefaults有没有,有用它的,没有用"false"
if (isDefaultValue(lazyInit)) {
// Potentially inherited from outer <beans> sections, otherwise falling back to false.
// 可能从外部<beans>继承,否则返回false
lazyInit = (parentDefaults != null ? parentDefaults.getLazyInit() : FALSE_VALUE);
}
defaults.setLazyInit(lazyInit);

//下面的逻辑和lazyInit差不多,即看本级是否有显性配置,没有的话,看父类有没有继承,也没有就设默认值。

String merge = root.getAttribute(DEFAULT_MERGE_ATTRIBUTE);
if (isDefaultValue(merge)) {
// Potentially inherited from outer <beans> sections, otherwise falling back to false.
merge = (parentDefaults != null ? parentDefaults.getMerge() : FALSE_VALUE);
}
defaults.setMerge(merge);

String autowire = root.getAttribute(DEFAULT_AUTOWIRE_ATTRIBUTE);
if (isDefaultValue(autowire)) {
// Potentially inherited from outer <beans> sections, otherwise falling back to 'no'.
autowire = (parentDefaults != null ? parentDefaults.getAutowire() : AUTOWIRE_NO_VALUE);
}
defaults.setAutowire(autowire);

if (root.hasAttribute(DEFAULT_AUTOWIRE_CANDIDATES_ATTRIBUTE)) {
defaults.setAutowireCandidates(root.getAttribute(DEFAULT_AUTOWIRE_CANDIDATES_ATTRIBUTE));
}
else if (parentDefaults != null) {
defaults.setAutowireCandidates(parentDefaults.getAutowireCandidates());
}

if (root.hasAttribute(DEFAULT_INIT_METHOD_ATTRIBUTE)) {
defaults.setInitMethod(root.getAttribute(DEFAULT_INIT_METHOD_ATTRIBUTE));
}
else if (parentDefaults != null) {
defaults.setInitMethod(parentDefaults.getInitMethod());
}

if (root.hasAttribute(DEFAULT_DESTROY_METHOD_ATTRIBUTE)) {
defaults.setDestroyMethod(root.getAttribute(DEFAULT_DESTROY_METHOD_ATTRIBUTE));
}
else if (parentDefaults != null) {
defaults.setDestroyMethod(parentDefaults.getDestroyMethod());
}
//extractSource方法这里没有做任何实现,默认返回null
defaults.setSource(this.readerContext.extractSource(root));
}

3.9 拆解根层级,并根据子标签命名空间做不同处理

我们再来看另一个核心方法:DefaultBeanDefinitionDocumentReader类的parseBeanDefinitions方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/**
* Parse the elements at the root level in the document:
* 将文档中<beans/>层级下的元素一层层剥开,为子标签的不同命名空间选择不同策略。
* 主要两种命名空间:beans命名空间 +其他命名空间(context/aop等命名空间)

* "import", "alias", "bean"等标签都属于beans命名空间
*
* 这里判断是否是不同的命名空间,不同命名空间,后续会使用不同解析器来解析。
*
* @param root the DOM root element of the document
*/
protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
//判断默认命名空间是"http://www.springframework.org/schema/beans"
if (delegate.isDefaultNamespace(root)) {
//获取根元素(<beans />)下的子Node,注意,Node不一定是子标签,可能是回车,可能是注释
NodeList nl = root.getChildNodes();
for (int i = 0; i < nl.getLength(); i++) {
Node node = nl.item(i);
if (node instanceof Element) {
//拿到了<beans>下的子标签
Element ele = (Element) node;
//如果该标签属于beans的名称空间,则进入这个方法
//xmlns="http://www.springframework.org/schema/beans"
if (delegate.isDefaultNamespace(ele)) {
parseDefaultElement(ele, delegate);
}
else {
//如果该标签属于其他的名称空间比如:context,aop等
//xmlns:aop="http://www.springframework.org/schema/aop"
//xmlns:context="http://www.springframework.org/schema/context"
//比如我们使用注解注入的话,会在spring的配置文件加<context:annotation-config/>
//这个启用注解的标签,就属于context命名空间
//在接下来的逻辑中,不同命名空间,使用不同处理器来解析
delegate.parseCustomElement(ele);
}
}
}
}
else {
delegate.parseCustomElement(root);
}
}

3.10 解析beans命名空间的配置

前文我们说到,如果该标签属于beans的名称空间,则进入parseDefaultElement(ele, delegate)方法,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {
//<import>标签进入这个方法
if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) {
importBeanDefinitionResource(ele);
}//<alias>标签进入这个方法
else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) {
processAliasRegistration(ele);
}//<bean>标签进入这个方法
else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) {
//核心方法
processBeanDefinition(ele, delegate);
}//又嵌套一层<beans>标签进入这个方法
else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) {
// recurse
// 如果是嵌套的beans,那么就会重新调用doRegisterBeanDefinitions进行递归
doRegisterBeanDefinitions(ele);
}
}

上述几个标签中,我们主要来看<bean>标签的解析方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* Process the given bean element, parsing the bean definition
* and registering it with the registry.
* 处理bean元素,解析成bean definition并注册到工厂中
*/
protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {
//使用delegate的内容(默认配置)来解析bean元素,核心方法
BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);
if (bdHolder != null) {
//如果有要求的话渲染beanDefinition
bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);
try {
// Register the final decorated instance.
//注册最终被渲染的实例到工厂中
BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());
}
catch (BeanDefinitionStoreException ex) {
getReaderContext().error("Failed to register bean definition with name '" +
bdHolder.getBeanName() + "'", ele, ex);
}
// Send registration event.
// 发送注册事件
// 这里是空实现
getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder));
}
}

3.11 解析并生成BeanDefinition实例

进入BeanDefinitionParserDelegate类的parseBeanDefinitionElement方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
/**
* Parses the supplied {@code <bean>} element. May return {@code null}
* if there were errors during parse. Errors are reported to the
* {@link org.springframework.beans.factory.parsing.ProblemReporter}.
*
* 解析bean元素。如果解析过程中发生错误则返回空
*/
public BeanDefinitionHolder parseBeanDefinitionElement(Element ele) {
return parseBeanDefinitionElement(ele, null);
}


public BeanDefinitionHolder parseBeanDefinitionElement(Element ele, BeanDefinition containingBean) {
//拿bean标签上的id
String id = ele.getAttribute(ID_ATTRIBUTE);
//拿bean标签上的name属性
String nameAttr = ele.getAttribute(NAME_ATTRIBUTE);
List<String> aliases = new ArrayList<String>();
//有name属性进入
if (StringUtils.hasLength(nameAttr)) {
//name属性对应的name值如果有分隔符",; ",那么切分成数组
String[] nameArr = StringUtils.tokenizeToStringArray(nameAttr, MULTI_VALUE_ATTRIBUTE_DELIMITERS);
//这些name值就是别名
aliases.addAll(Arrays.asList(nameArr));
}
//指定了id就用id值作为bean名称
String beanName = id;
//如果没有id,但是指定了name,就用name值作为bean名称
if (!StringUtils.hasText(beanName) && !aliases.isEmpty()) {
//拿第一个name值作为bean名称,其余的还是别名
beanName = aliases.remove(0);
if (logger.isDebugEnabled()) {
logger.debug("No XML 'id' specified - using '" + beanName +
"' as bean name and " + aliases + " as aliases");
}
}

if (containingBean == null) {
//检查bean名称和别名是否已经被使用了,如果用了就报错
//同时把这个bean的名称和别名加入代理的usedNames属性中
//private final Set<String> usedNames = new HashSet<String>();
checkNameUniqueness(beanName, aliases, ele);
}
//直接进入这个方法
AbstractBeanDefinition beanDefinition = parseBeanDefinitionElement(ele, beanName, containingBean);
if (beanDefinition != null) {
//既没有指定id,也没有指定name就走这里面
if (!StringUtils.hasText(beanName)) {
try {
//前面containingBean传递时为null,这里不走这个方法
if (containingBean != null) {
beanName = BeanDefinitionReaderUtils.generateBeanName(
beanDefinition, this.readerContext.getRegistry(), true);
}
else {
//生成一个bean名称,beanName
//如果这个bean是内部bean,全限定名后加#号再加哈希值
//如果是顶层bean,那么后面加#号再从0开始加数字,id已被注册数字就增1,直到唯一
//比如:tk.mybatis.spring.mapper.MapperScannerConfigurer#0
beanName = this.readerContext.generateBeanName(beanDefinition);
// Register an alias for the plain bean class name, if still possible,
// if the generator returned the class name plus a suffix.
// This is expected for Spring 1.2/2.0 backwards compatibility.
//如果可能的话,如果生成器返回类名加后缀,则注册一个别名,这个别名就是该类的类名。
//这是为了向后兼容
String beanClassName = beanDefinition.getBeanClassName();
if (beanClassName != null &&
beanName.startsWith(beanClassName) && beanName.length() > beanClassName.length() &&
!this.readerContext.getRegistry().isBeanNameInUse(beanClassName)) {
//如果该类名没有被使用,那么注册该类名作为别名,比如:
//tk.mybatis.spring.mapper.MapperScannerConfigurer作为
//tk.mybatis.spring.mapper.MapperScannerConfigurer#0的别名
aliases.add(beanClassName);
}
}
if (logger.isDebugEnabled()) {
logger.debug("Neither XML 'id' nor 'name' specified - " +
"using generated bean name [" + beanName + "]");
}
}
catch (Exception ex) {
error(ex.getMessage(), ele);
return null;
}
}
String[] aliasesArray = StringUtils.toStringArray(aliases);
//返回beanDefinition的持有者
return new BeanDefinitionHolder(beanDefinition, beanName, aliasesArray);
}
return null;
}

在上文中,核心方法是这句话:

AbstractBeanDefinition beanDefinition = parseBeanDefinitionElement(ele, beanName, containingBean);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
/**
* Parse the bean definition itself, without regard to name or aliases. May return
* 不关注名称和别名,只解析bean definition自身
* {@code null} if problems occurred during the parsing of the bean definition.
*/
@Nullable
public AbstractBeanDefinition parseBeanDefinitionElement(
Element ele, String beanName, @Nullable BeanDefinition containingBean) {
//栈结构,解析的时候放入bean标签,解析完成弹出
//如果还嵌套有子标签,则后续放入子标签
//栈结构当然后进先出,所以子标签先弹出
this.parseState.push(new BeanEntry(beanName));

String className = null;
if (ele.hasAttribute(CLASS_ATTRIBUTE)) {
//如果有指定class属性,则拿到class属性值
className = ele.getAttribute(CLASS_ATTRIBUTE).trim();
}
String parent = null;
if (ele.hasAttribute(PARENT_ATTRIBUTE)) {
//如果有指定parent属性,则拿到parent属性值
parent = ele.getAttribute(PARENT_ATTRIBUTE);
}

try {
//创建BeanDefinition并设置两属性,核心方法
AbstractBeanDefinition bd = createBeanDefinition(className, parent);
//将bean标签上的属性设置到bean definition中,核心方法2
parseBeanDefinitionAttributes(ele, beanName, containingBean, bd);
//如果bean标签下有子标签为description,拿到标签中的文本,设置到bean definition中
bd.setDescription(DomUtils.getChildElementValueByTagName(ele, DESCRIPTION_ELEMENT));
//如果bean标签下有子标签为meta,拿到他的key和value属性,设置到bean definition中
parseMetaElements(ele, bd);
//如果bean标签下有子标签为lookup-method,拿到他的name和bean属性,设置到bean definition中
parseLookupOverrideSubElements(ele, bd.getMethodOverrides());
//如果bean标签下有子标签为replaced-method,设置bean definition
parseReplacedMethodSubElements(ele, bd.getMethodOverrides());
//如果bean标签下有子标签为constructor-arg,设置bean definition的构造方式
parseConstructorArgElements(ele, bd);
//这个标签比较常用,为Property标签
//解析Property的属性设置到bean definition中
parsePropertyElements(ele, bd);
//有qualifier子标签才走这个方法
parseQualifierElements(ele, bd);
//设置资源
bd.setResource(this.readerContext.getResource());
//这里为null
bd.setSource(extractSource(ele));

return bd;
}
catch (ClassNotFoundException ex) {
error("Bean class [" + className + "] not found", ele, ex);
}
catch (NoClassDefFoundError err) {
error("Class that bean class [" + className + "] depends on not found", ele, err);
}
catch (Throwable ex) {
error("Unexpected failure during bean definition parsing", ele, ex);
}
finally {
//解析的时候放入,解析完成弹出
this.parseState.pop();
}
return null;
}

两个重要的核心方法

1
2
3
4
5
6
7
8
//创建BeanDefinition并设置两属性,核心方法
AbstractBeanDefinition bd = createBeanDefinition(className, parent);
//将bean标签上的属性设置到bean definition中,核心方法2
parseBeanDefinitionAttributes(ele, beanName, containingBean, bd);
...
//这个标签比较常用,为Property标签
//解析Property的属性设置到bean definition中
parsePropertyElements(ele, bd);

我们依次来看:

3.11.1 创建实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* Create a bean definition for the given class name and parent name.
* 通过给定的className和parentName创建beanDefinition
*
* @param className the name of the bean class
* @param parentName the name of the bean's parent bean
* @return the newly created bean definition
* @throws ClassNotFoundException if bean class resolution was attempted but failed
*/
protected AbstractBeanDefinition createBeanDefinition(@Nullable String className, @Nullable String parentName)
throws ClassNotFoundException {

return BeanDefinitionReaderUtils.createBeanDefinition(
parentName, className, this.readerContext.getBeanClassLoader());
}

调用BeanDefinitionReaderUtils的静态方法createBeanDefinition():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* Create a new GenericBeanDefinition for the given parent name and class name,
* eagerly loading the bean class if a ClassLoader has been specified.

* 通过给定的parentName和className穿件一个新的GenericBeanDefinition
* 如果指定了ClassLoader,就提前加载bean class

* @param parentName the name of the parent bean, if any
* @param className the name of the bean class, if any
* @param classLoader the ClassLoader to use for loading bean classes
* (can be {@code null} to just register bean classes by name)
* @return the bean definition
* @throws ClassNotFoundException if the bean class could not be loaded
*/
public static AbstractBeanDefinition createBeanDefinition(
@Nullable String parentName, @Nullable String className, @Nullable ClassLoader classLoader)
throws ClassNotFoundException {

GenericBeanDefinition bd = new GenericBeanDefinition();
bd.setParentName(parentName);
if (className != null) {
if (classLoader != null) {
//有classloader,则通过反射,动态加载一个实例返回
bd.setBeanClass(ClassUtils.forName(className, classLoader));
}
else {
//没有classloader,先存个className
bd.setBeanClassName(className);
}
}
return bd;
}

3.11.2 标签的属性注入

看完了create,我们再来看parseBeanDefinitionAttributes(ele, beanName, containingBean, bd);方法,该方法会将bean标签的属性值注入到BeanDefinition实例中,也就是给BeanDefinition赋值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
/**
* Apply the attributes of the given bean element to the given bean * definition.
* 将bean标签上的属性设置到bean definition中
* @param ele bean declaration element
* @param beanName bean name
* @param containingBean containing bean definition
* @return a bean definition initialized according to the bean element attributes
*/
public AbstractBeanDefinition parseBeanDefinitionAttributes(Element ele, String beanName,
@Nullable BeanDefinition containingBean, AbstractBeanDefinition bd) {
//bean标签上已经没有singleton属性了,新版用scope代替,所以出现singleton就报错提醒
if (ele.hasAttribute(SINGLETON_ATTRIBUTE)) {
error("Old 1.x 'singleton' attribute in use - upgrade to 'scope' declaration", ele);
}
//如果设置了scope就拿其值
else if (ele.hasAttribute(SCOPE_ATTRIBUTE)) {
bd.setScope(ele.getAttribute(SCOPE_ATTRIBUTE));
}
//此处containingBean为空
else if (containingBean != null) {
// Take default from containing bean in case of an inner bean definition.
// 如果bd是一个内部的beanDefinition,用包含它的bean的配置
bd.setScope(containingBean.getScope());
}
//如果设置了abstract就拿其值
if (ele.hasAttribute(ABSTRACT_ATTRIBUTE)) {
bd.setAbstract(TRUE_VALUE.equals(ele.getAttribute(ABSTRACT_ATTRIBUTE)));
}
//lazyInit如果没有设置则为默认值,默认值用的代理类中defaults属性,
//也就是this.defaults
String lazyInit = ele.getAttribute(LAZY_INIT_ATTRIBUTE);
if (isDefaultValue(lazyInit)) {
lazyInit = this.defaults.getLazyInit();
}
bd.setLazyInit(TRUE_VALUE.equals(lazyInit));
//拿autowire配置,无则用默认值,默认值用的代理类中defaults属性,即不进行autowire
String autowire = ele.getAttribute(AUTOWIRE_ATTRIBUTE);
bd.setAutowireMode(getAutowireMode(autowire));
//拿depends-on配置
if (ele.hasAttribute(DEPENDS_ON_ATTRIBUTE)) {
String dependsOn = ele.getAttribute(DEPENDS_ON_ATTRIBUTE);
bd.setDependsOn(StringUtils.tokenizeToStringArray(dependsOn, MULTI_VALUE_ATTRIBUTE_DELIMITERS));
}
//是否有autowire-candidate属性,没有或者为默认则不设置
String autowireCandidate = ele.getAttribute(AUTOWIRE_CANDIDATE_ATTRIBUTE);
if (isDefaultValue(autowireCandidate)) {
String candidatePattern = this.defaults.getAutowireCandidates();
if (candidatePattern != null) {
String[] patterns = StringUtils.commaDelimitedListToStringArray(candidatePattern);
bd.setAutowireCandidate(PatternMatchUtils.simpleMatch(patterns, beanName));
}
}
else {
bd.setAutowireCandidate(TRUE_VALUE.equals(autowireCandidate));
}
//是否有primary属性
if (ele.hasAttribute(PRIMARY_ATTRIBUTE)) {
bd.setPrimary(TRUE_VALUE.equals(ele.getAttribute(PRIMARY_ATTRIBUTE)));
}
//是否有init-method属性
if (ele.hasAttribute(INIT_METHOD_ATTRIBUTE)) {
String initMethodName = ele.getAttribute(INIT_METHOD_ATTRIBUTE);
bd.setInitMethodName(initMethodName);
}//没有init-method属性,就拿代理类defaults属性的
else if (this.defaults.getInitMethod() != null) {
bd.setInitMethodName(this.defaults.getInitMethod());
bd.setEnforceInitMethod(false);
}
//是否有destroy-method属性
if (ele.hasAttribute(DESTROY_METHOD_ATTRIBUTE)) {
String destroyMethodName = ele.getAttribute(DESTROY_METHOD_ATTRIBUTE);
bd.setDestroyMethodName(destroyMethodName);
}//没有destroy-method属性,就拿代理类defaults属性的
else if (this.defaults.getDestroyMethod() != null) {
bd.setDestroyMethodName(this.defaults.getDestroyMethod());
bd.setEnforceDestroyMethod(false);
}
//是否有factory-method属性
if (ele.hasAttribute(FACTORY_METHOD_ATTRIBUTE)) {
bd.setFactoryMethodName(ele.getAttribute(FACTORY_METHOD_ATTRIBUTE));
}
//是否有factory-bean属性
if (ele.hasAttribute(FACTORY_BEAN_ATTRIBUTE)) {
bd.setFactoryBeanName(ele.getAttribute(FACTORY_BEAN_ATTRIBUTE));
}

return bd;
}

这样,bean标签上的属性也就解析完成了,对其属性的描述不管设置了还是没有设置的,都有相应的值对应到bean definition中。

3.11.3 bean标签下的property注入

1
2
3
4
<bean id="student" class="com.sgcc.bean.Student">
<property name="name" value="无敌"/>
<property name="age" value="20"/>
</bean>

如上述配置,我们知道property的存在,parsePropertyElements()负责解析Property的属性设置到beanDefinition中,此方法在BeanDefinitionParserDelegate类中实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
/**
* Parse property sub-elements of the given bean element.
* 解析bean标签下property子标签
*/
public void parsePropertyElements(Element beanEle, BeanDefinition bd) {
NodeList nl = beanEle.getChildNodes();
//循环查找元素的子元素,也就是bean标签的子元素
for (int i = 0; i < nl.getLength(); i++) {
Node node = nl.item(i);
//标签名为property才能进入,进入这个方法
if (isCandidateElement(node) && nodeNameEquals(node, PROPERTY_ELEMENT)) {
parsePropertyElement((Element) node, bd);
}
}
}

/**
* Parse a property element.
* 解析property元素,核心的属性只有name和value
*/
public void parsePropertyElement(Element ele, BeanDefinition bd) {
//拿到property标签的name属性
String propertyName = ele.getAttribute(NAME_ATTRIBUTE);
if (!StringUtils.hasLength(propertyName)) {
error("Tag 'property' must have a 'name' attribute", ele);
return;
}
//解析的时候放入,解析完成弹出,这里放入property标签,
//注意,此时这里还存有bean父标签,子标签解析完成后先弹出
this.parseState.push(new PropertyEntry(propertyName));
try {
//bean标签下可以有多个property,但是不能重复name属性,有重复报错
if (bd.getPropertyValues().contains(propertyName)) {
error("Multiple 'property' definitions for property '" + propertyName + "'", ele);
return;
}
//核心方法,解析property标签属性
Object val = parsePropertyValue(ele, bd, propertyName);
//将name属性和对应的value封装为PropertyValue对象
PropertyValue pv = new PropertyValue(propertyName, val);
//解析property标签的子标签meta,
//拿到meta的key和value属性,设置到PropertyValue中
parseMetaElements(ele, pv);
//这里没有实现,为null
pv.setSource(extractSource(ele));
//将PropertyValue添加到bean definition中
bd.getPropertyValues().addPropertyValue(pv);
}
finally {
//解析的时候放入,解析完成弹出,这里弹出的是property标签
this.parseState.pop();
}
}



/**
* Get the value of a property element. May be a list etc.
* Also used for constructor arguments, "propertyName" being null in this case.
* 拿到property标签的value值,可能是list
* 也可能被constructor标签使用,这种情况propertyName为null
*/
@Nullable
public Object parsePropertyValue(Element ele, BeanDefinition bd, @Nullable String propertyName) {
//如果propertyName为null,则ele是constructor-arg标签
//否则为property标签
String elementName = (propertyName != null ?
"<property> element for property '" + propertyName + "'" :
"<constructor-arg> element");

// Should only have one child element: ref, value, list, etc.
//不管是哪种标签,下面都应该只有一个子标签: ref, value, list等.
NodeList nl = ele.getChildNodes();
Element subElement = null;
for (int i = 0; i < nl.getLength(); i++) {
Node node = nl.item(i);
if (node instanceof Element && !nodeNameEquals(node, DESCRIPTION_ELEMENT) &&
!nodeNameEquals(node, META_ELEMENT)) {
// Child element is what we're looking for.
//除开description和meta标签,子标签最多只能有一个
//如果该判断为真,说明不止一个除开description和meta的标签
if (subElement != null) {
error(elementName + " must not contain more than one sub-element", ele);
}
else {
//得到除开description和meta之外的子标签
//比如下面这种配置,那么<value>helloworld</value>就是子元素subElement。
//<property name="Nnnn">
// <value>helloworld</value>
//</property>
subElement = (Element) node;
}
}
}
//看标签属性用的是value还是ref
boolean hasRefAttribute = ele.hasAttribute(REF_ATTRIBUTE);
boolean hasValueAttribute = ele.hasAttribute(VALUE_ATTRIBUTE);
//value和ref属性不能同时存在,如果有子标签,则value和ref都不能存在,否则报错
if ((hasRefAttribute && hasValueAttribute) ||
((hasRefAttribute || hasValueAttribute) && subElement != null)) {
error(elementName +
" is only allowed to contain either 'ref' attribute OR 'value' attribute OR sub-element", ele);
}
//用的ref的情况,如<property name="name" ref="......"/>
if (hasRefAttribute) {
//拿到ref属性
String refName = ele.getAttribute(REF_ATTRIBUTE);
if (!StringUtils.hasText(refName)) {
error(elementName + " contains empty 'ref' attribute", ele);
}
//通过ref属性来构建一个RuntimeBeanReference实例对象
RuntimeBeanReference ref = new RuntimeBeanReference(refName);
ref.setSource(extractSource(ele));
return ref;
}
//用的value的情况,如<property name="name" value="..."/>
else if (hasValueAttribute) {
TypedStringValue valueHolder = new TypedStringValue(ele.getAttribute(VALUE_ATTRIBUTE));
valueHolder.setSource(extractSource(ele));
return valueHolder;
}
//子标签不为null的情况,比如
//<property name="Nnnn">
// <value>helloworld</value>
//</property>
else if (subElement != null) {
return parsePropertySubElement(subElement, bd);
}
else {
// Neither child element nor "ref" or "value" attribute found.
//没指定ref或者value或者子标签,返回null
error(elementName + " must specify a ref or value", ele);
return null;
}
}



/**
* Parse a value, ref or collection sub-element of a property or
* constructor-arg element.
* 解析property或者constructor-arg标签的子标签,可能为value, ref或者集合
* @param ele subelement of property element; we don't know which yet
* @param bd the current bean definition (if any)
*/
@Nullable
public Object parsePropertySubElement(Element ele, @Nullable BeanDefinition bd) {
return parsePropertySubElement(ele, bd, null);
}


/**
* Parse a value, ref or collection sub-element of a property or
* constructor-arg element.
* 解析property或者constructor-arg标签的子标签,可能为value, ref或者集合
* @param ele subelement of property element; we don't know which yet
* @param bd the current bean definition (if any)
* @param defaultValueType the default type (class name) for any
* {@code <value>} tag that might be created
*/
@Nullable
public Object parsePropertySubElement(Element ele, @Nullable BeanDefinition bd, @Nullable String defaultValueType) {
if (!isDefaultNamespace(ele)) {//如果这个子标签不属于beans的名称空间,则走这个方法
return parseNestedCustomElement(ele, bd);
}//如果是bean子标签,则走这个方法
else if (nodeNameEquals(ele, BEAN_ELEMENT)) {
BeanDefinitionHolder nestedBd = parseBeanDefinitionElement(ele, bd);
if (nestedBd != null) {
nestedBd = decorateBeanDefinitionIfRequired(ele, nestedBd, bd);
}
return nestedBd;
}//如果是ref子标签,则走这个方法
else if (nodeNameEquals(ele, REF_ELEMENT)) {
// A generic reference to any name of any bean.
String refName = ele.getAttribute(BEAN_REF_ATTRIBUTE);
boolean toParent = false;
if (!StringUtils.hasLength(refName)) {
// A reference to the id of another bean in a parent context.
refName = ele.getAttribute(PARENT_REF_ATTRIBUTE);
toParent = true;
if (!StringUtils.hasLength(refName)) {
error("'bean' or 'parent' is required for <ref> element", ele);
return null;
}
}
if (!StringUtils.hasText(refName)) {
error("<ref> element contains empty target attribute", ele);
return null;
}
RuntimeBeanReference ref = new RuntimeBeanReference(refName, toParent);
ref.setSource(extractSource(ele));
return ref;
}//如果是idref子标签,则走这个方法
else if (nodeNameEquals(ele, IDREF_ELEMENT)) {
return parseIdRefElement(ele);
}//如果是value子标签,则走这个方法
else if (nodeNameEquals(ele, VALUE_ELEMENT)) {
//以这个方法作为演示,其他的方法都是大同小异,进入。
return parseValueElement(ele, defaultValueType);
}//如果是null子标签,则走这个方法
else if (nodeNameEquals(ele, NULL_ELEMENT)) {
// It's a distinguished null value. Let's wrap it in a TypedStringValue
// object in order to preserve the source location.
TypedStringValue nullHolder = new TypedStringValue(null);
nullHolder.setSource(extractSource(ele));
return nullHolder;
}//如果是array子标签,则走这个方法
else if (nodeNameEquals(ele, ARRAY_ELEMENT)) {
return parseArrayElement(ele, bd);
}//如果是list子标签,则走这个方法
else if (nodeNameEquals(ele, LIST_ELEMENT)) {
return parseListElement(ele, bd);
}//如果是set子标签,则走这个方法
else if (nodeNameEquals(ele, SET_ELEMENT)) {
return parseSetElement(ele, bd);
}//如果是map子标签,则走这个方法
else if (nodeNameEquals(ele, MAP_ELEMENT)) {
return parseMapElement(ele, bd);
}//如果是props子标签,则走这个方法
else if (nodeNameEquals(ele, PROPS_ELEMENT)) {
return parsePropsElement(ele);
}//否则返回null,报错
else {
error("Unknown property sub-element: [" + ele.getNodeName() + "]", ele);
return null;
}
}



/**
* Return a typed String value Object for the given value element.
* 通过指定的value标签,返回指定的字符串value对象
*/
public Object parseValueElement(Element ele, @Nullable String defaultTypeName) {
// It's a literal value.
//拿到value中的文本,包括回车、tab制表符、空格
String value = DomUtils.getTextValue(ele);
//有无type属性
String specifiedTypeName = ele.getAttribute(TYPE_ATTRIBUTE);
String typeName = specifiedTypeName;
if (!StringUtils.hasText(typeName)) {
//没有就用入参defaultTypeName,其实这里defaultTypeName也是null
typeName = defaultTypeName;
}
try {
//构建一个value的封装类。
TypedStringValue typedValue = buildTypedStringValue(value, typeName);
//这里设置为空
typedValue.setSource(extractSource(ele));
//这里为空字符串
typedValue.setSpecifiedTypeName(specifiedTypeName);
//返回typedValue
return typedValue;
}
catch (ClassNotFoundException ex) {
error("Type class [" + typeName + "] not found for <value> element", ele, ex);
return value;
}
}

3.12 注册BeanDefinition到工厂中

好,回到processBeanDefinition方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* Process the given bean element, parsing the bean definition
* and registering it with the registry.
* 处理bean元素,解析成bean definition并注册到工厂中
*/
protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {
//使用delegate的内容(默认配置)来解析bean元素,核心方法
BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);
if (bdHolder != null) {
//如果有要求的话渲染beanDefinition,不是很重要
bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);
try {
// Register the final decorated instance.
//注册最终被渲染的实例到工厂中
BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());
}
catch (BeanDefinitionStoreException ex) {
getReaderContext().error("Failed to register bean definition with name '" +
bdHolder.getBeanName() + "'", ele, ex);
}
// Send registration event.
// 发送注册事件
// 这里是空实现
getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder));
}
}

BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);这一句我们已经在3.11章节中详细介绍了,接下来我们视角继续往下,看下BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* Register the given bean definition with the given bean factory.
* @param definitionHolder the bean definition including name and aliases
* @param registry the bean factory to register with
* @throws BeanDefinitionStoreException if registration failed
*/
public static void registerBeanDefinition(
BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry)
throws BeanDefinitionStoreException {

// Register bean definition under primary name.
// 注册beanDefinition的beanName
// 比如tk.mybatis.spring.mapper.MapperScannerConfigurer#0
String beanName = definitionHolder.getBeanName();
// 核心方法,比较重要,待会儿详解
registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());

// Register aliases for bean name, if any.
// 如果有别名的话,为bean name注册别名
String[] aliases = definitionHolder.getAliases();
if (aliases != null) {
for (String alias : aliases) {
registry.registerAlias(beanName, alias);
}
}
}

主流程由此进入了registry.registerBeanDefinition中,其中registry实例是DefaultListableBeanFactory的实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
@Override
public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
throws BeanDefinitionStoreException {

Assert.hasText(beanName, "Bean name must not be empty");
Assert.notNull(beanDefinition, "BeanDefinition must not be null");

if (beanDefinition instanceof AbstractBeanDefinition) {
try {
//做一个验证,静态工厂方法和覆盖方法不能组合使用
//如果bean definition中的beanClass属性不是String类型而是Class类型
//那么就要验证和准备这个bean定义的覆盖方法,检查指定名称的方法是否存在
((AbstractBeanDefinition) beanDefinition).validate();
}
catch (BeanDefinitionValidationException ex) {
throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName,
"Validation of bean definition failed", ex);
}
}
//查看beanName是否已经被注册在工厂的beanDefinitionMap属性中
BeanDefinition existingDefinition = this.beanDefinitionMap.get(beanName);
//已经被注册的情况走这个方法,覆盖或者抛异常
if (existingDefinition != null) {
if (!isAllowBeanDefinitionOverriding()) {
throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName,
"Cannot register bean definition [" + beanDefinition + "] for bean '" + beanName +
"': There is already [" + existingDefinition + "] bound.");
}
else if (existingDefinition.getRole() < beanDefinition.getRole()) {
// e.g. was ROLE_APPLICATION, now overriding with ROLE_SUPPORT or ROLE_INFRASTRUCTURE
if (logger.isWarnEnabled()) {
logger.warn("Overriding user-defined bean definition for bean '" + beanName +
"' with a framework-generated bean definition: replacing [" +
existingDefinition + "] with [" + beanDefinition + "]");
}
}
else if (!beanDefinition.equals(existingDefinition)) {
if (logger.isInfoEnabled()) {
logger.info("Overriding bean definition for bean '" + beanName +
"' with a different definition: replacing [" + existingDefinition +
"] with [" + beanDefinition + "]");
}
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Overriding bean definition for bean '" + beanName +
"' with an equivalent definition: replacing [" + existingDefinition +
"] with [" + beanDefinition + "]");
}
}
this.beanDefinitionMap.put(beanName, beanDefinition);
}
else {
//这场景表示beanName还没有被注册
//然后根据阶段不同又有一层判断
if (hasBeanCreationStarted()) {
// Cannot modify startup-time collection elements anymore (for stable iteration)
//这个阶段是bean已经开始创建,启动阶段不会进入这里
//如果在非启动阶段注册beanDefinition,那么要加锁后才能操作beanDefinitionMap、beanDefinitionNames
//和manualSingletonNames
synchronized (this.beanDefinitionMap) {
this.beanDefinitionMap.put(beanName, beanDefinition);
List<String> updatedDefinitions = new ArrayList<>(this.beanDefinitionNames.size() + 1);
updatedDefinitions.addAll(this.beanDefinitionNames);
updatedDefinitions.add(beanName);
this.beanDefinitionNames = updatedDefinitions;
if (this.manualSingletonNames.contains(beanName)) {
Set<String> updatedSingletons = new LinkedHashSet<>(this.manualSingletonNames);
updatedSingletons.remove(beanName);
this.manualSingletonNames = updatedSingletons;
}
}
}
else {
// 仍然处于启动时的注册阶段
// 所以这里走这个方法
// beanDefinitionMap是工厂的一个属性,ConcurrentHashMap类型
//他保存所有解析好的bean Definition的名称和实例的映射
// Still in startup registration phase
this.beanDefinitionMap.put(beanName, beanDefinition);
//beanName也单独使用了一个ArrayList来保存,方便遍历
this.beanDefinitionNames.add(beanName);
//如果该beanDefinition是手动注册的,还要从manualSingletonNames中移除beanDefinition的beanName
//manualSingletonNames是LinkedHashSet
this.manualSingletonNames.remove(beanName);
}
this.frozenBeanDefinitionNames = null;
}

if (existingDefinition != null || containsSingleton(beanName)) {
resetBeanDefinition(beanName);
}
else if (isConfigurationFrozen()) {
clearByTypeCache();
}
}

再看别名的注册:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@Override
public void registerAlias(String name, String alias) {
Assert.hasText(name, "'name' must not be empty");
Assert.hasText(alias, "'alias' must not be empty");
synchronized (this.aliasMap) {
if (alias.equals(name)) {
//移除别名中的beanName
//aliasMap是ConcurrentHashMap类型,保存别名和beanName的映射
this.aliasMap.remove(alias);
if (logger.isDebugEnabled()) {
logger.debug("Alias definition '" + alias + "' ignored since it points to same name");
}
}
else {
String registeredName = this.aliasMap.get(alias);
//如果别名对应beanName已经被注册,则不需要再注册一次
//别名不允许被覆盖
if (registeredName != null) {
if (registeredName.equals(name)) {
// An existing alias - no need to re-register
return;
}
if (!allowAliasOverriding()) {
throw new IllegalStateException("Cannot define alias '" + alias + "' for name '" +
name + "': It is already registered for name '" + registeredName + "'.");
}
if (logger.isInfoEnabled()) {
logger.info("Overriding alias '" + alias + "' definition for registered name '" +
registeredName + "' with new target name '" + name + "'");
}
}
//再检查一遍,aliasMap中不能已经存在name和alias
checkForAliasCircle(name, alias);
//工厂的aliasMap属性保存别名,那么alias已被注册
this.aliasMap.put(alias, name);
if (logger.isDebugEnabled()) {
logger.debug("Alias definition '" + alias + "' registered for name '" + name + "'");
}
}
}
}

3.13 加载xml文件beanDefinition流程图

4 加载注解配置的beanDefinition

Spring Resource资源文件体系

Posted on 2020-06-29 | In 框架 , Spring |
Words count in article: 1.8k | Reading time ≈ 7

1. Resource接口

Spring对于资源加载有着一套自己的框架——Resource,Resource继承自InputStream。
下面的是Resource的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public interface Resource extends InputStreamSource {
boolean exists();//判断资源是否存在
default boolean isReadable() {
return true;
}
//判断资源是否打开
default boolean isOpen() {
return false;
}
//判断资源是否是一个文件
default boolean isFile() {
return false;
}
//获取资源文件的URL
URL getURL() throws IOException;
//获取资源文件的URI
URI getURI() throws IOException;
//获取资源文件的File对象
File getFile() throws IOException;
///这个方法接口中有默认实现,返回的是ReadableByteChannel,这个类属于Java的NIO中的管道。
default ReadableByteChannel readableChannel() throws IOException {
return Channels.newChannel(getInputStream());
}
//获取内容的长度,这个方法返回一个long,因为文件内容可能很长。
long contentLength() throws IOException;
//这个方法返回的是最后修改时间,虽然也返回的是long,但是这个数字是一个时间戳。
long lastModified() throws IOException;
//这个方法根据relativePath相对路径返回一个相对与该Resource的Resource。
Resource createRelative(String relativePath) throws IOException;
@Nullable
//获取文件的名字。
String getFilename();
//获取一个对该资源的一个描述。
String getDescription();

}

2. Resource实现类

你可以理解为,Resource就是一个增强版的InputStreamSource,Resource 接口是Spring资源访问策略的抽象,它本身并不提供任何资源访问实现,具体的资源访问由该接口的实现类完成——每个实现类代表一种资源访问策略(策略模式)。

  • UrlResource:
    • UrlResource封装了java.net.URL,可用于访问通常可通过url访问的任何对象,如文件、HTTP目标、FTP目标和其他对象。所有URL可以使用一个标准化前缀来表示一个URL类型。例如: file:用于访问文件系统路径。 http:用于通过HTTP协议访问资源。 ftp:用于通过FTP访问资源。
  • ClassPathResource:
    • 表示从类路径加载资源。如果资源路径带上前缀ClassPath:,那么会隐式的解析为ClassPathResource。注意,如果类资源文件是在文件系统中,则该资源实现会被解析为java.io.File, 如果是在Jar包中,则会使用java.net.URL来解析。
  • FileSystemResource:
    • 他是java.io.File和java.nio.file.Path的Resource实现,支持解析为File或者URL。如D:/aaa/vvv.java
  • ServletContextResource:
    • 这是ServletContext的Resource实现,用于解释相关Web应用程序根目录中的相对路径。访问Web容器上下文中的资源而设计的类,负责对于Web应用根目录的路径加载资源。它支持以流和URL的方式访问,在WAR解包的情况下,也可以通过File方式访问。该类还可以直接从JAR包中访问资源。
  • InputStreamResource:
    • InputStreamResource 是InputStream 的Resource实现。只有在其他Resource实现不可用的时候才考虑使用它。和其他的Resource实现相反,它是一个already-opened resource的描述器,所以isOpen()会返回true。 如果你想保存资源描述器或者多次读取一个stream, 那么不要使用它。
  • ByteArrayResource:
    • 是byte array的Resource实现, 它创建了ByteArrayInputStream。它对于从任何给定的字节数组加载内容都很有用,而不必求助于单次使用的InputStreamResource。
  • PathResource:
    • Spring4.0提供的读取资源文件的新类。Path封装了java.net.URL、java.nio.Path、文件系统资源,它使用户能够访问任何可以通过URL、Path、系统文件路径表示的资源,如文件系统的资源,HTTP资源、FTP资源等。

3. 使用示例

使用示例如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Test
public void testResource() throws IOException {
String filePath = "E:\\源码\\Spring源码阅读\\testSpring\\src\\test\\resources\\spring.txt";

// 1. 使用系统的文件路径方式加载文件
WritableResource resource1 = new PathResource(filePath);

// 2. 使用类路径方式加载文件
Resource resource2 = new ClassPathResource("spring.txt");

// 3. 使用WritableResource接口写资源文件
OutputStream os = resource1.getOutputStream();
os.write("Spring是一套非常优秀的框架".getBytes());

// 4. 使用Resource接口读文件
InputStream in1 = resource1.getInputStream();
InputStream in2 = resource1.getInputStream();
BufferedInputStream bis = new BufferedInputStream(in1);
byte[] bytes = new byte[1024];
bis.read(bytes);
System.out.println(new String(bytes));
System.out.println("resource1: " + resource1.getFilename());
System.out.println("resource2: " + resource2.getFilename());
}

4. 资源路径通配符。

Resource解析各种资源路径,依靠资源路径通配符可以带来很多方便。

4.1 Ant-style Patterns

定义资源路径可以是用Ant风格的通配符,下面是 Ant-style patterns 的路径例子:

1
2
3
4
/WEB-INF/*-context.xml
com/mycompany/**/applicationContext.xml
file:C:/some/path/*-cont?xt.xml
classpath:com/mycompany/**/applicationContext.xml

Ant风格的资源地址支持三种通配符:

  • ?:匹配文件名中的一个字符
  • *:匹配文件名中的多个字符
  • **:匹配多层路径。

4.2 classpath*:前缀

构造基于XML的ApplicationContext,路径地址可以使用classpath*: 前缀,如下:

1
2
ApplicationContext ctx =
new ClassPathXmlApplicationContext("classpath*:conf/appContext.xml");

或者

1
2
3
4
5
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:applicationContext.xmlclasspath*:applicationContext-shiro.xml
</param-value>
</context-param>

classpath* 和 classpath 的区别是:classpath* 会去查找所有匹配的classpath, 而classpath 只会找到第一个匹配的资源。

5. ResourceLoader

不过有个问题随之而来,那就是Resource的选择,这么多的Resource如何知道选择使用哪一个?Spring提供了一个强大的资源加载机制,他可以通过前缀标识加载资源,如:classpath:, file:,ftp:等,同时还支持使用Ant风格的通配符。

ResourceLoader用来返回Resource实例,下面是其定义:

1
2
3
public interface ResourceLoader {
Resource getResource(String location);
}
前缀 例子 说明
classpath: classpath:com/myapp/config.xml 使用ClassPathResource从classpath中加载。
file: file:/data/config.xml 作为 URL 加载。使用UrlResource从文件系统目录中装载资源
http: http://myserver/logo.png 作为 URL 加载。使用UrlResource从Web服务器中装载资源
ftp: ftp://www.mcwebsite.top/bean.xml 作为 URL 加载。使用UrlResource从ftp服务器中装载资源
(none) /data/config.xml 根据ApplicationContext的具体实现选择对应类型的Resource

上表中最后一种情况,需要说明下:

所有的ApplicationContext都实现了ResourceLoader类。因此所有的ApplicationContext都可以用来获取Resource。

当在特定的应用程序上下文上调用getResource(),并且指定的位置路径没有特定的前缀时,将返回适合该特定应用程序上下文的资源类型。

例如,假设对ClassPathXmlApplicationContext实例执行了以下代码片段:

Resource template = ctx.getResource("some/resource/path/myTemplate.txt");

在ClassPathXmlApplicationContext中,这个方法返回ClassPathResource。

以此类推,在FileSystemXmlApplicationContext中,方法返回FileSystemResource。在WebApplicationContext, 方法返回ServletContextResource。

当然,就像我们表中说的,我们可以强制使用ClassPathResource,而不管ApplicationContext到底是什么。这样做的话,我们需要添加classpath:前缀。如下:

Resource template = ctx.getResource("classpath:some/resource/path/myTemplate.txt");

或

1
2
3
4
5
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:applicationContext.xmlclasspath*:applicationContext-shiro.xml
</param-value>
</context-param>

同样的,你可以强制使用UrlResource通过添加标准的java.net.URL前缀(context-param配置的话同理):

1
2
3
Resource template = ctx.getResource("file:///some/resource/path/myTemplate.txt");

Resource template = ctx.getResource("https://myhost.com/resource/path/myTemplate.txt");

Spring IoC概念分析

Posted on 2020-05-25 | In 框架 , Spring |
Words count in article: 13k | Reading time ≈ 50

1. IoC概念简介

IoC是随着近年来轻量级容器(Lightweight Container)的兴起而逐渐被很多人提起的一个名词,它的全称为Inversion of Control,中文通常翻译为“控制反转”。好莱坞原则“Don’t call us, we will call you.”恰如其分地表达了“反转”的意味,是用来形容IoC最多的一句话。

它不是什么技术,而是一种设计思想,就是将原本在程序中手动创建对象的控制权,交由Spring框架来管理。

  • 正控:若要使用某个对象,需要自己去负责对象的创建
  • 反控:若要使用某个对象,只需要从Spring容器中获取需要使用的对象,不关心对象的创建过程,也就是把创建对象的控制权反转给了Spring框架

2. 依赖注入

依赖注入(Dependency Injection,简称DI),2004年,Martin Fowler探讨了一个问题,既然IOC是控制反转,那么到底是“哪些方面的控制被反转了呢?”,经过详细地分析和论证后,他得出了答案:“获得依赖对象的过程被反转了”。控制被反转之后,获得依赖对象的过程由自身管理变为了由IoC容器主动注入。

于是,他给“控制反转”取了一个更合适的名字叫做“依赖注入(Dependency Injection)”。他的这个答案,实际上给出了实现IoC的方法:注入——所谓依赖注入,就是由IoC容器在运行期间,动态地将某种依赖关系注入到对象之中。

所以,依赖注入(DI)和控制反转(IoC)是从不同的角度的描述的同一件事情,就是指通过引入IOC容器,利用依赖关系注入的方式,实现对象之间的解耦。

或者说,IoC是一种思想,是一种目标,而DI这时一种手段,一种过程。

2.1 理论上的依赖注入方式

在学术理论上,依赖注入有三种实现方式:

2.1.1 三种注入的方式

当你来到酒吧,想要喝杯啤酒的时候,通常会直接招呼服务生,让他为你送来一杯清凉的啤酒。同样地,作为被注入对象,要想让IoC容器为其提供服务,并将所需要的被依赖对象送过来,也需要通过某种方式通知对方。

这里就牵涉到了三种依赖注入的方式:

  1. 构造方法注入
    • 顾名思义,构造方法注入,就是被注入对象可以通过在其构造方法中声明依赖对象的参数列表,让外部(通常是IoC容器)知道它需要哪些依赖对象。
      1
      2
      3
      4
      public FXNewsProvider(IFXNewsListener newsListner,IFXNewsPersister newsPersister) {
      this.newsListener = newsListner;
      this.newPersistener = newsPersister;
      }
    • IoC Service Provider会检查被注入对象的构造方法,取得它所需要的依赖对象列表,进而为其注入相应的对象。同一个对象是不可能被构造两次的,因此,被注入对象的构造乃至其整个生命周期,应该是由IoC Service Provider来管理的。构造方法注入方式比较直观,对象被构造完成后,即进入就绪状态,可以马上使用。
    • 这就好比你刚进酒吧的门,服务生已经将你喜欢的啤酒摆上了桌面一样。坐下就可马上享受一份清凉与惬意。

  2. setter 方法注入
    • 对于JavaBean对象来说,通常会通过setXXX()和getXXX()方法来访问对应属性。所以,当前对象只要为其依赖对象所对应的属性添加setter方法,就可以通过setter方法将相应的依赖对象设置到被注入对象中。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      public class FXNewsProvider{
      private IFXNewsListener newsListener;

      public IFXNewsListener getNewsListener() {
      return newsListener;
      }
      public void setNewsListener(IFXNewsListener newsListener) {
      this.newsListener = newsListener;
      }
      }
    • 这样,外界就可以通过调用setNewsListener方法为FXNewsProvider对象注入所依赖的对象了。setter方法注入虽不像构造方法注入那样,让对象构造完成后即可使用,但相对来说更宽松一些,可以在对象构造完成后再注入。
    • 这就好比你可以到酒吧坐下后再决定要点什么啤酒,可以要百威,也可以要青岛,随意性比较强。如果你不急着喝,这种方式当然是最适合你的。

  3. 接口注入
    • 首先注意,因为代码侵入性高,所以这种方式Spring框架不支持,只要了解即可。
    • 相对于前两种注入方式来说,接口注入没有那么简单明了。被注入对象如果想要IoC Service Provider为其注入依赖对象,就必须实现某个接口。这个接口提供一个方法,用来为其注入依赖对象。IoC Service Provider最终通过这些接口来了解应该为被注入对象注入什么依赖对象。
    • FXNewsProvider为了让IoC Service Provider为其注入所依赖的IFXNewsListener,首先需要实现IFXNewsListenerCallable接口,这个接口会声明一个injectNewsListner方法(方法名随意),该方法的参数,就是所依赖对象的类型。这样,InjectionServiceContainer对象,即对应的IoC Service Provider就可以通过这个接口方法将依赖对象注入到被注入对象FXNewsProvider当中。
    • 接口注入方式最早并且使用最多的是在一个叫做Avalon的项目中,相对于前两种依赖注入方式,接口注入比较死板和烦琐。如果需要注入依赖对象,被注入对象就必须声明和实现另外的接口。
    • 这就好像你同样在酒吧点啤酒,为了让服务生理解你的意思,你就必须戴上一顶啤酒杯式的帽子

2.1.2 三种注入方式的比较

  1. 接口注入:
    • 从注入方式的使用上来说,接口注入是现在不甚提倡的一种方式,基本处于“退役状态”。因为它强制被注入对象实现不必要的接口,带有侵入性。而构造方法注入和setter方法注入则不需要如此。
  2. 构造方法注入:
    • 这种注入方式的优点就是,对象在构造完成之后,即已进入就绪状态,可以马上使用。
    • 缺点就是,当依赖对象比较多的时候,构造方法的参数列表会比较长。而通过反射构造对象的时候,对相同类型的参数的处理会比较困难,维护和使用上也比较麻烦。而且在Java中,构造方法无法被继承,无法设置默认值。对于非必须的依赖处理,可能需要引入多个构造方法,而参数数量的变动可能造成维护上的不便。
  3. setter方法注入:
    • 因为方法可以命名,所以setter方法注入在描述性上要比构造方法注入好一些。另外,setter方法可以被继承,允许设置默认值,而且有良好的IDE支持。
    • 缺点当然就是对象无法在构造完成后马上进入就绪状态。

综上所述,构造方法注入和setter方法注入因为其侵入性较弱,且易于理解和使用,所以是现在使用最多的注入方式;而接口注入因为侵入性较强,近年来已经不流行了。

2.2 Spring的依赖注入方式

因为代码侵入性高的问题,接口注入的方式,spring框架并不支持。Spring的依赖注入方式只有构造方法注入和setter方法注入:

  1. 构造方法
    • 开箱即用,适合用于注入实例必须的初始值时使用,但是当参数列表较长时难以维护和使用。构造方法无法被继承,也无法设置默认值。适合较固定的对象使用。
      1
      2
      3
      4
      5
      <bean id="login" class="com.spring.test.di.LoginImpl"/>

      <bean id="loginAction" class="com.spring.test.di.LoginAction">
      <constructorarg index="0" ref="login"></constructor­arg>
      </bean>
  2. setter方法
    • 适合依赖对象多,且组成对象灵活多变的场景,是目前最为常见的注入方法。
      1
      2
      3
      4
      5
      <bean id="login" class="com.spring.test.di.LoginImpl"/>

      <bean id="loginAction" class="com.spring.test.di.LoginAction">
      <property name="login" ref="login"></property>
      </bean>

3. IoC Service Provider

了解了IoC和DI的概念中,我们可以知道,在DI的过程中,IoC Service Provider是一个非常重要的概念——业务对象可以通过IoC方式声明相应的依赖,但是最终仍然需要通过某种角色或者服务将这些相互依赖的对象绑定到一起,IoC Service Provider就是这样一个角色。

IoC Service Provider在这里是一个抽象出来的概念,它可以指代任何将IoC场景中的业务对象绑定到一起的实现方式。它可以是一段代码,也可以是一组相关的类,甚至可以是比较通用的IoC框架或者IoC容器实现。Spring的IoC容器就是一个提供依赖注入服务的IoC Service Provider。

3.1 IoC Service Provider的职责

IoC Service Provider的职责相对来说比较简单,主要有两个:

  1. 业务对象的注册管理。
    • 在IoC场景中,业务对象无需关心所依赖的对象如何构建如何取得,但这部分工作始终需要有人来做。所以,IoC Service Provider需要识别这部分需要管理的对象,并且将这些对象的构建逻辑从客户端对象那里剥离出来,以免这部分逻辑污染业务对象的实现。
  2. 业务对象间的依赖绑定。
    • IoC Service Provider通过结合之前构建和管理的所有业务对象,以及各个业务对象间可以识别的依赖关系,将这些对象所依赖的对象注入绑定,从而保证每个业务对象在使用的时候,可以处于就绪状态。

3.2 常见IoC Service Provider依赖注册方式

那么,对于IoC Service Provider来说,如何知道哪些对象是被其他对象依赖(即需要它管理起来的)的呢?又是如何知道某个管理的对象,具体要注入到哪一个具体的其他对象中呢?就像一个资深的酒吧服务员,客人点了哪些酒,且每一杯酒分别是被哪个客人点的,他都要了然于心,这时如何做到的呢?

很显然,我们需要记录下来这些的“服务信息”(在Spring的术语中,把BeanFactory需要使用的对象注册和依赖绑定信息称为Configuration Metadata),当前流行的IoC Service Provider产品使用的Configuration Metadata的方式主要有以下几种:

  1. 直接编码方式
    • 当前大部分的IoC容器都应该支持直接编码方式,比如PicoContainer、Spring、Avalon等。在容器启动之前,我们就可以通过程序编码的方式将被注入对象和依赖对象注册到容器中,并明确它们相互之间的依赖注入关系。
  2. 配置文件方式
    • 这是一种较为普遍的依赖注入关系管理方式。像普通文本文件、properties文件、XML文件等,都可以成为管理依赖注入关系的载体。不过,最为常见的,还是通过XML文件来管理对象注册和对象间依赖关系,比如Spring IoC容器和在PicoContainer基础上扩展的NanoContainer,都是采用XML文件来管理和保存依赖注入信息的。
  3. 元数据方式(注解)
    • 这种方式的代表实现是Google Guice,这是Bob Lee在Java 5的注解和Generic的基础上开发的一套IoC框架。我们可以直接在类中使用元数据信息来标注各个对象之间的依赖关系,然后由Guice框架根据这些注解所提供的信息将这些对象组装后,交给客户端对象使用。

3.3 Spring IoC Service Provider依赖注册方式

Spring IoC Service Provider的注册依赖方式同样是三种,也就是说,其他IoC Service Provider支持的主流的三种依赖注册方式,Spring都支持。

  1. 直接编码方式
    • 使用@Configuration注解可以将java的类文件声明成spring的配置类,使用@Bean来声明方法的返回对象要注册为spring的bean对象。
    • 而当bean中需要注入其他参数或者引用时,将其作为方法的参数即可,Spring会帮你注入这些引用。
    • 默认情况下,方法名即为id名,当然也可以为bean指定名称,通过其@Bean注解的name属性。
    • 同时@Bean注解的initMethod属性和destroyMethod属性,可以指定初始化和销毁时的生命周期回调函数。
    • 而@Scope和@Description注解,则可以给bean设置Scope和Description
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      //使用@Configuration注解可以将java的类文件声明成spring的配置类
      @Configuration
      public class SpringConfig {

      @Bean //你可以理解为定义一个String类型的bean,值是"test",做依赖注入用。
      public String username(){
      return "test";
      }

      @Bean
      public List<String> tags(){
      List<String> tags = new ArrayList<String>();
      tags.add("cool");
      tags.add("nice");
      return tags;
      }

      //使用@Bean 注解表明myBean需要交给Spring进行管理
      //如果未指定bean的id,默认采用的是 "方法名" + "首字母小写"的配置方式
      //name属性可以定义bean的id ; initMethod和destroyMethod属性指定初始化和销毁时的生命周期回调函数。
      @Bean(name = "userInterfaceIml" , initMethod = "init" , destroyMethod = "cleanup")
      @Scope("prototype") //指定该bean的scope
      @Description("Provides a basic example of a bean") //指定该bean的description
      public UserInterface userInterface(){
      return new UserInterfaceImpl();
      }

      @Bean
      //通过参数列表,将bean的依赖注入
      public UserCall userCall(UserInterface userInterface, String username, List<String> tags){
      UserCall uc = new UserCall();
      uc.setUi(userInterface);
      uc.setUsername(username);
      uc.setTags(tags);
      return uc;
      }
      }
      }
  2. 配置文件方式
    • Spring使用XML文件来管理和保存依赖注入信息,配置组件bean的话只需要使用<bean>标签即可。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      <bean id="userInterface" class="com.springbean.impl.UserInterfaceImpl" />

      //使用构造器注入。使用构造器注入的时候必须在类中存在对应的构造方法才能有效
      <util:list id="tagsList">
      <value>cool</value>
      <value>nice</value>
      </util:list>

      <bean id="userCall" class="com.springbean.UserCall">
      <constructor-arg name="ui" ref="userInterface"/>
      <constructor-arg name="username" value="test"/>
      <constructor-arg name="tags" ref="tagsList"></constructor-arg>
      </bean>

      //使用属性注入
      <bean id="userCall" class="com.springbean.UserCall">
      <property name="ui" ref="userInterface" />
      <property name="username" value="test" />
      <property name="tags" value="tagsList" />
      </bean>
  3. 元数据方式(注解)
    • spring支持通过注解方式管理依赖,但是需要指定spring扫描注解的包,指定扫描的包有两种方式
      1. 可以在Spring的xml文件中配置(前提是引入了Spring context的命名空间),使用<context:component-scan base-package="com.springbean.*"/>
      2. 注解@ComponentScan指定了spring将扫描这个配置类所在的包及其子包下面的所有类。
        1
        2
        @ComponentScan
        public class SpringConfig {}
    • 有了组件扫描后,所有被注解@Component或者它衍生的注解标记类都将被识别为组件类,他们完善了spring通过注解来注册依赖的功能:
      • @Component: 自动被comonent扫描。 表示被注解的类会自动被component扫描
      • @Repository: 用于持久层,主要是数据库存储库。
      • @Service: 表示被注解的类是位于业务层的业务component。
      • @Controller:表明被注解的类是控制component,主要用于展现层 。
    • 除此之外,spring使用注解@Autowired等完成依赖装配:
      • @Autowired:支持按类型自动转配
      • @Qualifier:根据byName的方式自动装配,其中@Qualifier不能单独使用。
        1
        2
        3
        4
        5
        public class User {
        @Autowired
        @Qualifier(value="carXXX")
        private Cat cat;
        }
      • @Resource(这个注解属于J2EE的):如果同时指定了name和type,则从Spring上下文中找到唯一匹配的bean进行装配,找不到则抛出异常;如果指定了name,则从上下文中查找名称(id)匹配的bean进行装配,找不到则抛出异常;如果指定了type,则从上下文中找到类型匹配的唯一bean进行装配,找不到或者找到多个,都会抛出异常;如果既没有指定name,又没有指定type,则自动按照byName方式(字段名)进行装配;如果没有匹配,则回退为一个原始类型进行匹配,如果匹配则自动装配;
        1
        2
        3
        4
        5
        6
        public class User {
        @Resource
        private Cat cat;
        @Resource(name="dogXXX")
        private Dog dog;
        }

4. Spring的IoC容器

上文中,我们从浅到深,从思想到概念,了解了DI过程中的一个重要的角色——IoC Service Provider。

IoC Service Provider只是一个概念,不同的框架,对IoC Service Provider的具体的实现也是五花八门,接下来我们了解一个完成度高,重要性高且知名度极高的IoC Service Provider实现产品——Spring IoC容器。

Spring的IoC容器是一个IoC Service Provider,但不止是一个IoC Service Provider,作为轻量级容器,Spring的IoC容器还提供了IoC之外的支持。如在Spring的IoC容器之上,Spring还提供了相应的AOP框架支持、企业级服务集成等服务。Spring的IoC容器和IoC Service Provider所提供的服务之间存在一定的交集,二者的关系如图

4.1 Spring IoC容器类型

Spring提供了两种容器类型:BeanFactory和ApplicationContext。

  1. BeanFactory。
    • 基础类型IoC容器,提供完整的IoC服务支持。如果没有特殊指定,默认采用延迟初始化策略(lazy-load)。只有当客户端对象需要访问容器中的某个受管对象的时候,才对该受管对象进行初始化以及依赖注入操作。所以,相对来说,容器启动初期速度较快,所需要的资源有限。对于资源有限,并且功能要求不是很严格的场景,BeanFactory是比较合适的IoC容器选择。
  2. ApplicationContext。
    • ApplicationContext在BeanFactory的基础上构建,是相对比较高级的容器实现,除了拥有BeanFactory的所有支持,ApplicationContext还提供了其他高级特性,比如事件发布、国际化信息支持等,这些会在后面详述。ApplicationContext所管理的对象,在该类型容器启动之后,默认全部初始化并绑定完成。所以,相对于BeanFactory来说,ApplicationContext要求更多的系统资源,同时,因为在启动时就完成所有初始化,容器启动时间较之BeanFactory也会长一些。在那些系统资源充足,并且要求更多功能的场景中,ApplicationContext类型的容器是比较合适的选择。

ApplicationContext包含BeanFactory的所有功能,几乎所有的应用系统都选择ApplicationContext而不是BeanFactory。只有在资源很少的情况下,才会考虑采用BeanFactory,如在移动设备上等。

通过下图,我们可以对BeanFactory和ApplicationContext之间的关系有一个更清晰的认识:

4.1.1 BeanFactory

在没有特殊指明的情况下,以BeanFactory为中心所讲述的内容同样适用于ApplicationContext,这一点需要明确一下,二者有差别的地方会在合适的位置给出解释。

BeanFactory,顾名思义,就是生产Bean的工厂。BeanFactory就像一个汽车生产厂。你从其他汽车零件厂商或者自己的零件生产部门取得汽车零件送入这个汽车生产厂,最后,只需要从生产线的终点取得成品汽车就可以了。至于业务对象如何组装,你不需要关心。

BeanFactory只是个interface,它核心实现,在DefaultListableBeanFactory实现类中。BeanFactory声明了如下的方法:

通过方法名我们也能大概了解每个方法的作用,基本上都是查询相关的方法,例如,取得某个对象的方法(getBean)、查询
某个对象是否存在于容器中的方法(containsBean),或者取得某个bean的状态或者类型的方法等。

这些api使得我们可以非常方便的从容器中获取特定类型的bean。那么,BeanFactory如何知道它需要管理和生成哪些bean呢?被托管的bean又是如何注册的呢?后文我们会就bean的注册/绑定/注入做深入介绍。

4.1.2 ApplicationContext

作为Spring提供的较之BeanFactory更为先进的IoC容器实现,ApplicationContext是BeanFactory的子类,故而ApplicationContext拥有BeanFactory支持的所有功能,但除此之外,还进一步扩展了基本容器的功能,如:更易与Spring AOP集成,容器启动后bean实例的自动初始化、国际化的信息支持、容器内事件发布等;

4.2 spring bean和bean定义

java bean对我们来说十分熟悉,我们把符合下面四点的java对象叫做java bean。

  1. 所有属性为private
  2. 提供默认构造方法
  3. 提供getter和setter
  4. 实现serializable接口

4.2.1 spring中的bean

spring中的bean是基于java bean概念的延伸,但为了更好的实现bean的注册/绑定/注入,spring bean的定义显然不能止步于此,为了更好的管理bean,spring在bean上做了许多拓展,不仅对bean本身的属性做纵向拓展,在横向的种类上,也按照不同的职责划分,定义了许多“专业”的,有特点功能的bean。

注意,特殊的bean也是基于普通bean的拓展,普通bean拥有的特点,特殊bean都有。

4.2.1.1 普通的spring bean

为了应对许多不同的场景,我们在配置spring bean的Configuration Metadata的时候,需要定义bean的许多属性来达到不同的目的,故而我们有必要了解spring为bean定义了哪些属性可用。

  1. id属性

    • 通常,每个注册到容器的对象都需要一个唯一标志来将其与“同处一室”的“兄弟们”区分开来,就好像我们每一个人都有一个身份证号一样(重号的话就比较麻烦)。通过id属性来指定当前注册对象的beanName是什么。
    • <bean id="djNewsListener" class="..impl.DowJonesNewsListener"> </bean>
  2. name属性

    • 除了可以使用id来指定<bean>在容器中的标志,还可以使用name属性来指定<bean>的别名(alias)
    • 与id属性相比,name属性的灵活之处在于,name可以使用id不能使用的一些字符,比如/。而且还可以通过逗号、空格或者冒号分割指定多个name。
    • <bean id="djNewsListener" name="/news/djNewsListener,dowJonesNewsListener" class="..impl.DowJonesNewsListener"> </bean>
  3. class属性

    • 每个注册到容器的对象都需要通过<bean>元素的class属性指定其类型,否则,容器可不知道这个对象到底是何方神圣。
    • 在大部分情况下,该属性是必须的。仅在少数情况下不需要指定,如后面将提到的在使用抽象配置模板的情况下。
    • <bean id="djNewsListener" class="..impl.DowJonesNewsListener"> </bean>
  4. scope属性

    • scope用来声明容器中的对象所应该处的限定场景或者说该对象的存活时间,即容器在对象进入其相应的scope之前,生成并装配这些对象,在该对象不再处于这些scope的限定之后,容器通常会销毁这些对象。
    • Spring容器最初提供了两种bean的scope类型:singleton和prototype,但发布2.0之后,又引入了另外三种scope类型,即request、session和global session类型。不过这三种类型有所限制,只能在Web应用中使用。
    • <bean id="mockObject2" class="...MockBusinessObject" scope="prototype"/>
      1. singleton:单例的意思。即标记为拥有singleton scope的对象定义,在Spring的IoC容器中只存在一个实例,所有对该对象的引用将共享这个实例。该实例从容器启动,并因为第一次被请求而初始化之后,将一直存活到容器退出。
      2. prototype:容器在接到该类型对象的请求的时候,会每次都重新生成一个新的对象实例给请求方。虽然这种类型的对象的实例化以及属性设置等工作都是由容器负责的,但是只要准备完毕,并且对象实例返回给请求方之后,容器就不再拥有当前返回对象的引用,请求方需要自己负责当前返回对象的后继生命周期的管理工作,包括该对象的销毁。
      3. request、session和global session:这三个scope类型是Spirng 2.0之后新增加的,它们不像之前的singleton和prototype那么“通用”,因为它们只适用于Web应用程序,通常是与XmlWebApplicationContext共同使用。三者的作用域顾名思义,分别对应web应用的request、session和global session。

4.2.1.2 FactoryBean

FactoryBean是我们接触到的第一个特殊bean,首先它是一个Bean(这表示spring bean的定义它都有),但又不仅仅是一个Bean(它有特殊功能)。它是一个能生产或修饰对象生成的工厂Bean,类似于设计模式中的工厂模式和装饰器模式。它能在需要的时候“改装”一个对象,且不仅仅限于它自身,它能返回任何Bean的实例。

简而言之,factoryBean是一个bean,一个拥有简单bean工厂职能的bean。

FactoryBean是一个接口,它只定义了三个方法:

1
2
3
4
5
6
7
8
9
public interface FactoryBean<T> {
@Nullable
T getObject() throws Exception;
@Nullable
Class<?> getObjectType();
default boolean isSingleton() {
return true;
}
}

这三个方法最核心的 getObject()方法,其他两个方法都服务于它。

那么FactoryBean有什么作用呢??

我们知道,在spring Ioc容器中getBean的时候,底层是通过java的反射机制调用bean的构造器来new一个对象返回,如果我希望从容器中返回的对象不是新new出来的对象,而是某个我指定的对象呢??

比如我们需要从容器中获取一辆车:

1
2
3
4
5
6
7
8

public class Car{
private String color;
public Car() {
this.color = "黑色";
}
... //set/get方法省略
}

因为反射都是调用无参构造器来new对象,所以我们只能得到一辆黑色的车,那我如果想要一辆白色的车呢?

我们现有的beanFactory只支持生产默认的黑色的车,那为了得到白色的车,我们得拥有指定想要哪台车的能力,如何指定呢?

我们知道,在对象的概念中,A extends B表示的是A是B; A implements B表示的是A有B提供的能力。我们希望我们在提车时可以自己选择自己想要的车,而FactoryBean就提供了这种能力。

Car类实现了FactoryBean,就表示告诉spring IoC:当beanFactory按照Car类的图纸(beanDefinition,下文将详细描述)来生产Car的实例的时候,如果发现我的图纸上有注明要指定我想要的汽车(即实现FactoryBean接口),那么beanFactory就得按照我的要求来生产我制定的汽车。

这时候,我们的“图纸”可以这么定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Component
public class Car implements FactoryBean<Car>{
private String color;
public Car() {
this.color = "黑色";
}
... //set/get方法省略

//这座新工厂,要生产白色的车
@Override
public Car getObject() throws Exception {
Car car=new Car();
car.setColor("白色");
return car;
}

@Override
public Class<?> getObjectType() {
return Car.class;
}
//表示我要的白车,在全局中单例
@Override
public boolean isSingleton() {
return true;
}
}

这时候你从容器中取出来的Car类型的实例,都会是白车了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RunWith(SpringRunner.class)
@SpringBootTest(classes = TestApplication.class)
public class FactoryBeanTest {
@Autowired
private ApplicationContext context;
@Test
public void test() {
Car car1 = (Car) context.getBean("car");
System.out.println("car1 = " + car1.getColor());
如果要获取Car非定制的实例,那么需要在名称前面加上'&'符号。
Car car2 = (Car) context.getBean("&car");
System.out.println("car2 = " + car2.getColor());
System.out.println("car1.equals(car2) = " + car1.equals(car2));
}
}

得到结果:

car1 = 白色
car2 = 黑色
car1.equals(car2) = false

说了这么多,为什么要有FactoryBean这个东西呢,有什么具体的作用吗?

其实FactoryBean在Spring中最为典型的一个应用就是用来创建AOP的代理对象。

我们知道AOP实际上是Spring在运行时创建了一个代理对象,也就是说这个对象,是我们在运行时创建的,而不是一开始就定义好的,这很符合工厂方法模式。更形象地说,AOP代理对象通过Java的反射机制,在运行时指定了一个定制的代理对象,在代理对象的目标方法中根据业务要求织入了相应的方法。这个对象在Spring中就是——ProxyFactoryBean。

所以,FactoryBean为我们实例化Bean提供了一个更为灵活的方式,我们可以通过FactoryBean创建出更为复杂的Bean实例。

4.2.2 BeanDefinition

在Java中,一切皆对象。在JDK中使用java.lang.Class来描述类这个对象。

在Spring中,存在bean这样一个概念,那Spring又是怎么抽象bean这个概念,用什么类来描述bean这个对象呢?Spring使用BeanDefinition来描述bean。

顾名思义,BeanDefinition就是Spring对bean的定义对象,spring从Configuration Metadata中读取bean的配置,包括它的beanName,是否是单例,具体指向哪个类,是否是懒加载,有哪些依赖等等信息,都存在BeanDefinition对象中,BeanDefinition就是beanFactory生产bean的图纸。

将bean定义成BeanDefinition后,spring对bean的操作就可以改为对BeanDefinition进行,比如拿到某个BeanDefinition后,可以根据里面的类名、构造函数、构造函数参数,使用反射进行对象创建。

BeanDefinition实现了AttributeAccessor和BeanMetadataElement接口。在Spring中充斥着大量的各种接口,每种接口都拥有不同的能力,某个类实现了某个接口,也就相应的拥有了某种能力:

  1. AttributeAccessor:顾名思义,这是一个属性访问者,它提供了对外访问属性的能力。
  2. BeanMetadataElement:提供了获取元数据元素的配置源对象的能力。

BeanDefinition的属性和方法如下图所示,大部分方法/属性的作用都能简单从名字区分出来,部分方法的作用,我们下面来简单介绍。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
//用于描述一个具体bean实例
public interface BeanDefinition extends AttributeAccessor, BeanMetadataElement {
//scope值,单例
String SCOPE_SINGLETON = ConfigurableBeanFactory.SCOPE_SINGLETON;

//scope值,非单例
String SCOPE_PROTOTYPE = ConfigurableBeanFactory.SCOPE_PROTOTYPE;

//Bean角色:
//用户
int ROLE_APPLICATION = 0;
//某些复杂的配置
int ROLE_SUPPORT = 1;
//完全内部使用
int ROLE_INFRASTRUCTURE = 2;

//返回此bean定义的父bean定义的名称,如果有的话 <bean parent="xxx">
String getParentName();
void setParentName(String parentName);

//获取bean对象className <bean class="xxx">
String getBeanClassName();
void setBeanClassName(String beanClassName);

//定义创建该Bean对象的工厂类 <bean factory-bean="xxx">
//如果该 Bean 采用工厂方法生成,指定工厂名称。
String getFactoryBeanName();
void setFactoryBeanName(String factoryBeanName);


//定义创建该Bean对象的工厂方法 <bean factory-method="xxx">
String getFactoryMethodName();
void setFactoryMethodName(String factoryMethodName);


//<bean scope="singleton/prototype">
String getScope();
void setScope(String scope);


//懒加载 <bean lazy-init="true/false">
boolean isLazyInit();
void setLazyInit(boolean lazyInit);

//依赖对象 <bean depends-on="xxx">
String[] getDependsOn();
void setDependsOn(String[] dependsOn);


//是否为被自动装配 <bean autowire-candidate="true/false">
boolean isAutowireCandidate();
void setAutowireCandidate(boolean autowireCandidate);

//是否为主候选bean 使用注解:@Primary。
//同一接口的多个实现,如果不指定名字的话,Spring 会优先选择设置 primary 为 true 的 bean
boolean isPrimary();
void setPrimary(boolean primary);


//返回此bean的构造函数参数值。
ConstructorArgumentValues getConstructorArgumentValues();

//获取普通属性集合
MutablePropertyValues getPropertyValues();
//是否为单例
boolean isSingleton();
//是否为原型
boolean isPrototype();
//是否为抽象类
// 如果这个 Bean 是被设置为 abstract,那么不能实例化,
// 常用于作为 父bean 用于继承,其实也很少用......
boolean isAbstract();

//获取这个bean的应用
int getRole();

//返回对bean定义的可读描述。
String getDescription();

//返回该bean定义来自的资源的描述(用于在出现错误时显示上下文)
String getResourceDescription();

BeanDefinition getOriginatingBeanDefinition();
}

BeanDefinition接口有诸多的实现类,不同的实现类,使用的场景也不尽相同:

  1. AbstractBeanDefinition,是BeanDefinition的主要实现类,也是所有bean定义的父类。
  2. RootBeanDefinition,是在XML配置时代,注册bean定义时用的类。
  3. ChildBeanDefinition,是在XML配置时代,注册bean定义时用的类,必须在配置时指定一个父bean定义。
  4. GenericBeanDefinition,在注解配置时代,推荐使用的bean定义类,可以在运行时动态指定一个父bean定义,也可以不指定。
  5. AnnotatedGenericBeanDefinition,在注解配置时代,通过编程方式注册bean定义时用的类,继承了GenericBeanDefinition。
  6. ScannedGenericBeanDefinition,在注解配置时代,通过扫描jar包中.class文件的方式注册bean定义时用的类,继承了GenericBeanDefinition。

4.3 Spring IoC容器流程

Spring的IoC容器所起的作用,就像下图所展示的那样,它会以某种方式加载Configuration Metadata(通常也就是XML格式的配置信息),然后根据这些信息绑定整个系统的对象,最终组装成一个可用的基于轻量级容器的应用系统。

Spring的IoC容器实现以上功能的过程,基本上可以按照类似的流程划分为两个阶段,即容器启动阶段和Bean实例化阶段

4.3.1 容器启动阶段

  1. 容器启动伊始,首先会通过某种途径加载Configuration MetaData。除了代码方式比较直接,在大部分情况下,容器需要依赖某些工具类(BeanDefinitionReader)对加载的Configuration MetaData。

  2. 对Configuration MetaData进行解析和分析,并将分析后的信息编组为相应的BeanDefinition,最后把这些保存了bean定义必要信息的BeanDefinition,注册到相应的BeanDefinitionRegistry

总地来说,该阶段所做的工作可以认为是准备性的,重点更加侧重于对象管理信息的收集。

4.3.2 Bean实例化阶段

经过第一阶段,现在所有的bean定义信息都通过BeanDefinition的方式注册到了BeanDefinitionRegistry中。当某个请求方通过容器的getBean方法明确地请求某个对象,或者因依赖关系容器需要隐式地调用getBean方法时,就会触发第二阶段的活动。

  1. 容器会首先检查所请求的对象之前是否已经实例化和初始化。如果没有,则会根据注册的BeanDefinition所提供的信息实例化被请求对象,并为其注入依赖,然后初始化。如果该对象实现了某些回调接口,也会根据回调接口的要求来装配它。

  2. 当该对象装配完毕之后,容器会立即将其返回请求方使用。

如果说第一阶段只是根据图纸装配生产线的话,那么第二阶段就是使用装配好的生产线来生产具体的产品了

注意,我们上面提高的bean的实例化,注入依赖(或者叫依赖装配),初始化,是三个递进的不同阶段,注意区分。

4.4 spring bean的生命周期

确的了解Spring Bean的生命周期是非常必要的。我们通常使用ApplicationContext作为Spring容器。这里,我们讲的也是 ApplicationContext中Bean的生命周期。而实际上BeanFactory也是差不多的,只不过处理器需要手动注册。

开门见山,我们先直接给出一张总图,然后再分别描述:

可以看到,Bean的完整生命周期经历了各种方法调用,这些方法可以划分为以下几类:

  1. Bean自身的方法:这个包括了Bean本身调用的方法(如构造器,依赖注入的set方法等)和通过配置文件中<bean>的init-method和destroy-method指定的方法。

  2. Bean级生命周期接口方法:这个包括了Aware接口的相关实现类(如BeanNameAware、BeanFactoryAware)以及InitializingBean、DiposableBean这些接口的方法。

  3. 容器级生命周期接口方法:

    1. Bean后处理器接口方法:所有实现了BeanPostProcessor这个接口的实现类,一般称它们为“后处理器”,或者“bean后处理器”。主要作用是对容器中的bean进行后处理,也就是额外的加强。(注意,它的作用对象是它所注册的容器中的所有收管bean)
    2. 工厂后处理器接口方法:所有实现了BeanFactoryPostProcessor这个接口的实现类,一般称它们为“工厂后处理器”,或者“容器后处理器”。主要作用是对IoC容器进行后处理,增强容器功能。(注意,它的作用对象是它所注册的容器的对象)

这些类或接口叫做Hook类/接口,这些接口/类的存在,使得Spring Framework具有非常高的扩展性,使得我们可以在bean的生命周期的关键节点介入,得到一些我们需要的信息,或者做一些对bean的“改装”。

第一类,Bean自身的方法,这个不再赘述,我们从bean的生命周期中各类的调用顺序,来依次介绍二三类的这些接口方法:

4.4.1 Bean级生命周期接口方法

4.4.1.1 InitializingBean/DisposableBean接口方法

InitializingBean和DisposableBean接口十分的简单:

1
2
3
public interface InitializingBean {
void afterPropertiesSet() throws Exception;
}
1
2
3
public interface DisposableBean {
void destroy() throws Exception;
}

功能也十分简单:如果想在bean的 创建/销毁 过程中做一些骚操作的话,就实现这两个接口中对应的接口方法,将骚操作逻辑定义在里面。

Spring在创建/销毁bean的过程中,会判断bean是否实现了这二者的接口方法,如果实现了,就在适当的时机调用它。

具体逻辑见AbstractAutowireCapableBeanFactory#initializeBean()和AbstractAutowireCapableBeanFactory#invokeInitMethods()

4.4.1.2 Aware接口方法

Spring中有很多继承于aware接口的类,如下图,那么这些类到底是做什么用到的呢??

有些时候,我们需要在bean的实例化过程中,获取bean的某些信息来做一些工作,这些信息包括bean的beanName,构造这个bean的ApplicationContext,加载这个bean类的beanClassLoader等等。

假设我们有一个Car类,我们希望在bean的初始化过程中能够有机会获取到bean的beanName,以便我们把beanName赋值给carName,让每辆car的carName和beanName一致,那么我们可以这么做:

1
2
3
4
5
6
7
8
9
public class Car implements BeanNameAware{  //让Car实现BeanNameAware
private String carName;
public void setBeanName(String beanName) {
//ID保存BeanName的值
carName=beanName;
}
...
...
}

这时我们在Configuration MetaData中定义两个Car类

1
2
3
4
5
<bean id="benchi"  class="balabala.Car">
</bean>

<bean id="baoma" class="balabala.Car">
</bean>
1
2
3
4
5
6
7
@Autowired
@Qualifier("benchi")
private Car benchi;

@Autowired
@Qualifier("baoma")
private Car baoma;

那么可以得到结果:benchi.carName=“benchi”,baoma.carName=“baoma”;

aware,翻译过来是知道的,已感知的,意识到的,所以,这些接口从字面意思是能感知到所有Aware前缀值的含义。

实际上,这些接口也确实提供了可实现的方法,在bean的实例化过程中,将各个Aware想要获取的信息通过参数的方式传到实现的方法中来,给开发者一个获取到相关信息值的机会。

如上例的Car implements BeanNameAware,BeanNameAware定义的setBeanName(String beanName)方法,就会在实例化过程中把beanName信息传进方法中来让开发者使用,得意于此,我们才能得到beanName,并将其赋值给carName。

至于其他的Aware实现类,他们获取的信息不同,但逻辑也都是一样的。

  • BeanNameAware接口是为了让自身Bean能够感知到,获取到自身在Spring容器中的id(也就是beanName)属性。

  • 实现了ApplicationContextAware接口的类,能够获取到ApplicationContext

  • 实现了BeanFactoryAware接口的类,能够获取到BeanFactory对象。

  • …

在实例化过程中,将相关信息传进接口方法中以供使用,spring怎么做到这一点的呢??

其实非常简单,bean在初始化前会调用一次ApplicationContextAwareProcessor类的postProcessBeforeInitialization方法,如果bean实现了Aware接口,那么会继续判断bean实现了具体的什么接口,执行对应接口的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private void invokeAwareInterfaces(Object bean) {
if (bean instanceof Aware) {
if (bean instanceof EnvironmentAware) {
((EnvironmentAware) bean).setEnvironment(this.applicationContext.getEnvironment());
}
if (bean instanceof EmbeddedValueResolverAware) {
((EmbeddedValueResolverAware) bean).setEmbeddedValueResolver(this.embeddedValueResolver);
}
if (bean instanceof ResourceLoaderAware) {
((ResourceLoaderAware) bean).setResourceLoader(this.applicationContext);
}
if (bean instanceof ApplicationEventPublisherAware) {
((ApplicationEventPublisherAware) bean).setApplicationEventPublisher(this.applicationContext);
}
if (bean instanceof MessageSourceAware) {
((MessageSourceAware) bean).setMessageSource(this.applicationContext);
}
if (bean instanceof ApplicationContextAware) {
((ApplicationContextAware) bean).setApplicationContext(this.applicationContext);
}
}
}

4.4.2 容器级生命周期接口方法

容器级生命周期接口方法主要分为Bean后处理器接口方法和工厂后处理器接口方法。前者可以对容器中的bean进行增强,后者对容器进行增强,二者我们依次介绍

4.4.2.1 Bean后处理器

BeanPostProcessor接口是所有Bean后处理器的顶层接口:

1
2
3
4
5
6
7
8
9
10
11
12
public interface BeanPostProcessor {
//参数:arg1:被增强的bean对象;arg2:被增强的bean对象的id
@Nullable
default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
//参数:arg1:被增强的bean对象;arg2:被增强的bean对象的id
@Nullable
default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
}

注意,postProcessBeforeInitialization和postProcessAfterInitialization,在接口上就已经有默认实现了,所以其他的Bean后处理器实现类,不一定要重写这两个方法。

可以看到postProcessBeforeInitialization和postProcessAfterInitialization是一组对称的方法,一个后缀是BeforeInitialization,一个后缀是AfterInitialization。注意,Initialization,初始化的意思,故而一个在初始化前,一个在初始化后。

spring bean的初始化(注意,初始化不是实例化)包含:

  1. 调用InitializingBean接口的afterPropertiesSet方法(如果有实现的话)。
  2. Configuration Metadata中的init方法,如xml配置的init-method属性指定方法,或@Bean注解注册bean定义时,设置注解initMethod属性指定的方法等。
  3. 使用java的注解@PostConstruct,把它标在bean的一个方法上。

而postProcessBeforeInitialization和postProcessAfterInitialization方法的调用位置就是:

bean的实例化-> bean的依赖装配 -> BeforeInitialization接口方法(初始化前) -> bean的初始化方法 -> AfterInitialization接口方法(初始化后)

那么bean后处理器如何使用呢?来,我们来自定义一个bean后处理器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Component
public class MyBeanProcessor implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(Object arg0, String arg1)
throws BeansException {
System.out.println("bean:" + arg1 + " after");
return arg0;
}
@Override
public Object postProcessBeforeInitialization(Object arg0, String arg1)
throws BeansException {
System.out.println("bean:" + arg1 + " before");
return arg0;
}
}

Spring在初始化bean的过程中,会优先初始化那些实现了像BeanPostProcessor这类特殊接口的bean,如果容器发现初始化的bean实现了BeanPostProcessor 接口,将会将其注册为bean后处理器。

一经注册,它对它注册的spring容器下的所有bean起作用,任何bean在初始化过程都会通过bean后处理器做额外增强操作。

作为开发者,我们可以通过实现BeanPostProcessor接口方法,来自定义后处理器类,也可以使用现成,spring为我们准备好的一些后处理器,下面我们简单介绍一些重要的bean后处理器。

4.4.2.1.1 InstantiationAwareBeanPostProcessor

InstantiationAwareBeanPostProcessor也是一个接口,注意,InstantiationAwareBeanPostProcessor的一对before和after接口方法,不是重写的BeanPostProcessor的postProcessBeforeInitialization和postProcessAfterInitialization。

Instantiation和Initialization,还是不一样的,前者是实例化,后者是初始化,要注意区分。

InstantiationAwareBeanPostProcessor实现BeanPostProcessor接口,更多意义上是为了将自己归类进bean后处理器中,好让容器识别自己的“身份”。它的逻辑载体(即三个方法),都是自实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public interface InstantiationAwareBeanPostProcessor extends BeanPostProcessor {
//注意!!!这个方法是InstantiationAwareBeanPostProcessor自己定义的
//不是重写BeanPostProcessor的postProcessBeforeInitialization,两个方法名很像,但不一样
//在bean实例化前调用,如果返回一个非null对象,则Spring就使用这个对象了,不再进行实例化了。
//所以这里可以返回一个目标bean的代理,来压制(延迟)目标bean的实例化。
//这个方法的参数是bean的类型,因为此时还没有bean实例呢。
@Nullable
default Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException {
return null;
}
//注意!!!这个方法是InstantiationAwareBeanPostProcessor自己定义的
//不是重写BeanPostProcessor的postProcessAfterInitialization,两个方法名很像,但不一样
//这是一个理想的地方用来执行自定义字段注入,因为此时Spring的自动装配尚未到来。
//通常方法返回true,如果返回false,后续的属性设置将被跳过。
default boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException {
return true;
}
//在bean属性设置前调用,可以用来定制即将为bean实例设置的属性。
//方法pvs是传进来的已有属性。方法默认返回null。表示不对属性进行操作。
@Nullable
default PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName)
throws BeansException {
return null;
}
@Deprecated
@Nullable
default PropertyValues postProcessPropertyValues(
PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName) throws BeansException {
return pvs;
}
}

InstantiationAwareBeanPostProcessor的名字中有Instantiation(实例化),说明和BeanPostProcessor只能介入初始化的前后不一样,InstantiationAwareBeanPostProcessor可以介入到bean的实例化的前后,所以它的执行时机是:

bean的实例化准备阶段 -> BeforeInstantiation接口方法(实例化前)-> bean的实例化 -> AfterInstantiation接口方法(实例化后) -> PropertyValues接口方法(定制bean所需的属性值) -> bean的属性设置

4.4.2.1.2 DestructionAwareBeanPostProcessor

DestructionAwareBeanPostProcessor接口和InstantiationAwareBeanPostProcessor对应,后者负责实例化前后的增强,后者负责销毁前后的增强。

1
2
3
4
5
6
7
8
9
public interface DestructionAwareBeanPostProcessor extends BeanPostProcessor {
//在bean实例销毁前会被调用,来执行一些定制的销毁代码。
void postProcessBeforeDestruction(Object bean, String beanName) throws BeansException;
//决定是否要为bean实例调用第一个方法来执行一些销毁代码。返回true表示需要,false表示不需要调用。
default boolean requiresDestruction(Object bean) {
return true;
}

}

spring bean的销毁包含:

  1. 调用DisposableBean接口的destroy方法(如果有实现的话)。
  2. Configuration Metadata中的init方法,如xml配置的destroy-method属性指定方法,或@Bean注解注册bean定义时,设置注解destroyMethod属性指定的方法等。
  3. 使用java的注解@PreDestroy,把它标在bean的一个方法上。

执行时机就在销毁前后,不再细述。

4.4.2.1.3 SmartInstantiationAwareBeanPostProcessor

占位,知道有这么一个后处理器,可以用来修改bean类型,定制构造方法,还有获取一个早期(初始化还没执行)bean实例的引用,典型的用法是可以用来解决循环引用。

4.4.2.1.4 MergedBeanDefinitionPostProcessor

占位,知道有这么一个后处理器,这个接口的主要目的不是用来修改合并后的bean定义的,虽然也可以进行一些修改。
它主要用来进行一些自省操作,如一些检测,或在处理bean实例之前缓存一些相关的元数据。
这些作用都在第一个方法里实现。

4.4.2.2 Bean工厂后处理器

和Bean后处理器一样,Bean工厂后处理器是一种特殊的Bean,这种Bean并不对外提供服务,它甚至可以无需id属性,它主要负责对容器本身进行某些特殊的处理和增强。

BeanFactoryPostProcessor是所有工厂后处理器的顶层接口,在spring容器实例化bean的逻辑中,spring正是通过instanceof BeanFactoryPostProcessor这一判断语句来确定一个bean是不是工厂后处理器。

如下图所示,spring提供的BeanFactoryPostProcessor实现类有很多,一些常见的功能,我们可以直接选择合适的工厂后处理器来继承或者实现,以免重复造轮子,其中BeanDefinitionRegistryPostProcessor是BeanFactoryPostProcessor最重要的一个实现类。

Spring中有两类工厂后处理器,BeanDefinitionRegistryPostProcessor和其他。其中其他里面又分为spring源生的,和我们自定义的。

BeanDefinitionRegistryPostProcessor继承自BeanFactoryPostProcessor,设计它的目的是为了使用它向容器注册额外的bean的配置信息——BeanDefinition对象。

4.4.2.2.1 自定义BeanFactoryPostProcessor

我们把spring提供的源生的Bean工厂后处理器之外的,我们自己通过实现BeanFactoryPostProcessor顶层接口的工厂后处理器称为普通工厂后处理器,或者自定义BeanFactoryPostProcessor;

我们先来看下BeanFactoryPostProcessor接口:

1
2
3
4
5
6
public interface BeanFactoryPostProcessor {
//postProcessBeanFactory方法,它的执行的时机是,所有的bean定义都已经注册完毕,不可能再增多了
//该方法允许去修改bean定义的一些属性。
//它允许覆盖或者设置bean的属性值,甚至是立即实例化bean,比如实例化bean后处理器对象。
void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException;
}

BeanFactoryPostProcessor能改变bean在实例化之前的一些原配置值,比如Scope,lazy,Primary,DependsOn,Role,Description等等。

比如我们有个单例的bean:

1
2
3
4
5
6
7
@Component
@Scope("singleton")
public class Teacher{
public Teacher(){
System.out.println("Construct");
}
}

自定义实现BeanFactoryPostProcessor的处理器:

1
2
3
4
5
6
7
8
9
@Component
public class TestBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
BeanDefinition beanDefinition = beanFactory.getBeanDefinition("teacher");
beanDefinition.setScope("prototype");
System.out.println("Scope:"+beanDefinition.getScope());
}
}

这样就完成了对于bean的作用域的变化。

4.4.2.2.2 BeanDefinitionRegistryPostProcessor
1
2
3
4
5
//bean定义注册后处理器,就是用来向容器中注册bean定义的,造成的结果就是beanDefinition的数目变多。
public interface BeanDefinitionRegistryPostProcessor extends BeanFactoryPostProcessor {
//它的接口方法执行的时机是,所有的“常规bean定义”都已注册完毕,该方法允许添加进一步的bean定义注册到容器中。
void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException;
}

多说无益,我们来看demo:

首先我们创建一个类并实现BeanDefinitionRegistryPostProcessor接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class TestBeanDefinitionRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor {
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
System.out.println("TestBeanDefinitionRegistryPostProcessor...postProcessBeanDefinitionRegistry");
System.out.println(registry.getBeanDefinitionCount());

//这里添加一个Dog.class的BeanDefinition进入容器
//RootBeanDefinition beanDefinition = new RootBeanDefinition(Dog.class); 作用同下行
AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(Dog.class).getBeanDefinition();
registry.registerBeanDefinition("dog",beanDefinition);
}

@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
System.out.println("TestBeanDefinitionRegistryPostProcessor...postProcessBeanFactory");
System.out.println(beanFactory.getBeanDefinitionCount());
}
}

这样就完成了往容器中添加BeanDefinition的操作。

4.4.2.2.3 源生工厂后处理器之ConfigurationClassPostProcessor

ConfigurationClassPostProcessor是Spring中非常重要的工厂后处理器,它的主要功能是参与BeanFactory的建造,在这个类中,会解析加了@Configuration的配置类,还会解析@ComponentScan、@ComponentScans注解扫描的包,以及解析@Import等注解。

ConfigurationClassPostProcessor 实现了 BeanDefinitionRegistryPostProcessor 接口,而 BeanDefinitionRegistryPostProcessor 接口继承了 BeanFactoryPostProcessor 接口,所以 ConfigurationClassPostProcessor 中需要重写 postProcessBeanDefinitionRegistry() 方法和 postProcessBeanFactory() 方法。而ConfigurationClassPostProcessor类的作用就是通过这两个方法去实现的。

具体代码逻辑,可以见该文:ConfigurationClassPostProcessor源码解析,介绍的非常的详细。

- 

JVM学习总结之『一个类的前世今生』

Posted on 2020-05-07 | In JAVA , JAVA JVM |
Words count in article: 6.2k | Reading time ≈ 26

1 我乃中山靖王之后

大家好,我叫李大锤,是一名不入流的演员,我即将参演一部名叫《三国演义》的舞台剧,导演是棺材板按不住的罗贯中老先生。而我,即将扮演三位主角之一的刘皇叔,嘿嘿,想想还有点小激动呢!

按照剧本,我是一名出生低微的屌丝,被嘲笑为“织席贩履”之辈,所以一开始,我长这个B样:

1
2
public class LiuBei {
}

然而,我开局就会收关张两位挂逼做小弟,于是,我变成了:

1
2
3
4
public class LiuBei {
int zhangFei=123;//张飞
Object guanYu=new Object();//关羽 为了嫌麻烦,就不给他们定制特定的类了,就int & Object
}

算了,就不追求麾下武将如云谋士如雨了,人太多写的也累,有二弟和三弟出场就够了。

当然,作为未来的汉昭烈帝,我开局还会一些特殊技能,不亏是主角之一,这技能真是别具一格:

1
2
3
4
5
6
7
8
9
10
public void shuaiErZi() {//摔儿子
}

public void shouMaiRenXin() {//收买人心
shuaiErZi();
}
public void geiWoShang(){//给我上
System.out.printf(zhangFei+"");
guanYu.toString();//嘛,就让关张二人随便丢了个技能,Object类嘛,就toString一下。
}

好了,身残志坚,躺在棺材里还在coding的著名程序员罗贯中先生,已经通过他精湛的代码功底,为我编写了一个详(jian)细(lou)的开局设定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class LiuBei {
int zhangFei=123;//张飞
Object guanYu=new Object();//关羽 为了嫌麻烦,就不给他们定制特定的类了,就int & Object
public void shuaiErZi() {//摔儿子
}

public void shouMaiRenXin() {//收买人心
shuaiErZi();
}
public void geiWoShang(){//给我上
System.out.printf(zhangFei+"");
guanYu.toString();//嘛,就让关张二人随便丢了个技能,Object类嘛,就toString一下。
}
}

现在,让我们去为舞台剧做一下准备吧!

2 新手村

舞台剧开演在即,来,摄影机往前,我们先来俯瞰一下整个会场的布局吧(详细介绍见:JAVA内存结构和内存管理):

首先,面积最为广大的,就是我们舞台的后台,我们唤作堆,所有有名有幸的三国豪杰们(对象们),都会在后台齐聚,各自准备。

然后,我们可以看到一块巨大的显示屏,我们唤作方法区,上面是本剧的台本,上面写着:

  • 各个英雄豪杰的设定/经历(类信息)等信息
    • 刘备会遇到关张,然后还会摔儿子技能(属性,方法)
    • 曹操麾下有曹仁曹纯夏侯兄弟等挂逼,还有好人妻这个技能。(属性,方法)
    • ….
  • 一些人尽皆知的信息(常量)
    • 比如现在是东汉末年,嗯,比如东汉末年是个常量。
    • …
  • 某位英雄广为人知的设定(类的静态变量)。
    • 刘备:说织席贩履的给老子滚出来啊魂淡!!
    • 曹操:梦中杀伦什么的,我真不是故意的。
    • 孙权:就不能不提合肥,不提孙十万吗。。

舞台之上,我们看到了有三束聚光灯各自照亮舞台一隅,这是以我们三位主角曹孙刘为视角的三个线程,然后被聚光灯照亮的三块方寸之地,主要是虚拟机栈和本地方法栈,还有一个小的牌子,叫做程序计数器,用来标记此间主角演到剧情的何处了。

3 英雄要问出身

逛完了舞台以后,我得去看看我的台本,虽然我在接戏之前已经知道了罗贯中老先生为我量身定做的草稿:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class LiuBei {
int zhangFei=123;//张飞
Object guanYu=new Object();//关羽 为了嫌麻烦,就不给他们定制特定的类了,就int & Object
public void shuaiErZi() {//摔儿子
}

public void shouMaiRenXin() {//收买人心
shuaiErZi();
}
public void geiWoShang(){//给我上
System.out.printf(zhangFei+"");
guanYu.toString();//嘛,就让关张二人随便丢了个技能,Object类嘛,就toString一下。
}
}

但草稿只是草稿,正经的舞台剧,肯定不能用这么简陋的东西来演出,不说别的,看草稿我只知道我有关张两个小弟,但演出时,我至少得知道关张是谁来演,我到底和谁撘对手戏吧?是胡歌还是霍建华?

所以,还需要把草稿再加工,变成真正的台本,这个过程,叫做编译,这时,java文件会编译成class文件。

class文件的内容我们不再赘述,详情在JAVA Class文件和类加载机制一文中可见。我们只要记得几个核心要素:

  • 类型信息包含魔数,主次版本号等。

  • 常量池里面存放着字面量和符号引用。

    • 常量池中每一项常量都是一个表,在JDK1.7之后共有14种表结构,这14种常量类型各自有自己的结构,下面列出每个常量项的结构及含义
    • 字面量可以理解为就是字符文本,class文件中的其他信息要用到字符文本的时候,都是“引用”他,比如字段表中,刘备有关羽这个小弟,那“关羽”这个名字的文本就存放于常量池中。
    • 符号引用包含下面三类:
      • 全限定名:就是类名全称,例如:org/xxx/class/testClass
      • 简单名称:即没有类型和参数修饰的字段或者方法名称,例如方法test()的简单名称就是test,m字段的简单名称就是m。
      • 描述符:描述符的作用是描述字段的数据类型、方法的参数列表(包括数量、类型及顺序)和返回值。根据描述符的规则,基本数据类型以及代表无返回值的void类型都用一个大写字符来表示,而对象类型则用字符L加对象的全限定名表示
        • 如“viod main(String[] args)” 的描述符为“([Ljava/lang/String;)V
        • 如“String[][]”,会被记录为”[[Ljava/lang/String”
        • “int[]”被记录为“[I”。
  • 字段表集合,记录这个类的字段信息,比如我们刘备拥有关张两个小弟做对象,

    • 这里我们会记录关张的字段名称,如关羽的名称的值就是常量池中“关羽”常量的引用
    • 记录描述符(descriptor_index中记录),描述这个字段的类型,我们这里是Object类型,那么这个值就是指向常量池中的“Ljava/lang/Object”常量的引用。
    • 以及各种修饰符:类似于:汉寿亭侯·美髯公·武圣·刮骨疗法临床实验者·季汉扛把子·关羽=public transient volatile Object guanYu。
  • 方法表集合,记录这个类的方法信息,比如我们刘备拥有摔儿子和收买人心方法。

    • 这里我们会记录方法的名称,同样引用常量池。
    • 记录描述符(descriptor_index中记录),描述这个方法的描述符,我们这里是void shuaiErZi(),那么这个值就是指向常量池中的“()V”常量的引用。(注意这里的V是指void,描述符不包括方法名称)
    • 以及各种修饰符:类似于:作用全场的·效果拔群的·刘备角色固有的·摔儿子=public volatile static shuaiErZi
    • 方法体里面有代码的,都会有一个code属性(引用属性表集合),里面有摔儿子说明文本长度(属性长度),操作数栈最大深度等,还有摔儿子的具体操作步骤(代码的字节码指令)。

来,我们使用javap工具

javap -c -v -p -l -constants /home/lisheng/IdeaProjects/learning/out/production/learning/com/company/LiuBei.class

将public class LiuBei的class文件反解析出来,如下,这就是刘备这个角色经过编译后的舞台剧脚本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
Classfile /home/lisheng/IdeaProjects/learning/out/production/learning/com/company/LiuBei.class
Last modified 2019-12-10; size 1018 bytes
MD5 checksum 7133ac0c7e83a62e1081db1945bc6cf9
Compiled from "LiuBei.java"
public class com.company.LiuBei
minor version: 0 //次版本
major version: 52 //主版本
flags: ACC_PUBLIC, ACC_SUPER //LiuBei类的修饰符
Constant pool: //类的常量池
#1 = Methodref #3.#32 // java/lang/Object."<init>":()V
//#1表示常量池index,此处是一个Methodref类型的常量。
//《JAVA Class文件和类加载机制》一文我们知道Methodref类型内有两个index字段,又引用了两个常量。分别表示方法所属类全限定名,以及方法描述符。
//所以#3.#32 即为#1 = Methodref引用了index=3和index=32的常量。
//我们知道#3是Class类型,引用了#34= java/lang/Object,所以其实#3就是 java/lang/Object的类常量。这表示#1代表的方法是 java/lang/Object类的方法。
//我们知道#32是NameAndType类型常量,又引用了 #20=<init>,#21=()V,合起来就是#32存储了#1代表的方法的方法描述符。
//如此,我们得到了一个完整的Methodref,其内容记录了方法所在类的全限定名以及方法描述符。
//下面以此类推,不再赘述

#2 = Fieldref #15.#33 // com/company/LiuBei.zhangFei:I
#3 = Class #34 // java/lang/Object
#4 = Fieldref #15.#35 // com/company/LiuBei.guanYu:Ljava/lang/Object;
#5 = Methodref #15.#36 // com/company/LiuBei.shuaiErZi:()V
#6 = Fieldref #37.#38 // java/lang/System.out:Ljava/io/PrintStream;
#7 = Class #39 // java/lang/StringBuilder
#8 = Methodref #7.#32 // java/lang/StringBuilder."<init>":()V
#9 = Methodref #7.#40 // java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
#10 = String #41 //
#11 = Methodref #7.#42 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#12 = Methodref #7.#43 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#13 = Methodref #44.#45 // java/io/PrintStream.printf:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;
#14 = Methodref #3.#43 // java/lang/Object.toString:()Ljava/lang/String;
#15 = Class #46 // com/company/LiuBei
#16 = Utf8 zhangFei
#17 = Utf8 I
#18 = Utf8 guanYu
#19 = Utf8 Ljava/lang/Object;
#20 = Utf8 <init>
#21 = Utf8 ()V
#22 = Utf8 Code
#23 = Utf8 LineNumberTable
#24 = Utf8 LocalVariableTable
#25 = Utf8 this
#26 = Utf8 Lcom/company/LiuBei;
#27 = Utf8 shuaiErZi
#28 = Utf8 shouMaiRenXin
#29 = Utf8 geiWoShang
#30 = Utf8 SourceFile
#31 = Utf8 LiuBei.java
#32 = NameAndType #20:#21 // "<init>":()V
#33 = NameAndType #16:#17 // zhangFei:I
#34 = Utf8 java/lang/Object
#35 = NameAndType #18:#19 // guanYu:Ljava/lang/Object;
#36 = NameAndType #27:#21 // shuaiErZi:()V
#37 = Class #47 // java/lang/System
#38 = NameAndType #48:#49 // out:Ljava/io/PrintStream;
#39 = Utf8 java/lang/StringBuilder
#40 = NameAndType #50:#51 // append:(I)Ljava/lang/StringBuilder;
#41 = Utf8
#42 = NameAndType #50:#52 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#43 = NameAndType #53:#54 // toString:()Ljava/lang/String;
#44 = Class #55 // java/io/PrintStream
#45 = NameAndType #56:#57 // printf:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;
#46 = Utf8 com/company/LiuBei
#47 = Utf8 java/lang/System
#48 = Utf8 out
#49 = Utf8 Ljava/io/PrintStream;
#50 = Utf8 append
#51 = Utf8 (I)Ljava/lang/StringBuilder;
#52 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#53 = Utf8 toString
#54 = Utf8 ()Ljava/lang/String;
#55 = Utf8 java/io/PrintStream
#56 = Utf8 printf
#57 = Utf8 (Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;
{
int zhangFei;//字段表,张飞这个字段,name_index指向的是常量池的#16=zhangFei
descriptor: I//descriptor_index指向的是常量池的#17=I,表示类型是int
flags:

java.lang.Object guanYu;//字段表,关羽这个字段,name_index指向的是常量池的#18=guanYu
descriptor: Ljava/lang/Object;//descriptor_index指向的是常量池的 #19=Ljava/lang/Object;,表示类型是object类
flags:

public com.company.LiuBei();//这里开始是方法表,LiuBei()是LiuBei类的默认构造器。name_index指向常量池LiuBei字面量。
descriptor: ()V//descriptor_index指向常量池的#21 = Utf8 ()V
flags: ACC_PUBLIC
Code://code属性,存储着构造器的字节码指令
stack=3, locals=1, args_size=1//
0: aload_0
//从本地变量表中加载索引为0的变量的值,也即this的引用,压入栈
1: invokespecial #1 // Method java/lang/Object."<init>":()V
//出栈,invokespecial表示调用方法,调用哪个方法呢,调用#1代表的java/lang/Object."<init>":()V 方法初始化对象,就是this指定的对象的init()方法完成初始化
4: aload_0
//再一次从本地变量表中加载索引为0的变量的值,也即this的引用,压入栈
5: bipush 123
//将123常量压入栈,当int取值-128~127时,JVM采用bipush指令将常量压入操作数栈中。
7: putfield #2 // Field zhangFei:I
// 将123赋值给zhangFei
//下面同理,new一个Object对象,再执行Object的<init>方法,然后赋值给guanyu,返回。
10: aload_0
11: new #3 // class java/lang/Object
14: dup
15: invokespecial #1 // Method java/lang/Object."<init>":()V
18: putfield #4 // Field guanYu:Ljava/lang/Object;
21: return
LineNumberTable:
//指令与代码行数的偏移对应关系,每一行第一个数字对应代码行数,第二个数字对应前面code中指令前面的数字
line 3: 0
line 4: 4
line 5: 10
LocalVariableTable:
//局部变量表,start+length表示这个变量在字节码中的生命周期起始和结束的偏移位置
//slot就是这个变量在局部变量表中的槽位(槽位可复用),name就是变量名称,Signatur局部变量类型描述

Start Length Slot Name Signature
0 22 0 this Lcom/company/LiuBei;
//下面同理,不再赘述
public void shuaiErZi();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 7: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 this Lcom/company/LiuBei;

public void shouMaiRenXin();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokevirtual #5 // Method shuaiErZi:()V
4: return
LineNumberTable:
line 10: 0
line 11: 4
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/company/LiuBei;

public void geiWoShang();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
3: new #7 // class java/lang/StringBuilder
6: dup
7: invokespecial #8 // Method java/lang/StringBuilder."<init>":()V
10: aload_0
11: getfield #2 // Field zhangFei:I
14: invokevirtual #9 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
17: ldc #10 // String
19: invokevirtual #11 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
22: invokevirtual #12 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
25: iconst_0
26: anewarray #3 // class java/lang/Object
29: invokevirtual #13 // Method java/io/PrintStream.printf:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;
32: pop
33: aload_0
34: getfield #4 // Field guanYu:Ljava/lang/Object;
37: invokevirtual #14 // Method java/lang/Object.toString:()Ljava/lang/String;
40: pop
41: return
LineNumberTable:
line 13: 0
line 14: 33
line 15: 41
LocalVariableTable:
Start Length Slot Name Signature
0 42 0 this Lcom/company/LiuBei;
}
SourceFile: "LiuBei.java"

看完了上面的反解析内容,我们要明白:方法表和常量池里面Methodref的区别:

  • 前者包含包括代码在内的全部方法信息,而后者充其量翻译出来,只包含了方法名+方法描述符+所在类限定名。
  • Methodref顾名思义,只是一个引用,是作为字节码的参数而存在的,如invokespecial #1,#1就是一个Methodref。
    • 所以我们可以看到常量池中存在Methodref=com/company/LiuBei.shuaiErZi:()V,却不存在Methodref=com/company/LiuBei.shouMaiRenXin:()V,因为shouMaiRenXin方法在刘备类的代码中没有被调用,所以它不需要一个包含它基本信息的Methodref
  • 再通俗一点比喻,刘备有技能收买人心,而收买人心技能的发动步骤中包含“大声喊出’摔儿子’三个字,同时发动自己的摔儿子技能”,所以刘备需要像记口诀一样记住“摔儿子”这三个字(即需要在常量池里有这个ref),而因为自己根本不会有喊出“收买人心”四个字的机会,所以常量池里没有必要有“收买人心”的ref。

4 争天下也要排练

上面终于搞懂了我们的台本(类信息)的内容,我也终于理解了罗贯中老导演写的代码到底是什么意思了。舞台剧快开始了,大家赶紧排练(类加载)吧。

加载

排练的第一步,我们每个演员总得拿到我们各自的台本(类信息)吧?

加载的过程,就是将台本纸稿(class文件)的内容导入到方法区大屏幕上的过程,这样我们每个人在排练的时候就可以像看提词器一样,偷瞄我们的设定。

通过一个台本(类)的名称(全限定名),将所有需要的台本文稿(class文件)内容导入到大屏幕,可以使用的导入方式有:

  • 目前可以从zip包获取,即jar,ear,war格式的基础。
  • 从网络获取,即applet实现。
  • 运行时计算生成,典型如动态代理。
  • 由其他文件生成,典型如JSP应用,即为JSP文件生成的class类。
  • 从数据库中读取,这种较少见。

在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。并没有明确存放于要在堆中,实际上它虽然是对象,但是HotSpot虚拟机仍将其存放在方法区中。

验证

验证是为了确保台本信息符合这个舞台剧的需求(确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全),一场三国的舞台剧,你不可以乱入一个李逵吧!

验证会检查格式,规范,引用的验证。

准备

方法区大屏幕上已经显示出我们导入的台本内容了,我们可以看到上面写着刘备的一些信息,假设他有个“织席贩履”的设定(类变量),即 static String sheDing=“织席贩履”。那么我们要把这四个字摆到显眼的地方去,因为他是人尽皆知的设定(类变量),表演中被引用到的概率还是很高的。(实例变量不会在此时分配内存)

所以在方法区大屏幕找一个地方(分配内存),但是注意,只是留了一块空间给它,但是还没有将“织席贩履
四个字给写上去,所以它还只是初始值。

基本数据类型的初始值有这些

解析

我们之前介绍过,在台本里面存储的很多都是符号引用(全限定名,简单名称,描述符),比如我们只知道张飞和关羽这两个人的名字,类型而已,并不知道具体对应到哪一个演员。

解析就是将台本上的符号引用,跟真正的演员对应起来的过程。(虚拟机将常量池内的符号引用替换为直接引用的过程)

我们来分析一下刘备的解析过程:

首先,类加载器加载LiuBei这个类的信息。

然后,我们根据台本,知道刘备有“给我上”这个技能(真实解析顺序并非如此,但这里只是示例,逻辑是相通的)。

1
2
3
4
public void geiWoShang(){//给我上
System.out.printf(zhangFei+"");
guanYu.toString();//嘛,就让关张二人随便丢了个技能,Object类嘛,就toString一下。
}

geiWoShang方法的完整信息,存在方法表集合中(记录了各种修饰符,字段类型,和字段名称,以及各种属性)。

我们看上文的反编译信息可以看到,常量池中存在shuaiErZi方法的Methodref,那么同样是刘备的方法,为什么常量池中没有geiWoShang方法的Methodref呢?我们要记住,只有作为字节码参数的目标(方法,或者字段),才有必要在常量池中放置他们的引用。shuaiErZi方法被shouMaiRenXin方法引用,所以有shuaiErZi方法的Methodref。

方法表长这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public void geiWoShang();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
3: new #7 // class java/lang/StringBuilder
6: dup
7: invokespecial #8 // Method java/lang/StringBuilder."<init>":()V
10: aload_0
11: getfield #2 // Field zhangFei:I
14: invokevirtual #9 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
17: ldc #10 // String
19: invokevirtual #11 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
22: invokevirtual #12 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
25: iconst_0
26: anewarray #3 // class java/lang/Object
29: invokevirtual #13 // Method java/io/PrintStream.printf:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;
32: pop
33: aload_0
34: getfield #4 // Field guanYu:Ljava/lang/Object;
37: invokevirtual #14 // Method java/lang/Object.toString:()Ljava/lang/String;
40: pop
41: return
LineNumberTable:
line 13: 0
line 14: 33
line 15: 41
LocalVariableTable:
Start Length Slot Name Signature
0 42 0 this Lcom/company/LiuBei;

其中有一个属性叫做code,里面的内容就是方法体代码的字节码,它长这样:

1
2
3
4
5
6
 ...
33: aload_0
34: getfield #4 // Field guanYu:Ljava/lang/Object;
37: invokevirtual #14 // Method java/lang/Object.toString:()Ljava/lang/String;
40: pop
...

是的,我们忽略其他,只看调用了guanYu.toString();来作为例子。

getfield #4表示将常量池第四项压入栈。好,常量池第4项还没被解析,那么我们要向解析geiWoShang方法的code,就得先解析常量池第四项。

常量池第四项是啥呢,是个Fieldref,对,是关羽这个字段的Fieldref。

结构抽象后大概是这样:

1
2
3
4
5
6
7
8
9
10
Fieldref{
Class{
index="com/company/LiuBei";index指向的是常量池的 #15=com/company/LiuBei;,表示关羽字段是属于LiuBei类的字段。
}

NameAndType{
name_index="guanYu";//字段表,关羽这个字段,指向的是常量池的#18=guanYu
descriptor_index="Ljava/lang/Object";//descriptor_index指向的是常量池的 #19=Ljava/lang/Object;,表示关羽字段的类型是object类
}
};

解析字段,前提是它所属的类要被加载,我们根据Fieldref的Class_info知道他所属的类是LiuBei,这个已经加载过了,那忽略。(否则,就进入了别的类的加载过程,即用刘备类的加载器区加载别的类。)

然后根据Fieldref的name_index和descriptor_index得到该字段的名称和描述符,去所属类LiuBei的字段表中寻找名称和描述符完全一致的字段。好,找到了:

1
2
3
java.lang.Object guanYu;//字段表,关羽这个字段,name_index指向的是常量池的#18=guanYu
descriptor: Ljava/lang/Object;//descriptor_index指向的是常量池的 #19=Ljava/lang/Object;,表示类型是object类
flags:

那么把关羽这个字段表在刘备类中的偏移量当做直接引用,覆盖常量池的第四项,即#4=关羽这个字段表在刘备类中的偏移量,关羽字段解析完毕,做个标记,解析完成。

这样下次执行getfield #4时,#4直接指向了关羽字段表的直接引用。

同理,我们接下来解析invokevirtual #14,表示调用#14指向的示例方法。

常量池中#14 = Methodref长这样

1
2
3
4
5
6
7
8
9
10
Methodref{
Class{
index="java/lang/Object";index指向的是常量池的 #3=java/lang/Object;,表示该方法是Object的方法。
}

NameAndType{
name_index=" toString";//指向的是常量池的#53=toString,表示名称
descriptor_index="()Ljava/lang/String";//descriptor_index指向的是常量池的 #54=()Ljava/lang/String;,表示方法描述符
}
}

同理,先解析所属的Object类,哦,也加载过了。

那么根据名称和描述符,去Object类的方法表中找到toString方法的偏移量,然后赋值给常量池第十四项。

以此类推,完成所有类的符号引用向直接引用的转变。

初始化

初始化阶段是执行类构造器<clinit>()方法的过程。给类变量赋初值,此时“织席贩履”可以赋值在之前留出的空间上了。

理解sql中的group by和having

Posted on 2020-05-06 | In 关系型数据库 , SQL |
Words count in article: 1.5k | Reading time ≈ 5

前言

group by的可以帮助我们在特定场景下查询到我们需要的数据,但group by的用法一直给人一种“飘忽”感,究其原因,还是对于该关键字缺乏深入理解。

下面通过一个例子,来简单解释group by的原理。

1. GROUP BY

假设我们有表1,表名为test:

如果我们执行如下SQL语句:

1
SELECT name FROM test GROUP BY name

我们很容易可以得到运行的结果:

为了能够更好的理解“group by”多个列“和”聚合函数“的应用,这里可以在表1到表2的过程中,引入一个虚构的中间表:虚拟表3。

FROM test Group BY name:该句执行后,我们想象生成了虚拟表3,如下所图所示:

生成过程是这样的:group by name,那么找name那一列,具有相同name值的行,合并成一行,如对于name值为aa的,那么<1 aa 2>与<2 aa 3>两行合并成1行,所有的id值和number值写到一个单元格里面。

接下来再针对虚拟表3执行Select语句:

  1. 如果执行select *的话,那么返回的结果应该是虚拟表3,可是id和number中有的单元格里面的内容是多个值的,而关系数据库就是基于关系的,单元格中是不允许有多个值的,所以,执行select * 语句是不允许的。

    • 为了约束使用者在编写group by时select多值字段,设计DBMS的开发者也是伤透了脑筋。开发者并不知道将来这个数据库会被用来做什么,所以,他不可能从逻辑上来检查你的select上出现的语句是不是分组属性的一个子集。所以,最简单的方法就是看你的select上出现的属性在group by上出现过。出现过,就通过编译,否则不会。

    • mysql对group by 进行了非ANSI标准的扩展,允许select后含有非group by 的列。所以在mysql中,group by时执行select *不会报错 ,但也得不到我们想要的数据,只会select出原表中的第一个数据

  2. 我们再看name列,每个单元格只有一个数据,所以我们select name的话,就没有问题了。为什么name列每个单元格只有一个值呢,因为我们就是用name列来group by的。

  3. 那么对于id和number里面的单元格有多个数据的情况怎么办呢?答案就是用聚合函数,聚合函数就用来输入多个数据,输出一个数据的。如cout(id),sum(number),而每个聚合函数的输入就是每一个多数据的单元格。

    • 例如我们执行select name,sum(number) from test group by name,那么sum就对虚拟表3的number列的每个单元格进行sum操作,例如对name为aa的那一行的number列执行sum操作,即2+3,返回5,最后执行结果如下:
  4. group by 多个字段该怎么理解呢:如group by name,number,我们可以把name和number 看成一个整体字段,以他们整体来进行分组的。如下图

    • 接下来就可以配合select和聚合函数进行操作了。如执行select name,sum(id) from test group by name,number,结果如下图:
    • -

2. HAVING

首先,不要错误的认为having必须和group by 配合使用。其实having可以单独使用

having关键字在我们的印象中,貌似和where关键字十分相似,那二者有什么区别呢?

  1. 含义:
    • “Where”是一个约束声明,在查询数据库的结果返回结果之前对数据库中的查询条件进行约束,即在结果返回之前起作用,且where后面不能使用“聚合函数”;
      • where后面之所以不能使用聚合函数是因为where的执行顺序在聚合函数之前,所以在执行where的时候,还没有结果集,更别说对结果集做聚合了。
    • Having”是一个过滤声明,所谓过滤是在查询数据库的结果返回之后进行过滤,即在结果返回之后起作用,并且having后面可以使用“聚合函数”。
      • having既然是对查出来的结果进行过滤,那么就不能对没有select出来的字段使用having,如select id , name from student having score >90;这句话就是错误的。

where和having,一个是起作用在结果返回前,用来过滤记录;一个是起作用在结果返回后,用来过滤结果。这种场景的典型应用如这句:SELECT region,count(school) FROM T02_Bejing_school WHERE region IN ('海淀' , '西城' , '东城') GROUP BY region HAVING count(school) > 10;该句sql可以筛选出北京西城、东城、海淀三个区中学校数量超过10所的区及各区学校数量。即先用where把这三个区的中学过滤出来,然后对结果集做group by,得到一张组合后的虚拟表,最后通过having对虚拟表做二次过滤。

  1. 使用的场景:
    • 只有WHERE可以使用的场景:
      • 除select外,where还可以用于update、delete和insert into values(select * from table where ..)语句中,having则不行。
      • select语句中,没有select出要被约束的字段的时候,也不可以使用having。就如上文提到的非法语句:select id , name from student having score >90;
    • 只有HAVING可以使用的场景:
      • 如果要过滤的字段是原生表中不存在的字段,而是经过聚合函数计算后的字段,那么不可以使用where,只能用having。如:
        • 合法语句:select id , avg(price) as ag from goods group by category having ag > 1000
          • 非法语句:select id , avg(price) as ag from goods where ag group by category > 1000
    • 二者都可以使用的场景:
      • 要约束的字段既是原生表的字段,又是sql中被select出来的字段,这时候where和having等效:
        • select price , name from goods where price > 100
        • select price , name from goods having price > 100

参考资料

  1. 理解group by
  2. Group by的理解
  3. 正确理解MySQL中的where和having的区别

MySQL核心要点汇总

Posted on 2020-05-06 | In 关系型数据库 , MySQL |
Words count in article: 11.9k | Reading time ≈ 43

1. 相关概念

1.1 内/外/全联接

假设有两张表,一张本校的校友信息表 t1,一张两院院士信息表 t2,使用二者的身份证号码(ID字段)来关联(即t1.ID=t2.ID)。

  • 内联接:在两张表进行连接查询时,只保留两张表中完全匹配的结果集。
    • select .... from t1 inner join t2 on t1.ID=t2.ID
    • 结果是只保留既是本校校友,又是两院院士的人的信息。
  • 外联接:分为左联接和右联接两种
    1. 左联接:在两张表进行连接查询时,会返回左表所有的行,即使左表在右表中没有匹配的记录。
      • select .... from t1 left (outer) join t2 on t1.ID=t2.ID
      • 结果是返回全部本校校友的记录,部分校友可能同时是院士,其他大部分校友,t2表的相关字段值都为null。
    2. 右联接:在两张表进行连接查询时,会返回右表所有的行,即使右表在左表中没有匹配的记录。
      • select .... from t1 right (outer) join t2 on t1.ID=t2.ID
      • 结果是返回全部两院院士的记录,部分院士可能是我校校友,其他大部分院士,t1表的相关字段值都为null。
  • 全联接:在两张表进行连接查询时,返回左表和右表中所有的行(即便没有匹配)。
    • select .... from t1 full join t2 on t1.ID=t2.ID
    • 结果是返回本校校友+两院院士所有人的记录(当然会去重)。
    • 其实也就是left join和right join的并集。

单纯的select * from a,b是笛卡尔乘积。比如a表有5条数据,b表有3条数据,那么最后的结果有5*3=15条数据。但是如果对两个表进行关联:select * from a,b where a.id = b.id意思就变了,此时就等价于:select * from a inner join b on a.id = b.id。即就是内连接。但是这种写法并不符合规范,可能只对某些数据库管用,如sqlserver。推荐最好不要这样写。最好写成inner join的写法。

1.2 drop、delete与truncate的区别

SQL中的drop、delete、truncate都表示删除,但是三者有一些差别

  1. delete和truncate只删除表的数据不删除表的结构,drop都删除。
  2. 一般来说,执行速度方面是 drop> truncate >delete
  3. delete语句是dml,这个操作会放到rollback segement中,事务提交之后才生效; 如果有相应的trigger,执行的时候将被触发。
  4. truncate、drop是ddl,操作立即生效,原数据不放到rollback segment中,不能回滚.。操作不触发trigger.

1.3 数据并发问题

在典型的应用程序中,多个事务并发运行,经常会操作相同的数据来完成各自的任务(多个用户对同一数据进行操作)。并发虽然是必须的,但可能会导致以下的问题。

  1. 脏读(Dirty read):

    • 针对同一个字段,一个事务(假设事务A)读到了另一个的事务(假设事务B)提交前的数据。比如事务B执行过程中修改了数据X,在未提交前,事务A读取了X,而事务B却回滚了,这样事务A就形成了脏读。
  2. 丢失修改(Lost to modify):

    • 指在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢失修改。
    • 例如:事务1读取某表中的数据A=20,事务2也读取A=20,事务1修改A=A-1,事务2也修改A=A-1,最终结果A=19,事务1的修改被丢失。
  3. 不可重复读(Unrepeatableread):

    • 一般发生在一个事务要在事务内读取一个字段多次的场景。
    • 事务A首先读取了一条数据,然后执行逻辑的时候,事务B将这条数据改变了,然后事务A再次读取的时候,发现数据和第一次读取的时候不一样了,就是所谓的不可重复读了。
  4. 幻读(Phantom read):

    • 幻读与不可重复读类似。也发生在一个事务在事务内部针对某些记录多次查询的情况。
    • 例如在一个事务(A)读取了几行数据,接着另一个并发事务(B)插入并提交了一些数据,并且这些数据符合事务A的where条件时。在第二次的查询中,事务(A)就会发现相比第一次查询,第二次多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。

不可重复读和幻读两者有些相似,他们的区别是:

不可重复读 幻读
针对的是update或delete 针对的是insert
重点是修改:同样的条件, 你读取过的数据, 再次读取出来发现值不一样了 重点在于新增或者删除 (数据条数变化):同样的条件, 第1次和第2次读出来的记录数不一样
## 1.4 事务隔离级别

SQL 标准定义了四个隔离级别:

  1. READ-UNCOMMITTED(读未提交): 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。
  2. READ-COMMITTED(读已提交): 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
  3. REPEATABLE-READ(可重复读): 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
  4. SERIALIZABLE(可串行化): 最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。

MySQL InnoDB 存储引擎的默认的隔离级别是 REPEATABLE-READ(可重复读)。我们可以通过SELECT @@tx_isolation;命令来查看

我们知道隔离级别越低,事务请求的锁越少,并发效率越高,所以大部分数据库系统的隔离级别都是 READ-COMMITTED(读取提交内容) ,但是你要知道的是InnoDB 存储引擎默认使用 REPEATABLE-READ(可重读) 并不会有任何性能损失。

与 SQL 标准不同的地方在于 InnoDB 存储引擎在 REPEATABLE-READ(可重读) 事务隔离级别下使用的是Next-Key Lock 锁算法,因此可以避免幻读的产生,这与其他数据库系统(如 SQL Server) 是不同的。

所以说InnoDB 存储引擎的默认的隔离级别是 REPEATABLE-READ(可重读) 已经可以完全保证事务的隔离性要求,即达到了 SQL标准的 SERIALIZABLE(可串行化) 隔离级别。

2 数据库设计的三范式

2.1 第一范式(1NF)

1NF是对属性的原子性,要求每一列(或者叫字段,属性)具有原子性,不可再分解;

如

学生表(学号,姓名,性别,生日)

如果认为最后一列还可以再分成(出生年,出生月,出生日),它就不满足第一范式了;

2.2 第二范式(2NF)

第二范式是指在满足第一范式的条件下,除主键外的每一列都完全依赖于主键(主要针对于联合主键而言)。

2NF是对记录的惟一性,要求记录有惟一标识,即实体的惟一性,即不存在部分依赖;

举个反例:

表(学号、课程号、姓名、学分) 联合主键为学号和课程号

这个表明显涵盖了两个信息主体:

  1. 学生信息:学号和姓名字段属于学生信息,且姓名依赖于学号(学生信息的唯一标识)
  2. 课程信息:课程号和学分字段属于课程信息,学分依赖课程号(课程信息的唯一标识)。

姓名由学号即可唯一标识,是对主键的部分依赖;
学分由课程号即可唯一标示,是对主键的部分依赖;

由于2NF要求非主键字段必须完全依赖主键,所以不符合二范式。
可能会存在问题:

  • 数据冗余:,每条记录都含有相同信息;
  • 删除异常:删除所有学生成绩,就把课程信息全删除了;
  • 插入异常:学生未选课,无法记录进数据库;
  • 更新异常:调整课程学分,所有行都调整。

正确做法:

  • 学生表:Student(学号, 姓名);
  • 课程表:Course(课程号, 学分);
  • 选课关系表:StudentCourse(学号, 课程号, 成绩)。

2.3 第三范式(3NF)

第三范式是指在满足第二范式的基础上,每一条数据不能依赖于其他的非主属性,也就是消除了传递依赖关系。

3NF是对字段的冗余性,要求任何字段不能由其他字段派生出来,它要求字段没有冗余,即不存在传递依赖;

例如

表(学号, 姓名, 年龄, 学院名称, 学院电话)

因为存在依赖传递: (学号) → (学生)→(所在学院) → (学院电话) 。

可能会存在问题:

  • 数据冗余:有重复值;
  • 更新异常:有重复的冗余信息,修改时需要同时修改多条记录,否则会出现数据不一致的情况

正确做法:

  • 学生:(学号, 姓名, 年龄, 所在学院);
  • 学院:(学院, 电话)。

2.4 反范式化

一般说来,数据库只需满足第三范式(3NF)就行了。没有冗余的数据库未必是最好的数据库,有时为了提高运行效率,就必须降低范式标准,适当保留冗余数据。达到以空间换时间的目的。

比如:有一张存放商品的基本表,“金额”这个字段的存在,表明该表的设计不满足第三范式,因为“金额”可以由“单价”乘以“数量”得到,说明“金额”是冗余字段。但是,增加“金额”这个冗余字段,可以提高查询统计的速度,这就是以空间换时间的作法。

3. MySql存储引擎简述

简单来说,存储引擎就是指表的类型以及表在计算机上的存储方式。

存储引擎的概念是MySQL的特点,Oracle中没有专门的存储引擎的概念,Oracle有OLTP和OLAP模式的区分。不同的存储引擎决定了MySQL数据库中的表可以用不同的方式来存储。我们可以根据数据的特点来选择不同的存储引擎。

在MySQL中的存储引擎有很多种,可以通过mysql> show engines;语句来查看。下面重点关注InnoDB、MyISAM、MEMORY这三种。

3.1 InnoDB引擎

MySQL默认的事务型引擎,也是最重要和使用最广泛的存储引擎。在MySQL从3.23.34a版本开始包含InnnoDB。

InnoDB给MySQL的表提供了事务处理、回滚、崩溃修复能力和多版本并发控制的事务安全。它是MySQL上第一个提供外键约束的存储引擎。而且InnoDB对事务处理的能力,也是其他存储引擎不能比拟的。

InnoDB的性能与自动崩溃恢复的特性,使得它在非事务存储需求中也很流行。除非有非常特别的原因需要使用其他的存储引擎,否则应该优先考虑InnoDB引擎。

3.2 MyISAM引擎

在MySQL 5.1 及之前的版本,MyISAM是默认引擎。MyISAM提供的大量的特性,包括全文索引、压缩、空间函数(GIS)等,但MyISAM并不支持事务以及行级锁,而且一个毫无疑问的缺陷是崩溃后无法安全恢复。正是由于MyISAM引擎的缘故,即使MySQL支持事务已经很长时间了,在很多人的概念中MySQL还是非事务型数据库。尽管这样,它并不是一无是处的。对于只读的数据,或者表比较小,可以忍受修复操作,则依然可以使用MyISAM(但请不要默认使用MyISAM,而是应该默认使用InnoDB)

3.3 MEMORY引擎

MEMORY是MySQL中一类特殊的存储引擎。它使用存储在内存中的内容来创建表,而且数据全部放在内存中。这些特性与前面的两个很不同。

每个基于MEMORY存储引擎的表实际对应一个磁盘文件。该文件的文件名与表名相同,类型为frm类型。该文件中只存储表的结构。而其数据文件,都是存储在内存中,这样有利于数据的快速处理,提高整个表的效率。值得注意的是,服务器需要有足够的内存来维持MEMORY存储引擎的表的使用。如果不需要了,可以释放内存,甚至删除不需要的表。

MEMORY默认使用哈希索引。速度比使用B型树索引快。当然如果你想用B型树索引,可以在创建索引时指定。

注意,MEMORY用到的很少,因为它是把数据存到内存中,如果内存出现异常就会影响数据。如果重启或者关机,所有数据都会消失。因此,基于MEMORY的表的生命周期很短,一般是一次性的。

3.4 如何合适的选择存储引擎

  • 有以下要求,则适合采用InnoDB:

    • 需要对事务的完整性要求比较高(比如银行)
    • 要求实现并发控制(比如售票)
    • 如果需要频繁的更新、删除操作的数据库,也可以选择InnoDB,因为支持事务的提交(commit)和回滚(rollback)。
  • 有以下要求,则适合采用MyISAM:

    • 如果表主要是用于插入新记录和读出记录,那么选择MyISAM能实现处理高效率。
    • 如果应用对数据的完整性、并发性要求比较低,也可以使用。
  • 有以下要求,则适合采用MEMORY:

    • 如果需要很快的读写速度,对数据的安全性要求较低,且数据量很小时,可以选择MEMOEY。

3.5 MyISAM与InnoDB区别

项目 InnoDB MyISAM
存储结构 所有的表都保存在同一个数据文件中(也可能是多个文件,或者是独立的表空间文件),InnoDB表的大小只受限于操作系统文件的大小,一般为2GB。 每个MyISAM在磁盘上存储成三个文件。分别为:表定义文件、数据文件、索引文件。第一个文件的名字以表的名字开始,扩展名指出文件类型。.frm文件存储表定义。数据文件的扩展名为.MYD (MYData)。索引文件的扩展名是.MYI (MYIndex)。
存储空间 需要更多的内存和存储,它会在主内存中建立其专用的缓冲池用于高速缓冲数据和索引。 MyISAM支持支持三种不同的存储格式:静态表(默认,但是注意数据末尾不能有空格,会被去掉)、动态表、压缩表。当表在创建之后并导入数据之后,不会再进行修改操作,可以使用压缩表,极大的减少磁盘的空间占用。
可移植性、备份及恢复 免费的方案可以是拷贝数据文件、备份 binlog,或者用 mysqldump,在数据量达到几十G的时候就相对痛苦了。 数据是以文件的形式存储,所以在跨平台的数据转移中会很方便。在备份和恢复时可单独针对某个表进行操作。
事务支持 提供事务支持事务,外部键等高级数据库功能。 具有事务(commit)、回滚(rollback)和崩溃修复能力(crash recovery capabilities)的事务安全(transaction-safe (ACID compliant))型表。 强调的是性能,每次查询具有原子性,其执行速度比InnoDB类型更快,但是不提供事务支持。
AUTO_INCREMENT InnoDB中必须包含只有该字段的索引。引擎的自动增长列必须是索引,如果是组合索引也必须是组合索引的第一列。 可以和其他字段一起建立联合索引。引擎的自动增长列必须是索引,如果是组合索引,自动增长可以不是第一列,他可以根据前面几列进行排序后递增。
锁 支持事务和行级锁,是innodb的最大特色。行锁大幅度提高了多用户并发操作的新能。但是InnoDB的行锁,只是在WHERE中指定主键是有效的,非主键的WHERE都会锁全表的。 只支持表级锁,用户在操作myisam表时,select,update,delete,insert语句都会给表自动加锁,如果加锁以后的表满足insert并发的情况下,可以在表的尾部插入新的数据。
全文索引 原来不支持FULLTEXT类型的全文索引,但是innodb可以使用sphinx插件支持全文索引,并且效果更好。后来从InnoDB1.2.x版本(MySQL 5.6版本)起,InnoDB存储引擎开始支持全文索引 支持 FULLTEXT类型的全文索引
表主键 如果没有设定主键或者非空唯一索引,就会自动生成一个6字节的主键(用户不可见),数据是主索引的一部分,附加索引保存的是主索引的值。 允许没有任何索引和主键的表存在,索引都是保存行的地址。
表的具体行数 没有保存表的总行数,如果使用select count(*) from table;就会遍历整个表,消耗相当大,但是在加了where条件后,myisam和innodb处理的方式都一样。 保存有表的总行数,如果select count(*) from table;会直接取出出该值。
CRUD操作 如果你的数据执行大量的INSERT或UPDATE,出于性能方面的考虑,应该使用InnoDB表。 如果执行大量的SELECT,MyISAM是更好的选择。
外键 支持 不支持

4. MySql索引

我们都希望查询数据的速度能尽可能的快,因此数据库系统的设计者会从查询算法的角度进行优化。

在数据之外,数据库系统维护着满足特定查找算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构上实现高级查找算法。这种数据结构,就是索引。

上图展示了一种可能的索引方式。左边是数据表,一共有两列七条记录,最左边的是数据记录的物理地址(注意逻辑上相邻的记录在磁盘上也并不是一定物理相邻的)。

为了加快Col2的查找,可以维护一个右边所示的二叉查找树,每个节点分别包含索引键值和一个指向对应数据记录物理地址的指针,这样就可以运用二叉查找在O(log2n)的复杂度内获取到相应数据。

虽然这是一个货真价实的索引,但是实际的数据库系统几乎没有使用二叉查找树或其进化品种红黑树(red-black tree)实现的,原因会在下文介绍。

4.1 索引的优缺点

优点:

  • 可以快速检索,减少I/O次数,加快检索速度;
  • 根据索引分组和排序,可以加快分组和排序;

缺点:

  • 索引本身也是表,因此会占用存储空间,一般来说,索引表占用的空间是数据表的1.5倍;
  • 索引表的维护和创建需要时间成本,这个成本随着数据量增大而增大;
  • 构建索引会降低数据表的修改操作(删除,添加,修改)的效率,因为在修改数据表的同时还需要修改索引表;

4.2 索引的分类

4.2.1 按类型分类

  1. 聚集索引
    • 主键索引;
      • 数据列不允许重复,不允许为NULL,一个表只能有一个主键。
  2. 二级索引(又称辅助索引、非聚簇索引)
    • 唯一索引;
      • 约束数据列不允许重复,允许为NULL值
      • 一个表允许组合多个列创建唯一索引,这时约束的是:不同记录,被唯一索引约束的这多个列不能让完全相同
    • 普通索引(又叫辅助索引);
      • 可以通过ALTER TABLE table_name ADD INDEX index_name (column);创建普通索引
      • 会对该列创建索引。
    • 组合索引(又称联合索引,复合索引);
      • 即普通索引的多字段版本
      • 可以通过ALTER TABLE table_name ADD INDEX index_name(column1, column2, column3);创建组合索引
      • 如下图,可以理解成把几个字段拼接起来的一个普通索引

4.2.2 按按数据结构分类

  • BTree索引
    • 下文详解
  • B+Tree索引;
    • 下文详解
  • 哈希索引;
    • 只有memory存储引擎支持哈希索引,哈希索引用索引列的值计算该值的hashCode,然后在hashCode相应的位置存储该值所在行数据的物理位置。
    • 因为使用散列算法,因此访问速度非常快,但是一个值只能对应一个hashCode,而且是散列的分布方式,因此哈希索引不支持范围查找和排序的功能。
  • 全文索引;
    • 通过建立倒排索引来实现,查询效率比like有很大提升。
    • 5.6版本前的MySQL自带的全文索引只能用于MyISAM存储引擎,如果是其它数据引擎,那么全文索引不会生效。5.6版本之后InnoDB存储引擎开始支持全文索引
    • 在MySQL中,全文索引只对英文有用,目前对中文还不支持。5.7版本之后通过使用ngram插件开始支持中文。

4.2.3 聚簇索引和非聚簇索引的区别(针对InnoDB)

假设我们有如下表

mysql对ID生成了聚簇索引,我们再对k字段生成普通索引(非聚簇),如下图:

其中R代表一整行的记录。

从图中不难看出,聚簇索引和非聚簇索引的区别是:非聚簇索引的叶子节点存放的是主键的值,而聚簇索引的叶子节点存放的是整行数据。

根据这两种结构我们来进行下查询,看看他们在查询上有什么区别。

  1. 如果查询语句是 select * from table where ID = 100,即主键查询的方式,则只需要搜索 ID 这棵 B+树。

  2. 如果查询语句是 select * from table where k = 1,即非主键的查询方式,则先搜索k索引树,得到ID=100,再到ID索引树搜索一次,这个过程也被称为回表。

回表是非常重要的概念,需要敲黑板划重点记住。其过程就如下图所示:

什么非主键索引结构叶子节点存储的是主键值?
一是保证一致性,更新数据的时候只需要更新主键索引树,二是节省存储空间。

4.2.4 为什么建议使用主键自增的索引

自增的主键,插入到索引的时候,直接在最右边插入就可以了

但是如果插入的是 ID = 350 的一行数据,由于 B+ 树是有序的,那么需要将下面的叶子节点进行移动,腾出位置来插入 ID = 350 的数据,这样就会比较消耗时间,如果刚好 R4 所在的数据页已经满了,需要进行页分裂操作,这样会更加糟糕。

所以使用自增主键,每次插入的 ID 都会比前面的大,那么就可以避免这种情况。

4.3 索引的数据结构

索引的数据结构,常见的是B树和B+树,MySql的索引使用的是B+树,关于B树一家子的分析,可以详见下文:B树/B+树分析

不过虽然都是使用B+树来做数据结构,但在MySQL中,索引属于存储引擎级别的概念,不同存储引擎对索引的实现方式是不同的(不过至少都是B+树)。

4.3.1 MyISAM索引实现

MyISAM引擎使用B+Tree作为索引结构,其主键索引和普通索引在结构上没有区别,叶节点的data域存放的是数据记录的地址。

4.3.1.1 MyISAM主键索引

如下图,这时一个针对主键col1字段的索引结构图:

可以看出MyISAM的索引文件仅仅保存数据记录的地址。

4.3.1.2 MyISAM普通索引

在MyISAM中,主索引和普通索引(Secondary key)在结构上没有任何区别,只是主索引要求key是唯一的,而普通索引的key可以重复。如果我们在Col2上建立一个辅助索引,则此索引的结构如下图所示:

同样也是一颗B+Tree,data域保存数据记录的地址。因此,MyISAM中索引检索的算法为首先按照B+Tree搜索算法搜索索引,如果指定的Key存在,则取出其data域的值,然后以data域的值为地址,读取相应数据记录。

发现没有?MyISAM的索引方式,跟我们上文说的非聚簇索引十分相像(一个是存放id,一个是存放地址)。所以MyISAM索引的实现方式是非聚簇索引。

4.3.2 InnoDB索引实现

虽然InnoDB也使用B+Tree作为索引结构,但具体实现方式却与MyISAM截然不同。对,InnoDB的索引是聚簇式的:InnoDB的数据文件本身就是索引文件,树的叶节点data域保存了完整的数据记录。

4.3.2.1 InnoDB主键索引实现

我们先来看 InnoDB的主键索引,这个索引的key是数据表的主键,因此InnoDB表数据文件本身就是主索引。

因为InnoDB的数据文件本身要按主键聚集,所以InnoDB要求表必须有主键(MyISAM可以没有),如果没有显式指定,则MySQL系统会自动选择一个可以唯一标识数据记录的列作为主键,如果不存在这种列,则MySQL自动为InnoDB表生成一个隐含字段作为主键,这个字段长度为6个字节,类型为长整形。

4.3.2.2 InnoDB普通索引实现

在MyISAM中主索引和普通索引(Secondary key)在结构上没有任何区别,但InnoDB中,普通索引和主键索引是不同的,前文我们也介绍过,InnoDB的普通索引是非聚簇式的。

例如,图11为定义在Col3上的一个辅助索引:

图中的15,18这些数字,就是col3所对应的主键值,普通索引搜索需要检索两遍索引:首先检索辅助索引获得主键,然后用主键到主索引中检索获得记录。

了解不同存储引擎的索引实现方式对于正确使用和优化索引都非常有帮助,例如知道了InnoDB的索引实现后,就很容易明白为什么不建议使用过长的字段作为主键,因为所有辅助索引都引用主索引,过长的主索引会令辅助索引变得过大。再例如,用非单调的字段作为主键在InnoDB中不是个好主意,因为InnoDB数据文件本身是一颗B+Tree,非单调的主键会造成在插入新记录时数据文件为了维持B+Tree的特性而频繁的分裂调整,十分低效,而使用自增字段作为主键则是一个很好的选择。

最后来一张图总结一下InnoDB和Mylsam两种不同索引的结构:

4.3.3 联合索引的数据结构

我们知道了Mysql的索引采用B+树,那么,联合索引的B+树长什么样呢??

4.3.3.1 MylSAM的联合索引

假如我们有一张表

那么,联合索引的B+树结构是长这样的:

注意,这是MyISAM的联合索引,也就是说,叶子节点的key是索引列b,c,d的组合,value是指向表记录的内存地址。如果是InnoDB的联合索引,那么叶子结点应该key是b,c,d的组合,value是表的pk,也就是a字段。

即每个元素的key,都是b,c,d三个字段的组合。那么不同元素之间的排序是依照什么规则呢?第一列的值大小吗?

答案是:先判断 b 再判断 c 然后是 d,即优先级为b>c>d。

4.3.3.2 InnoDB的联合索引

有一张表test,这张表除了主键id外,还有a,b, c 三列

假设给这三个字段建一个复合索引 index_abc (a, b, c),那么其B+树的结构如下图所示:

key的排序同理,先判断 a 再判断 b 然后是 c,即优先级为b>c>d。

4.4 索引生效条件

我们创建了索引,但很多时候,我们发现我们的查询语句无法使用到索引,基于此,我们首先要了解索引的命中规则。

那么怎么知道我们写的sql语句是否有使用到索引呢,可以使用explain命令,直接在sql语句前加explain执行:

explain执行结果关注以下几个字段:

  1. select_type:
    • 查询的类型,主要是区别普通查询和联合查询、子查询之类的复杂查询
      • SIMPLE:查询中不包含子查询或者UNION
      • 查询中若包含任何复杂的子部分,最外层查询则被标记为:PRIMARY
      • 在SELECT或WHERE列表中包含了子查询,该子查询被标记为:SUBQUERY
  2. possible_keys
    • 表示查询时可能使用的索引。如果是空的,没有相关的索引。这时要提高性能,可通过检验WHERE子句,看是否引用某些字段,或者检查字段不是适合索引
  3. key
    • 显示sql执行过程中实际使用的键或索引,如果为null则表示未使用任何索引,必须进行优化。
  4. rows
    • rows是指这次查找数据所内循环的次数。
  5. Extra:
    • 执行情况的说明和描述。包含不适合在其他列中显示但十分重要的额外信息
  6. type
    • type意味着类型,这里的type官方全称是“join type”,意思是“连接类型”,这样很容易给人一种错觉觉得必须需要俩个表以上才有连接类型。事实上这里的连接类型并非字面那样的狭隘,
    • 它更确切的说是一种数据库引擎查找表的一种方式,在《高性能mysql》一书中作者更是觉得称呼它为访问类型更贴切一些。
    • mysql5.7中type的类型达到了14种之多,这里只记录和理解最重要且经常遇见的六种类型,它们分别是all<index<range<ref<eq_ref<const。从左到右,它们的效率依次是增强的。
    • 撇开sql的具体应用环境以及其他因素,你应当尽量优化你的sql语句,使它的type尽量靠右,但实际运用中还是要综合考虑各个方面的。

4.4.1 explain的type字段类型

  1. all:这便是所谓的“全表扫描”,如果是在一个查找数据项的sql中出现了all类型,那通常意味着你的sql语句处于一种最原生的状态,有很大的优化空间。all是一种非常暴力和原始的查找方法,非常的耗时而且低效。
  2. index:这种连接类型只是另外一种形式的全表扫描,只不过它的扫描顺序是按照索引的顺序。这种扫描根据索引然后回表取数据,和all相比,他们都是取得了全表的数据,而且index要先读索引而且要回表随机取数据
  3. range:range指的是有范围的索引扫描,相对于index的全索引扫描,它有范围限制,因此要优于index。关于range比较容易理解,需要记住的是出现了range,则一定是基于索引的。同时除了显而易见的between,and以及’>’,’<’外,in和or也是索引范围扫描。
  4. ref:出现该连接类型的条件是: 查找条件列使用了索引而且不为主键和unique。其实,意思就是虽然使用了索引,但该索引列的值并不唯一,有重复(使用了普通索引的意思)。这样即使使用索引快速查找到了第一条数据,仍然不能停止,要进行目标值附近的小范围扫描。但它的好处是它并不需要扫全表,因为索引是有序的,即便有重复值,也是在一个非常小的范围内扫描。
  5. ref_eq:ref_eq 与 ref相比牛的地方是,它知道这种类型的查找结果集只有一个。什么情况下结果集只有一个呢!那便是使用了主键或者唯一性索引进行查找的情况。比如根据学号查找某一学校的一名同学,在没有查找前我们就知道结果一定只有一个,所以当我们首次查找到这个学号,便立即停止了查询。这种连接类型每次都进行着精确查询,无需过多的扫描,因此查找效率更高,当然列的唯一性是需要根据实际情况决定的。
  6. const:通常情况下,如果将一个主键放置到where后面作为条件查询,mysql优化器就能把这次查询优化转化为一个常量。即直接按主键或唯一键读取。
  7. NULL:不用访问表或者索引,直接就能得到结果,如select 1 from test where 1

看起来const和ref_eq貌似是一样的啊,都是使用主键或者唯一性索引,其实eq_ref是用于联表查询的情况,按联表的主键或唯一键联合查询。

4.4.2 索引失效场景

很多时候,我们在列上建了索引,查询条件也是索引列,但最终执行计划没有走它的索引。那到底哪些场景,会导致索引失效呢?

  1. 列与列对比

    • 某个表中,有两列(id和c_id)都建了单独索引,下面这种查询条件不会走索引
    • select * from test where id=c_id;
  2. 存在NULL值条件

    • 我们在设计数据库表时,应该尽力避免NULL值出现,如果非要不可避免的要出现NULL值,也要给一个DEFAULT值
    • select * from test where id=c_id;
  3. NOT条件

    • 我们知道建立索引时,给每一个索引列建立一个条目,如果查询条件为等值或范围查询时,索引可以根据查询条件去找对应的条目。反过来当查询条件为非时,索引定位就困难了,执行计划此时可能更倾向于全表扫描,这类的查询条件有:<>、NOT、not exists
      1
      2
      3
      select * from test where id<>500;
      select * from test where not in (6,7,8,9,0);
      select * from test where not exists (select 1 from test_02 where test_02.id=test.id);
  4. LIKE通配符的前匹配

    • 当使用模糊搜索时,尽量采用后置的通配符,例如:name%,因为走索引时,其会从前去匹配索引列,这时候是可以找到的,如果采用前匹配,那么查索引就会很麻烦,比如查询所有姓张的人,就可以去搜索’张%’。相反如果你查询所有叫‘明’的人,那么只能是%明。这时候索引如何定位呢?前匹配的情况下,执行计划会更倾向于选择全表扫描。后匹配可以走INDEX RANGE SCAN。
    • select * from test where name like '张%';
  5. 条件上对列使用函数

    • 查询条件上尽量不要对索引列使用函数,比如下面这个SQL——这样是不会走索引的,因为索引在建立时会和计算后可能不同,无法定位到索引。
    • select * from test where upper(name)='SUNYANG';
    • 但如果查询条件不是对索引列进行计算,那么依然可以走索引。比如
    • select * from test where name=upper('sunyang');
  6. 数据类型的转换

    • 当查询条件存在隐式转换时,索引会失效。比如在数据库里id存的number类型,但是在查询时,却用了下面的形式:
    • select * from sunyang where id='123';
  7. 谓词运算

    • 我们在上面说,不能对索引列进行函数运算,这也包括加减乘除的谓词运算,这也会使索引失效。建立一个sunyang表,索引为id,看这个SQL:
    • select * from sunyang where id/2=15;
    • 这里很明显对索引列id进行了’/2’除二运算,这时候就会索引失效,这种情况应该改写为:
    • select * from sunyang where id=30;
  8. or连接中包含非独立索引

    • 先看如下这个sql:
    • SELECT * from t WHERE id = 1 or uid = 2;
    • 如果id和uid都有单独的索引,那么mySql优化器会采用index merge 技术使其走索引。index merge 技术简单说就是在用OR,AND连接的多个查询条件时,可以分别使用前后查询中的索引,然后将它们各自的结果合并交集或并集。
    • 但如果uid列上没有单独的索引,那么这个sql将不会走索引,即便id上有主键索引。

4.4.3 联合索引生效条件(最左前缀原则)

上文中我们介绍了联合索引的数据结构,对于index(b,c,d)是长这样的:

因为联合索引中的元素key都是一个组合值<b,c,d>,且排序依据的优先级是b>c>d,所以联合索引的生效条件,要满足最左前缀原则。我们看如下sql:

1
2
3
4
5
6
7
8
9
10
SELECT * from t1 WHERE b = 1 and c = 2 and d = 3; //走索引
SELECT * from t1 WHERE b = 1 and c = 2 //走索引
SELECT * from t1 WHERE b = 1 //走索引
SELECT * from t1 WHERE c = 2 and d = 3; //不走索引
SELECT * from t1 WHERE d = 3; //不走索引
SELECT * from t1 WHERE b = 1 and d = 3 //走部分索引,至少会走到b = 1的子树上。

//范围查询
SELECT * from t1 WHERE b < 1; //走索引
SELECT * from t1 WHERE b < 1 and c < 2 and d>3; //走部分索引,b<1走了索引,后面两个条件无法走索引。(索引最多用于一个范围列)

这就是最左前缀原则,还是比较好理解的,需要注意的是索引最多用于一个范围列(且只能是最左的列)。

不过大多数时候,mySql优化器会按照现有的索引来优化sql语句的where条件顺序,比如SELECT * from t1 WHERE c = 2 and b = 1就会被优化为SELECT * from t1 WHERE b = 1 and c = 2,使得这条sql可以走索引。

4.5 索引优化

4.5.1 索引的选择性

既然索引可以加快查询速度,那么是不是只要是查询语句需要,就建上索引?答案是否定的。因为索引虽然加快了查询速度,但索引也是有代价的:索引文件本身要消耗存储空间,同时索引会加重插入、删除和修改记录时的负担,另外,MySQL在运行时也要消耗资源维护索引,因此索引并不是越多越好。一般两种情况下不建议建索引。

  1. 表记录比较少:

    • 例如一两千条甚至只有几百条记录的表,没必要建索引,让查询做全表扫描就好了。至于多少条记录才算多,这个个人有个人的看法,我个人的经验是以2000作为分界线,记录数不超过 2000可以考虑不建索引,超过2000条可以酌情考虑索引。
  2. 索引的选择性较低。

    • 所谓索引的选择性(Selectivity),是指不重复的索引值(也叫基数,Cardinality)与表记录数(#T)的比值:
    • Index Selectivity = Cardinality / #T
    • 显然选择性的取值范围为(0, 1],选择性越高的索引价值越大,这是由B+Tree的性质决定的。例如,employees.titles表,如果title字段经常被单独查询,是否需要建索引,我们看一下它的选择性:
    • SELECT count(DISTINCT(title))/count(*) AS Selectivity FROM employees.titles;

4.5.2 前缀索引

有一种与索引选择性有关的索引优化策略叫做前缀索引,就是用列的前缀代替整个列作为索引key,当前缀长度合适时,可以做到既使得前缀索引的选择性接近全列索引,同时因为索引key变短而减少了索引文件的大小和维护开销。

下面以employees.employees表为例介绍前缀索引的选择和使用。

如果我们需要频繁按名字搜索员工,这样显然效率很低,因此我们可以考虑建索引。有两种选择,建或<first_name, last_name>,看下两个索引的选择性:

1
2
3
4
5
6
7
8
9
10
11
12
SELECT count(DISTINCT(first_name))/count(*) AS Selectivity FROM employees.employees;
+-------------+
| Selectivity |
+-------------+
| 0.0042 |
+-------------+
SELECT count(DISTINCT(concat(first_name, last_name)))/count(*) AS Selectivity FROM employees.employees;
+-------------+
| Selectivity |
+-------------+
| 0.9313 |
+-------------+

显然选择性太低,<first_name, last_name>选择性很好,但是first_name和last_name加起来长度为30,有没有兼顾长度和选择性的办法?可以考虑用first_name和last_name的前几个字符建立索引,例如<first_name, left(last_name, 3)>,看看其选择性:

1
2
3
4
5
6
SELECT count(DISTINCT(concat(first_name, left(last_name, 3))))/count(*) AS Selectivity FROM employees.employees;
+-------------+
| Selectivity |
+-------------+
| 0.7879 |
+-------------+

选择性还不错,但离0.9313还是有点距离,那么把last_name前缀加到4:

1
2
3
4
5
6
SELECT count(DISTINCT(concat(first_name, left(last_name, 4))))/count(*) AS Selectivity FROM employees.employees;
+-------------+
| Selectivity |
+-------------+
| 0.9007 |
+-------------+

这时选择性已经很理想了,而这个索引的长度只有18,比<first_name, last_name>短了接近一半,我们把这个前缀索引建上:

1
2
ALTER TABLE employees.employees
ADD INDEX `first_name_last_name4` (first_name, last_name(4));

此时再执行一遍按名字查询,比较分析一下与建索引前的结果:MYSQL中使用SHOW PROFILE命令分析性能

1
2
3
4
5
6
7
SHOW PROFILES;
+----------+------------+---------------------------------------------------------------------------------+
| Query_ID | Duration | Query |
+----------+------------+---------------------------------------------------------------------------------+
| 87 | 0.11941700 | SELECT * FROM employees.employees WHERE first_name='Eric' AND last_name='Anido' |
| 90 | 0.00092400 | SELECT * FROM employees.employees WHERE first_name='Eric' AND last_name='Anido' |
+----------+------------+---------------------------------------------------------------------------------+

性能的提升是显著的,查询速度提高了120多倍。

前缀索引兼顾索引大小和查询速度,但是其缺点是不能用于ORDER BY和GROUP BY操作,也不能用于Covering index(即当索引本身包含查询所需全部数据时,不再访问数据文件本身)。

4.5.3 覆盖索引

我们知道,联合索引的B+树是长这个样子的(InnoDB版本,index_abc为(a,b,c)的联合索引):

那么假如我们有如下的语句:

select a,b,c from test where b>3

请问这句话走不走索引呢?

答案是:走索引

where b>3 根据最左前缀原则明明不会命中index_abc啊,为什么这条语句会走索引呢??

因为这句sql,不用回表,这会极大的提高查询性能。

为什么不用回表?因为对比联合索引的结构图我们可以看到,该句sql select的三个字段,都是联合索引的索引字段,这使得联合索引index_abc的叶子结点上就已经能够得到a,b,c三个字段了,用不着回表就足够把需要的a,b,c数据都查出来。

但where b>3不满足最左前缀原则啊!那么索引是怎么走的呢?

答案是,遍历B+树上的所有节点。是的,因为不满足最左前缀,所以该句sql无法很高效的利用索引来将性能达到极致,但是遍历B+树上的所有节点仍然比全表扫描要快得多,因为B+树多叉的特性,其节点数量远远小于表记录的数量。

这种索引叫做覆盖索引,即现有的索引能够覆盖select的字段,那么就可以通过遍历索引树节点,且无需回表的方式,来提高查询性能。

理解了覆盖索引的含义,那么举一反三,我们可以知道:

1
2
3
4
select a,b from test where c>3     //走索引
select id,a,b from test where c>3 //走索引,别忘了联合索引的叶子节点上除了联合索引的索引列,还有主键id
select a,b,d from test where c>3 //不走索引,因为d不在index_abc的覆盖范围内
select a,b,c from test where a>3 //走索引,且还能满足最左前缀原则,性能最高。

所以在select的字段不多的时候,我们可以考虑创建这几个字段的联合索引,来促使sql走覆盖索引,提高查询性能。

4.5.4 索引下推

对于user_table表,我们现在有(username,age)联合索引。

如果现在有一个需求,查出名称中以“张”开头且年龄小于等于10的用户信息,语句如下:

select * from user_table where username like '张%' and age > 10

那么我们可以推测出来,语句有两种执行可能:

  1. 根据(username,age)联合索引查询出所有满足名称以“张”开头的叶子节点,得到pk,然后回表查询出相应的全行数据,然后再在结果中筛选出满足年龄小于等于10的用户数据
  2. 根据(username,age)联合索引查询所有满足名称以“张”开头的叶子节点,然后再对这些叶子节点筛选出年龄小于等于10的叶子节点,得到pk,之后再回表查询全行数据。

明显的,第二种方式需要回表查询的全行数据比较少,这就是mysql的索引下推,即where条件中的字段如果能被某个联合索引覆盖(和覆盖索引有点像),那么直接在联合索引中完成过滤操作,缩小范围,最后再做回表操作。

mysql默认启用索引下推,我们也可以通过修改系统变量optimizer_switch的index_condition_pushdown标志来控制
SET optimizer_switch = 'index_condition_pushdown=off';


参考材料

  1. 数据库逻辑设计之三大范式通俗理解,一看就懂,书上说的太晦涩
  2. 《高性能MySQL》
  3. MySQL索引背后的数据结构及算法原理
  4. mysql中explain的type的解释
  5. 左匹配原则,聚集索引,回表查询,索引覆盖 你真的懂了吗
  6. 索引下推(5.6版本+)
  7. mysql的联合索引的B+树到底张什么样子?

B树/B+树分析

Posted on 2020-04-28 | In 数据结构与算法 , 树/堆 |
Words count in article: 5.8k | Reading time ≈ 20

前言

目前我们常见的动态查找树主要有:二叉查找树(Binary Search Tree),平衡二叉查找树(Balanced Binary Search Tree),红黑树(Red-Black Tree ),这三者是典型的二叉树结构,利用二分法,可以使其查询的时间复杂度为O(log2N),即与树的深度相关。

但二叉树一个节点中包含有一个元素,和指向两个子节点的指针,在现实生活中,未免有些太“奢侈”了。为了降低树的深度,提高查找效率,我们完全可以采用多叉树结构(由于树节点元素数量是有限的,自然该节点的子树数量也就是有限的)来得到一棵更加“矮胖”的树,以适应我们日益增长的数据量和查询性能要求,在这种背景下,B树和他的亲戚们应运而生。

1. B树

B-tree(B-tree树即B树,B即Balanced,平衡的意思)这棵神奇的树是在Rudolf Bayer, Edward M. McCreight(1970)写的一篇论文《Organization and Maintenance of Large Ordered Indices》中首次提出的(wikipedia中:http://en.wikipedia.org/wiki/B-tree,阐述了B-tree名字来源以及相关的开源地址)。

B树属于多叉树,又名平衡多路查找树(查找路径不只两个),数据库索引技术里大量使用者B树和B+树的数据结构。

强调一下,有的文章里出现的B-树,就是B树。因为B树的原英文名称为B-tree,而国内很多人喜欢把B-tree译作B-树,其实,这是个非常不好的直译,很容易让人产生误解。如人们可能会以为B-树是一种树,而B树又是一种一种树。而事实上是,B-tree就是指的B树。

什么是B树?抛出一大堆概念前,我们先看看他长什么样:

这里面,每个字母,都表示一个键值对[key,value],在关系型数据库的使用场景中,key一般是索引值(如果是主键索引的话,那就是ID字段,如果是普通索引,那就是索引对应的字段值,如果是联合索引,可以简单理解为对应多个字段的拼接),value一般是指向行数据的指针(聚簇索引是这样)或者主键id(非聚簇索引);

结合该图,我们可以归纳出B树的规则:

  1. 节点容量:每个节点,都可以容纳多个键值对。
  2. 排序方式:所有节点键值对是按key递增次序排列,并遵循左小右大原则;
  3. 层级结构:所有叶子节点均在同一层
  4. 子树指针:节点中每个键值对的两侧,都可以放置指针(不一定都有值,可以是null),如果有值,则左边指向左子树(key都比当前key小),右边指向右子树(key都比当前key大)

B树种,每个节点最多可以容纳多少个键值对呢?这当然不可能是无限的,在数据结构的定义中,我们引入如下概念来描述:

  1. 度(degree),在树中,每个节点的子树个数就称为该节点的度。(注意是子树数量,而不是键值对的数量)
  2. 阶(order),在树中,一个节点可以拥有的最大子树数量称为阶。(注意是子树数量,而不是键值对的数量)

然而上述的规则,只能得到一个B树,却不一定得到一个平衡的B树,极端一点,下图这样的树,它也可以是个B树,但这显然不是我们想要的。

对于一个M阶的平衡的B树,除了上述的规则之外,我们还要加上如下的约束:

  1. 根节点至少有两颗子树
  2. 除根节点和叶子结点外,其他节点至少应该有m/2个子树。
  3. 每个节点的键值对数量k,应该m-1≥k≥ceil(m/2)-1。

ceil()是个朝正无穷方向取整的函数 如ceil(1.1)结果为2

如下图,就是一个5阶的平衡B树,4≥k≥2。

注意,每个节点中的键值对,value都是指向实际data的指针,像下图:

1.1 B树的查询

如上图我要从上图中找到E字母,查找流程如下

  1. 获取根节点的关键字进行比较,当前根节点关键字为M,E<M(26个字母顺序),所以往找到指向左边的子节点(二分法规则,左小右大,左边放小于当前节点值的子节点、右边放大于当前节点值的子节点);

  2. 拿到关键字D和G,D<E<G 所以直接找到D和G中间的节点;

  3. 拿到E和F,因为E=E 所以直接返回关键字和指针信息(如果树结构里面没有包含所要查找的节点则返回null);

1.2 B树的插入

一棵平衡的B树之所以能维持其平衡性,B树的插入和删除算法功不可没,我们先来看下B树如何应对记录的插入。

对于一个m阶的平衡的B树,从上文我们知道,需要保持其每个节点的键值对数量k为:m-1≥k≥ceil(m/2)-1,新的记录一般是插入在叶子节点上,为了保持这个数量和树的平衡性,我们规定:

  1. 还是按照key递增次序排列,遵循左小右大的原则,在叶子节点上找到新元素的定位。
  2. 若插入时,插入的节点元素个数小于m-1,则该元素直接插入。
  3. 否则,将该节点的元素分裂。

我们下面以5阶B树为例子,在5阶B树中,结点最多有4个键值对,最少有2个键值对。(下面我们把键值对称为元素)

  1. 插入树的第一批元素,A,C,G,N,因为数量不超过4,所以刚好能放在一个节点里面:
  2. 当试着插入H时,节点发现空间不够(4阶B树,一个节点最多放4个元素),以致将其分裂成2个节点,移动中间元素G上移到新的根节点中,比G元素小的A和C留在当前节点中,而比G元素大的H和N放置新的其右邻居节点中。如下图:
  3. 接下来插入E,K,Q,因为都不触及上界,所以不需要任何分裂操作
  4. 插入M(在K和N之间)就会导致一次分裂,注意M恰好是中间关键字元素,以致向上移到父节点中
  5. 插入F,W,L,T不需要任何分裂操作
  6. 插入Z时,最右的叶子节点空间满了,需要进行分裂操作,中间元素T上移到父节点中。
  7. 插入D时,导致最左边的叶子节点被分裂,D恰好也是中间元素,上移到父节点中,然后字母P,R,X,Y陆续插入不需要任何分裂操作
  8. 最后,当插入S时,含有[N,P,Q,R]的节点需要分裂,把中间元素Q上移到父节点中,但是情况来了,父节点中空间已经满了,无法加入Q了,所以也要进行分裂,将父节点中的中间元素M上移到新形成的根结点中。

1.3 B树的删除

B树的删除比插入要更复杂一些,但总体而言,为了保持树的平衡,还是有以下的原则:

分为如下几种情况:

  1. 要删除的记录d在叶子节点上

    • 1.1 如果该节点删除了该元素d后元素数量k仍然大于等于ceil(m/2)-1

      那么这种情况最简单,直接删除元素d和其对应的指针即可。这种情况,我们称为元素直删

    • 1.2 如果该节点删除了元素d后k小于ceil(m/2)-1,那么这时也分两种情况

      • 1.2.1 与该节点相邻的兄弟节点,有任一节点,其k大于等于ceil(m/2)。(这表示即便k-1,也仍然大于等于ceil(m/2)-1)

        那么这种情况,我们应该向兄弟节点,借一个元素过来,但不是简单的借,因为要保证B树元素从左到右递增的顺序,故而借法是有门道的,我们暂称为元素租借。

      • 1.2.2 与该节点相邻的兄弟节点,都没有多余的元素可以借出去,即其k都等于ceil(m/2)-1

        那么这种情况,删除元素d后,我们应该和兄弟节点合并,这样k1=ceil(m/2)-1-1和k2=ceil(m/2)-1,k1+k2还是小于m,符合B树的平衡约束(这也是为什么k的下限是ceil(m/2)-1的原因),这种情况,我们称为元素合并

  2. 要删除的记录d在非叶子节点上

    • 那么这种情况,我们应该在d指针指向的子树中找到一个元素f来替代d的位置,同时在子树中删除f(如果f的删除引起了m-1≥k≥ceil(m/2)-1的不满足,那么操作方法按照情况1:要删除的记录d在叶子节点上来处理),这种情况,我们称为元素顶替

总结之后,我们发现,删除记录的核心操作,就在元素直删,元素租借,元素合并和元素顶替这四个操作步骤之间,直删元素比较简单,我们不多说,剩下的,三种操作步骤,我们来理一下:

我们有一个5阶的b树,原始状态长这样:

  1. 元素顶替
    • 我们先反着来,先删除位于非叶子节点上的记录27
    • 这时候,需要从27的左右指针指向的两棵子树中找元素来置换27,左子树是23,24,26,右子树是28,29
    • 为了保证b树从左到右增序的顺序,所以有资格被置换的元素只有26和28。
    • b树删除的大部分实现,都是采用后继顶替优先原则,即27被删除,则拿27的后继28来顶替。
    • 将28元素替到27原来的位置,同时将右子节点中的28删去,如下图:
    • 28,29节点删去28之后,显然k小于了ceil(m/2)-1=2,这时候,29有两个兄弟节点:23,24,26和31,32
    • 如果29向23,24,26求援,就会引发元素租借的情况,反之,向31,32求援,就会引发元素合并的情况

其实向哪边求援都可以,实现不同,最终最多导致B树的形态会稍不一样,但肯定的是,他们都是平衡的树。

  1. 元素租借

    • 重申一下, 元素顶替操作结束后,B树长这样
    • 假如29向23,24,26求援,那么23,24,26明显有“余粮”,就会触发租借元素的情况。
    • 23,24,26不能直接将任意一个元素放到29中来,23,24,26任意元素都比28元素小,如果放置在28元素的右子节点上,就违背了左小右大的原则。
    • 那怎么借呢?既然两个当事人23,24,26和29分别是28元素的左右子节点,那就让28元素进入29,26元素替代28元素的位置,这样就皆大欢喜了。
  2. 元素合并

    • 重申一下, 元素顶替操作结束后,B树长这样
    • 假如29向31,32求援,那么31,32明显没有“余粮”,那么就会引发元素合并的情况。
    • 29和31,32不能直接合并,因为30元素还在父节点上,按照左小右大的原则,30元素一定要在29元素和31元素之间
    • 那怎么合并呢?既然两个当事人 29和31,32分别是30元素的左右子节点,那就让30元素与29和31,32一起加入合并,这样就皆大欢喜了。

这就结束了么?不是的,大家注意到没有,合并元素操作,其实相当于在父节点中删去了一个元素,如果这一次的删除,导致了父节点的元素数量小于ceil(m/2)-1怎么办?

我们来看这种情况,现在我们的原图长这样:

  1. 接着删除key为40的记录,删除后结果如下图所示。

  2. 删完后就剩39一个元素了,没话说,找个兄弟节点合并呗,合并结果如下,可以看到,对于原来的36,41节点来说,相当于36元素被删除了,导致41节点不符合约束。

  3. 这时候,其实有两种策略

    • 一种是采用元素顶替操作:删了我的36,那就拿39顶替呗。
    • 还有一种是向兄弟节点22,26求援,这时要根据兄弟节点的“余粮”情况,酌情触发元素合并或者元素租借。
    • 那到底是采用第一种方案好还是第二种方案好呢?
    • 答案是第二种,因为第二种情况,可能触发元素合并,只要触发元素合并,就有可能降低树的高度,使得B树不仅平衡,而且更加“矮胖”,使查找效率更高。

所以总结一句话,因为元素合并导致的父节点元素数量不符合约束,执行策略时,优先向可能触发元素合并的方向靠拢,有利于使树的高度降低。

1.4 B树的优点

如果经常访问的数据离根节点很近,而B树的非叶子节点本身存有关键字其数据的地址,所以这种数据检索的时候会要比B+树快。

2. B+树

B-Tree有许多变种,其中最常见的是B+Tree,例如MySQL就普遍使用B+Tree实现其索引结构。

B+树是B树的一个升级版,相对于B树来说B+树更充分的利用了节点的空间,让查询速度更加稳定,其速度完全接近于二分法查找。为什么说B+树查找的效率要比B树更高、更稳定;我们先看看两者的区别:

  1. B+树的非叶子节点不保存关键字记录的指针,只进行数据索引,这样使得B+树每个非叶子节点所能保存的关键字大大增加;
  2. B+树叶子节点保存了父节点的所有关键字记录的指针,所有数据地址必须要到叶子节点才能获取到。所以每次数据查询的次数都一样;
  3. B+树叶子节点的关键字从小到大有序排列,左边结尾数据都会保存右边节点开始数据的指针。

2.1 B+树的插入

B+树的插入操作和B树的插入操作大同小异,即都满足:

  1. 还是按照key递增次序排列,遵循左小右大的原则,在叶子节点上找到新元素的定位。
  2. 若插入时,插入的节点元素个数小于m-1,则该元素直接插入。
  3. 否则,将该节点的元素分裂。

但B+树的叶子节点,会包含所有的元素(不像B树,有些元素在叶子节点上,有些元素在非叶子节点上),所以插入操作有一些许的差异。

我们下面还是以5阶B+树为例子,在5阶B+树中,结点最多有4个元素,最少有2个元素。

  1. 空树中插入5:

  2. 依次插入8,10,15:

  3. 插入16,

    • 这时超过了关键字的个数限制,所以要进行分裂。在叶子结点分裂时,分裂出来的左节点2个记录,右节点3个记录,中间key成为索引结点中的key,分裂后当前节点指向了父节点(根节点)。结果如下图所示:
    • 当然我们还有另一种分裂方式,给左结点3个记录,右结点2个记录,此时索引结点中的key就变为15。不同实现而已,本质差不多。

  4. 继续插入17

  5. 插入18,插入后当前节点的关键字个数大于5,进行分裂。

    • 分裂成两个节点,左节点2个记录,右节点3个记录,关键字16进位到父节点(索引类型)中,将当前结点的指针指向父结点。
  6. 插入若干数据后:

  7. 在上图中插入7,结果如下图所示

    • 此时当前节点的关键字个数超过4,需要分裂。左节点2个记录,右节点3个记录。分裂后关键字7进入到父节点中,将当前结点的指针指向父节点
    • 当前结点的关键字个数超过4,需要继续分裂。左结点2个关键字,右结点2个关键字,关键字16进入到父结点中,将当前结点指向父结点,结果如下图所示:

2.2 B+树的删除

回顾上文的B树的删除,我们知道B树有元素直删,元素租借,元素合并和元素顶替这四个操作场景,B+树与B树大同小异,几乎没有区别。只不过B+树没有元素顶替

B+树没有元素顶替,是因为B+树的叶子节点,有所有的键值对信息,所以不存在删除的键值对不是叶子节点的情况。

我们下面还是以5阶B+树为例子,在5阶B+树中,结点最多有4个元素,最少有2个元素。

初始状态如下:

  1. 元素直删

    • 在上图基础上删除22
    • 删除后叶子结点中key的个数大于等于2,删除结束
  2. 元素租借

    • 在元素直删完了之后的基础上,再删除15,得到下图:
    • 删除后当前结点只有一个元素,不满足条件,而兄弟结点有三个元素(注意,当前节点的兄弟节点只有[7,8,9]),则可以向兄弟节点借一个元素过来。当然,根据排序原则,只能借9。
    • 9元素去到[10]节点之后,那么索引也要相应的更改,否则也不满足排序原则,即原来非叶子节点中的10改为9:
  3. 元素合并

    • 在元素租借完了之后,我们再删除7,删除后的结果如下图所示:
    • 可以看到,删除完了以后,当前节点元素个数小于2,且左右节点的元素数量都是2,即都没有富余的元素。
    • 这时候我们选择元素合并,可以选择和左兄弟合并,也可以和右兄弟合并,这里我们选择左兄弟。
    • 合并的时候我们前文说过,为了保证顺序,兄弟节点还会将父节点中对应的元素一起纳入合并,即[5,6]、[8]会和父节点中的7一起合并,不过这次删除的是7,所以7不存在了,故而,得到:
    • 不过注意,因为7索引的删除,导致了父节点只剩下了一个元素9,这显然不符合数量约束。
    • [9]的兄弟节点也没有余粮,则只能拉着父节点的元素16,和右兄弟[18,20]合并。得到下图:

其实说是没有元素顶替,但为了便于理解,也可以用元素顶替的思路来看最后这个删除操作:
1.因为删除的是7,7也在非叶子节点上,所以7删除了,要从子节点中找一个元素来顶替。
2.不论是8顶替上去还是6顶替上去,都会使得有个子节点数量不符合,触发元素合并。
3.这时候左右节点的元素+他们关联的父节点的元素合并,得到的结果,还是[5,6,8]

2.3 B+树的优点

  1. B+树的层级更少:相较于B树,B+每个非叶子节点存储的元素数更多(因为B+树的非叶子节点不存data,相同容量下可容纳的元素更多),使得B+树相对于B树更加“矮胖”,即B+树的层级更少所以查询数据更快;

  2. B+树查询速度更稳定:B+所有关键字数据地址都存在叶子节点上,所以每次查找的次数都相同所以查询速度要比B树更稳定。不像B树,有时候在非叶子节点上找到,那就相对快,有时候在叶子节点上找到,那就相对慢。

  3. B+树天然具备排序功能:B+树所有的叶子节点数据构成了一个有序链表,在查询大小区间的数据时候更方便,数据紧密性很高,缓存的命中率也会比B树高。

  4. B+树全节点遍历更快:B+树遍历整棵树只需要遍历所有的叶子节点即可,,而不需要像B树一样需要对每一层进行遍历,这有利于数据库做全表扫描。

2.4 为什么B+树比B树更适合做数据库索引?

其实理由也正是基于上诉的优势而言,不过我们把语义按照数据库索引适配性的角度转化一下:

  1. B+树的磁盘读写代价更低:B+树的内部节点并没有指向关键字具体信息的指针,因此其内部节点相对B树更小,如果把所有同一内部节点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多,一次性读入内存的需要查找的关键字也就越多,相对IO读写次数就降低了。

  2. B+树的查询效率更加稳定:由于非叶节点并不是最终指向文件内容的节点,而只是叶子结点中关键字的索引。所以任何关键字的查找必须走一条从根节点到叶子节点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。

  3. 对范围查询的适配性好:在数据库中基于范围的查询是非常频繁的,B树不支持这样的操作或者说效率太低。而B+树只需要去遍历叶子节点的链表,就可以实现整棵树的范围查询。

  4. 全节点遍历更快:由于B+树的数据都存储在叶子节点中,分支节点均为索引,方便扫库,只需要扫一遍叶子节点即可,但是B树因为其分支结点同样存储着数据,我们要找到具体的数据,需要进行一次中序遍历按序来扫,所以B+树更加适合在区间查询的情况,所以通常B+树用于数据库索引。

Redis的缓存雪崩/缓存穿透/缓存预热+布隆过滤器介绍

Posted on 2020-04-09 | In 中间件 , Redis |
Words count in article: 2.3k | Reading time ≈ 7

Redis作为当下最主流的数据缓存中间件,在实际的运作过程中,有如下几个场景需要特别注意:

1. 缓存雪崩

  • 有时:
    • 缓存集中过期失效。(例如:我们短时间设置大量数据的缓存时采用了相同的过期时间,就会在同一时刻出现大面积的缓存过期)
  • 由于
    • 大量的原本应该访问缓存的请求都因为缓存失效而降级到查询数据库了
  • 导致
    • 短时间内对数据库CPU和内存造成巨大压力,严重的会造成数据库宕机。从而形成一系列连锁反应,造成整个系统崩溃。
  • 解决办法
    • 加锁:大多数系统设计者考虑用加锁( 最多的解决方案)或者请求队列的方式来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。
    • 分散过期时间:还有一个简单方案就是将缓存失效时间分散开,比如加一个随机因子,使过期时间离散。

2. 缓存穿透

缓存穿透又叫缓存击穿

  • 有时:
    • 请求查询一个数据库不存在的数据
  • 由于
    • 我们查询数据库结果为空的时候,不会把这个空结果放入缓存
  • 导致
    • 每次查询一个不存在的数据,都不会命中缓存(因为这样的缓存不存在)。促使请求都会降级到数据库上,就像缓存被击穿了一样。
    • 假如有恶意攻击,就可以利用这个漏洞,对数据库造成压力,甚至压垮数据库。即便是采用UUID,也是很容易找到一个不存在的KEY,进行攻击。
  • 解决办法
    • 缓存空值:如果从数据库查询的对象为空,也放入缓存,只是设定的缓存过期时间较短,比如设置为60秒。
    • 布隆过滤器(Bloom Filter):推荐!将所有可能存在的key都插入到Bloom Filter中,这样肯定不存在的key就可以被Bloom Filter过滤。

2.1 布隆过滤器

布隆过滤器(Bloom Filter)由Burton Howard Bloom在1970年提出,是一种空间效率高的概率型数据结构。它专门用来快速判断一个元素是否在一个集合中。听起来是很稀松平常的需求,为什么要使用BF这种数据结构呢?

2.1.1 背景

回想一下,我们平常在检测集合中是否存在某元素时,都会采用比较的方法。考虑以下情况:

  • 如果集合用线性表存储,查找的时间复杂度为O(n)。
  • 如果用平衡BST(如AVL树、红黑树)存储,时间复杂度为O(logn)。
  • 如果用哈希表存储,并用链地址法与平衡BST解决哈希冲突(参考JDK8的HashMap实现方法),时间复杂度也要有O[log(n/m)],m为哈希分桶数。

总而言之,当集合中元素的数量极多时,不仅查找会变得很慢,而且占用的空间也会大到无法想象。Bloom Filter就是解决这个矛盾的利器。

2.1.2 原理

  • Bloom Filter是由一个长度为m的bit数组(bit array)与k个哈希函数(hash function)组成的数据结构。bit数组均初始化为0,所有哈希函数都可以分别把输入数据尽量均匀地散列。

    • 如下图:假设m=10,k=2,即有f1和f2两个哈希函数。

  • 当要插入一个元素时,将其数据分别输入k个哈希函数,产生k个哈希值。以哈希值作为位数组中的下标,将所有k个对应的比特置为1。

    • 如下图,假设输入的数据是N1,经过计算f1(N1)得到的数值得为2,f2(N1)得到的数值为5,则将数组下标为2和下表为5的位置置为1
  • 当要查询(即判断是否存在)一个元素时,同样将其数据输入哈希函数,然后检查对应的k个比特。如果有任意一个比特为0,表明该元素一定不在集合中。如果所有比特均为1,表明该集合有(较大的)可能性在集合中。

    • 假设这时候查询N存不存在,将其带入f1和f2,得到结果r1和r2,分别检查r1和r2下标对应的bit数组元素值,假设为b1和b2
      • b1和b2有任意一个比特为0,表明该元素一定不在集合中。
      • b1和b2均为1,表明该集合有(较大的)可能性在集合中。

为什么不是一定在集合中呢?因为一个比特被置为1有可能会受到其他元素的影响,这就是所谓“假阳性”(false positive)。相对地,“假阴性”(false negative)在BF中是绝不会出现的。

下图示出一个m=18, k=3的BF示例。集合中的x、y、z三个元素通过3个不同的哈希函数散列到位数组中。当查询元素w时,因为有一个比特为0,因此w不在该集合中。

2.1.3 优缺点

BF的优点是显而易见的:

  1. 不需要存储数据本身,只用比特表示,因此空间占用相对于传统方式有巨大的优势,并且能够保密数据;
  2. 时间效率也较高,插入和查询的时间复杂度均为O(k);
  3. 哈希函数之间相互独立,可以在硬件指令层面并行计算。

但是,它的缺点也同样明显:

  1. 存在假阳性的概率,不适用于任何要求100%准确率的情境;(缓存击穿就不需要100%过滤不存在的key,所以适合)
  2. 只能插入和查询元素,不能删除元素,这与产生假阳性的原因是相同的。我们可以简单地想到通过计数(即将一个比特扩展为计数值)来记录元素数,但仍然无法保证删除的元素一定在集合中。

所以,Bloom Filter在对查准度要求没有那么苛刻,而对时间、空间效率要求较高的场合非常合适,另外,由于它不存在假阴性问题,所以用作“不存在”逻辑的处理时有奇效。目前来看,他有如下三个使用场景:

  • 网页爬虫对URL的去重,避免爬取相同的URL地址
  • 反垃圾邮件,从数十亿个垃圾邮件列表中判断某邮箱是否垃圾邮箱(同理,垃圾短信)
  • 缓存穿透,将所有可能存在的数据缓存放到布隆过滤器中,当黑客访问不存在的缓存时迅速返回避免缓存及DB挂掉。

2.1.4 假阳性率的计算

假阳性是Bloom Filter最大的痛点,因此有必要权衡,比如计算一下假阳性的概率。为了简单一点,就假设我们的哈希函数选择位数组中的比特时,都是等概率的。当然在设计哈希函数时,也应该尽量满足均匀分布。

  • 在位数组长度m的BF中插入一个元素,它的其中一个哈希函数会将某个特定的比特置为1。因此,在插入元素后,该比特仍然为0的概率是:

  • 现有k个哈希函数,并插入n个元素,自然就可以得到该比特仍然为0的概率是:

  • 反过来讲,它已经被置为1的概率就是:

  • 也就是说,如果在插入n个元素后,我们用一个不在集合中的元素来检测,那么被误报为存在于集合中的概率(也就是所有哈希函数对应的比特都为1的概率)为:

  • 当n比较大时,根据重要极限公式,可以近似得出假阳性率:

所以,在哈希函数的个数k一定的情况下:

  • 位数组长度m越大,假阳性率越低;
  • 已插入元素的个数n越大,假阳性率越高。

有一些框架内已经内建了BF的实现,免去了自己实现的烦恼。比如Guava 27.0.1版本的源码,BF的具体逻辑位于com.google.common.hash.BloomFilter类中

3. 缓存预热

  • 定义
    • 缓存预热是一个比较常见的概念,即系统上线后,将相关的缓存数据直接加载到缓存系统。这样用户就可以直接查询事先被预热的缓存数据,避免在用户请求的时候,先查询数据库,然后再将数据缓存。
  • 解决思路
    1. 直接写个缓存刷新开关,上线时手工操作下;
    2. 数据量不大,可以在项目启动的时候自动进行加载;
    3. 定时刷新缓存;
1…345…7
cherish-ls

cherish-ls

纸上得来终觉浅

68 posts
27 categories
92 tags
GitHub
© 2021 cherish-ls | Site words total count: 457.5k
Powered by Hexo
|
Theme — NexT.Muse v5.1.4
访问人数 访问总量 次
0%