不为有趣之事,何遣有涯之生
不失其所者久,死而不亡者寿

DDD领域驱动战略篇(4)

hzqiuxm阅读(13261)

DDD领域驱动战略篇(4)

架构与代码模型

认识分层架构

分层架构是运用最为广泛的架构模式,几乎每个软件系统都需要通过层来隔离不同的关注点,以此应对不同需求的变化,使得这种变化可以独立进行

传统经典三层架构

DDD经典分层结构

  • 用户界面/展现层:负责向用户展现信息以及解释用户命令
  • 应用层:很薄的一层,用来协调应用的活动,它不包含业务逻辑,它不保留业务对象的状态,但它保有应用任务的进度状态
  • 领域层:本层包含关于领域的信息,这是业务软件的核心所在。在这里保留业务对象的状态,对业务对象和它们状态的持久化被委托给了基础设施层
  • 基础设施层 :本层作为其他层的支撑库存在。它提供了层间的通信,实现对业务对象的持久化,包含对用户界面层的支撑库等作用
分层的依据和原则
  • 基于关注点为不同的调用目的划分层次:以领域驱动设计的四层架构为例,之所以引入应用层就是为了给调用者提供完整的业务用例
  • 面对变化:针对不同的变化原因确定层次的边界,严禁层次之间互相干扰
  • 保证同一层的组件处于同一个抽象层次
层与层之间协作
  • 依赖倒置原则:我们要依赖不变或稳定的元素(类、模块或层),抽象不应该依赖于细节,细节应该依赖于抽象
  • 低层模块的细节实现可以独立变化,避免变化对高层模块产生污染
  • 在编译时,高层模块可以独立于低层模块单独存在
  • 对于高层模块而言,低层模块的实现是可替换的
  • 为了更好地解除高层对低层的依赖,我们往往需要将依赖倒置原则与依赖注入结合起来

分层架构的演化

整洁架构


该架构思想提出的模型并非传统的分层架构,而是类似于一个内核模式的内外层架构,由内及外分为四层

  • 层次越靠内的组件依赖的内容越少,处于核心的 Entities 没有任何依赖
  • 层次越靠内的组件与业务的关系越紧密,因而越不可能形成通用的框架
  • Entities 层封装了企业业务规则,准确地讲,它应该是一个面向业务的领域模型
  • Use Cases 层是打通内部业务与外部资源的一个通道,因而提供了输出端口(Output Port)与输入端口(Input Port),但它对外的接口展现的其实是应用逻辑,或者说是一个用例
  • Gateways、Controllers 与 Presenters 其本质都是适配器(Adapter),用于打通应用业务逻辑与外层的框架和驱动器,实现逻辑的适配以访问外部资源
  • 系统最外层包括框架和驱动器,负责对接外部资源,不属于系统(仅指限界上下文而言)开发的范畴,但选择这些框架和驱动器,是属于设计决策要考虑的内容。这一层的一些组件甚至与要设计的系统不处于同一个进程边界
六边型架构

六边形架构在满足整洁架构思想的同时,更关注于内层与外层以及与外部资源之间通信的本质:六边形架构通过内外两个六边形为系统建立了不同层次的边界。核心的内部六边形对应于领域驱动设计的应用层与领域层,外部六边形之外则为系统的外部资源,至于两个六边形之间的区域,均被 Cockburn 视为适配器(Adapter),并通过端口(Port)完成内外区域之间的通信与协作,故而六边形架构又被称为端口-适配器模式

下面是对六边型架构的一个落地实践示例:

六边形架构中,Gateways、Controllers 与 Presenters 统一看做是适配器
不同的命名代表的是对其职责认识上的不同,如果认为是“网关”,则将该组件的实现视为一种门面,内部负责多个对象之间的协作以及职责的委派;如果认为是“适配器”,则是为了解决内外协议(数据协议与服务接口)之间的不一致而进行的适配。

微服务架构

下图的逻辑边界代表了一个微服务

该架构图并未严格按照分层架构模式来约定各个组件的位置与职责,这是完全合理的设计
整幅图的架构其实蕴含了两个方向:自顶向下和由内至外

  • Resource(REST服务中资源层的意思) 组件就是我们通常定义的 Controller,对应于上下文映射中的开放主机服务
  • 当微服务需要调用外部服务,且外部服务由 HTTP 协议通信,就需要提供一个 HTTP Client 组件完成对外部服务的调用
  • 为了避免当前微服务对外部服务的强依赖,又或者对客户端的强依赖,需要引入 Gateways 来隔离,充当防腐层

领域驱动架构的演进

改良的DDD经典分层

将基础设施层奇怪地放在了整个架构的最上面

整个架构模型清晰地表达了领域层别无依赖的特质,但整个架构却容易给人以一种错乱感
这个架构模型仍然没有解决人们对分层架构的认知错误,例如它并没有很好地表达依赖倒置原则与依赖注入
这个架构模型将基础设施层放在了整个分层架构的最顶端,导致它依赖了用户展现层
那如何继续演进呢?避免领域模型出现贫血模型,保证领域模型的纯粹性

避免贫血,保证纯粹

经典分层架构图:

将数据访问层中的Java Beans这种没有任何业务行为的对象称之为“贫血对象”,基于这样的贫血对象进行领域建模,得到的模型则被称之为“贫血模型”
贫血模型开发起来比较简单,但不具备丰富表达能力,无法有效应对重用与变化,无法展现业务特性

改进后的架构图:

目前,前后端分离已经是流行趋势,逐渐的用户表现层将彻底分离出去,形成一个完全松耦合的前端层
不管前端的展现方式如何,它的设计思想是面向调用者,而非面向领域。因此,我们在讨论领域驱动设计时,通常不会将前端设计纳入到领域驱动设计的范围

前端演化后架构:

这个分层架构展现了“离经叛道”的一面,因为基础设施层在这里出现了两次,但同时也充分说明了基础设施层的命名存在不足。当我们提及基础设施(Infrastructure)时,总还是会想当然地将其视为最基础的层。同时,这个架构也凸显了分层架构在表现力方面的缺陷。

引入应用层架构:

领域驱动分层架构中的应用层其实是一个外观,不包含业务逻辑,但对外它却提供了一个一致的体现业务用例的接口。一般它和用户故事一一对应。

基础设施层本质

出现的二个基础设施层是不是有点奇怪?从依赖关系看,处于领域层下端的基础设施层是通过实现抽象 Repository 接口导致的。二个基础设施层其实体现出的是南北网关的概念,我们结合整洁架构与六边型架构,可以得到下面的表达公式:

Controllers + Gateways + Presenters = Adapters = Infrastructure Layer

分层架构是一种架构模式,遵循了“关注点分离”原则。因此,在针对不同限界上下文进行分层架构设计时,还需要结合当前限界上下文的特点进行设计,合理分层,保证结构的清晰和简单。

层次职责与协作案例

案例业务描述

电商系统的下订单场景,在买家提交订单时,除了与订单直接有关的业务之外,还需要执行以下操作:

  • 订单数据的持久化:OrderRepository 提供插入订单功能。它属于支撑订单提交业务的基础功能,但将订单持久化到数据库的实现 OrderMapper 并不属于该业务范畴
  • 发送通知邮件:NotificationService 提供通知服务。它属于支撑通知业务的基础功能,但邮件发送的实现 EmailSender 却不属于该业务范畴
  • 异步发送消息给仓储系统:提交订单成功后需要异步发送消息 OrderConfirmed 给仓储系统,这一通信模式是通过消息队列来完成的。EventBus 发送 OrderConfirmed 事件属于支撑订单提交成功的基础功能,但发送该事件到 RabbitMQ 消息队列的 RabbitEventBus 则不属于该业务范畴

基本的架构如下:

  • 北向网关:OrderController提供RESTAPI服务,属于基础设施层
  • 南向网关:OrderMapper、EmailSender 和 RabbitEventBus分别提供了订单持久化,邮件发送,事件总线具体实现
  • 持久化的抽象方法属于领域逻辑,放在领域层;事件和消息通知抽象不属于领域逻辑,放在应用层
  • 领域层:包含 PlaceOrderService、Order、Notification、OrderConfirmed 与抽象的 OrderRepository,封装了纯粹的业务逻辑,不掺杂任何与业务无关的技术实现
  • 应用层:包含 OrderAppService 以及抽象的 EventBus 与 NotificationService,提供对外体现业务价值的统一接口,同时还包含了基础设施功能的抽象接口
  • 基础设施层:包含 OrderMapper、RabbitEventBus 与 EmailSender,为业务实现提供对应的技术功能支撑,但真正的基础设施访问则委派给系统边界之外的外部框架或驱动器

问题:在实现过程,我们会碰到组装邮件和组装订单确认事件的内容,假设这两个功能在该业务场景下是属于领域层的,那应用层怎么获取领域层生成的对象呢?

方案一

应用层直接调用领域层的方法获取
类目录结构:

类图关系:

方案二

将“上层对下层的调用”改为“下层对上层的通知”,领域层通过事件通知的方式告知应用层,相当于应用层是订阅者,领域层是发布者
事件发布接口定义在领域层,具体实现在应用层(这里的前提是:发送邮件与异步发送通知属于应用逻辑的一部分)
领域服务在下订单完成后,需要分别发布 NotificationComposed 与 OrderConfirmed 事件,应用层在调用业务逻辑时要先注册订阅事件

类目录结构:

类图关系:

方案三

重新分配 NotificationService 与 EventBus,将这两个抽象接口放到单独的一个名为 interfaces 的包中,这个 interfaces 包既不属于应用层,又不属于领域层
通过这样的职责分配后,业务逻辑发生了转移,发送邮件与异步发送通知的调用不再放到应用服务 OrderAppService 中,而是封装到了 PlaceOrderService 领域服务。
这里的前提是:发送邮件与异步发送通知属于业务逻辑的一部分

类目录结构:

类图关系:

限界上下文与架构

  • 架构范围:限界上下文体现的是一个垂直的架构边界,主要针对后端架构层次的垂直切分;不仅仅作用于领域层和应用层,它是架构设计而非仅仅是领域设计的关键因素
限界上下文的通信边界

划分时,限界上下文之间是否为进程边界隔离,直接影响架构设计,进程内与进程间在如下方面存在迥然不同的处理方式:通信、消息的序列化、资源管理、事务与一致性处理、部署。除此之外,通信边界的不同还影响了系统对各个组件(服务)的重用方式与共享方式

  • 进程内通信边界:通过模块和命名空间来划分设计,即使处于同一个进程的边界,我们仍需重视代码模型的边界划分

越容易重用,越容易耦合,进程间通信我们依然可以适当采取防腐层来进行设计,如下图所示:

项目上下文中定义通知服务的接口 NotificationService,并由 NotificationClient 去实现这个接口,它们扮演的就是防腐层的作用
倘若在未来需要将通知上下文分离为进程间的通信边界,这种变动将只会影响到防腐层的实现,作为 NotificationService 服务的调用者,并不会受到这一变化的影响

  • 进程间通信:一个限界上下文通过分布式的通信方式调用另一个限界上下文的方法,有两种风格:1 数据共享架构;2 零共享架构

数据库共享架构:即多个限界上下文共享同一个数据库
数据库共享架构也可能是一种“反模式”。当两个分处不同限界上下文的服务需要操作同一张数据表(这张表被称之为“共享表”)时,就传递了一个信号,即我们的设计可能出现了错误

零共享架构:将两个限界上下文共享的外部资源彻底斩断
彻底分离的限界上下文变得小而专,使得我们可以很好地安排遵循 2PTs 规则的小团队去治理它
然而,这种架构的复杂度也不可低估,数据一致性是棘手的问题,运维与监控的复杂度也随之而剧增

限界上下文对架构的影响

在进行架构设计时,其中最主要的视图就是逻辑视图和物理视图,这两种视图都需要考虑限界上下文以及它们之间的协作关系
在考虑逻辑视图时,我们会为限界上下文履行的职责所吸引,同时又需得关注它们之间的协作,此时,就该物理视图粉墨登场了

对于跨进程边界进行协作的限界上下文,建议为其绘制上下文映射,并通过六边形架构来确定二者之间的通信端口与通信协议
分布式调用需要考虑各种方案原则选择:CAP 原则的约束,最终一致性(BASE),TCC(Try-Confirm-Cancel)模式等

限界上下文与六边型及微服务
  • 一个限界上下文就是一个六边形,限界上下文之间的通信通过六边形的端口进行
  • 一个微服务就是一个六边形,微服务之间的协作就是限界上下文之间的协作

限界上下文即微服务:我们可以利用领域驱动设计对限界上下文的定义,以及根据前述识别限界上下文的方法来设计微服务
微服务即限界上下文:运用微服务设计原则,可以进一步甄别限界上下文的边界是否合理,对限界上下文进行进一步的演化
微服务即六边形:深刻体会微服务的“零共享架构”,并通过六边形架构来表达微服务
限界上下文即六边形:运用上下文映射来进一步探索六边形架构的端口与适配器角色
六边形即限界上下文:通过六边形架构的端口确定限界上下文之间的集成关系

电商购物示例

用例如下:

根据用例可以会的六个限界上下文:Product Context,Basket Context,Order Context,Inventory Context,Payment Context,Notification Context
如果这六个限界上下文之间采用跨进程通信,实际上就是六个微服务
采用客户方供应商开发模式,得到如下上下文映射图:

命令:是一个动作,是一个要求其他服务完成某些操作的请求,它会改变系统的状态,命令会要求响应
查询:是一个请求,查看是否发生了什么事。重要的是,查询操作没有副作用,它们不会改变系统的状态
事件:既是事实又是触发器,用通知的方式向外部表明发生了某些事

如果采用事件协作机制,协作过程如下:

1 Basket Context 发布 OrderRequested 事件,Order Context 订阅该事件,然后执行提交订单的流程
2 Order Context 验证订单,并发布 InventoryRequested 事件,要求验证订单中购买商品的数量是否满足库存要求
3 Inventory Context 订阅此事件并对商品库存进行检查,倘若检查通过,则发布 AvailabilityValidated 事件
4 Order Context 侦听到 AvailabilityValidated 事件后,验证通过,发布 OrderValidated 事件从而发起支付流程
5 Payment Context 响应 OrderValidated 事件,在支付成功后发布 PaymentProcessed 事件
6 Order Context 订阅 PaymentProcessed 事件,确认支付完成进而发布 OrderConfirmed 事件
7 Basket Context、Notification Context 与 Shipment Context 上下文都将订阅该事件。Basket Context 会清除购物篮,Notification Context 会发起对买家和卖家的通知,而 Shipment Context 会发起配送流程,在交付商品给买家后,发布 ShipmentDelivered 事件并被 Order Context 订阅

事件皆以“过去时态”命名,味着它是过去发生的且不可变更的数据,代表了某种动作的发生,并以事件的形式留下了足迹

DDD的代码模型

理解了限界上下文和分层架构的本质,要确认系统的代码模型自然也就水到渠成,不同项目可以使用不同的代码模型,同一个项目中代码模型应作为架构规范共同遵守

遵循DDD代码模型

  • application:对应了领域驱动设计的应用层,主要内容为该限界上下文中所有的应用服务
  • interfaces:对 gateways 中除 persistence 之外的抽象,包括访问除数据库之外其他外部资源的抽象接口,以及访问第三方服务或其他限界上下文服务的抽象接口。从分层架构的角度讲,interfaces 应该属于应用层,但在实践时,往往会遭遇领域层需要访问这些抽象接口的情形,单独分离 出 interfaces,非常有必要
  • domain:对应了领域驱动设计的领域层,可以将 repositories 单独分出来,目的是为了更好地体现它在基础设施层扮演的与外部资源打交道的网关语义
  • repositories:代表了领域驱动设计中战术设计阶段的资源库,皆为抽象类型。如果该限界上下文的资源库并不复杂,可以将 repositories 合并到 domain 中
  • infrastructure:对应了领域驱动设计的基础设施层,其下可以视外部资源的集成需求划分不同的包。其中,controllers 相对特殊,它属于对客户端提供接口的北向网关,等同于上下文映射中“开放主机服务(OHS)”的概念。如果为了凸显它的重要性,可以将 controllers 提升到与 application、domain、gateways 同等层次。我之所以将其放在 gateways 之下,还是想体现它的网关本质。persistence 对应了 repositories 抽象,至于其余网关,对应的则是 interfaces 下的抽象,包括消息队列以及与其他限界上下文交互的客户端。例如,通过 http 通信的客户端。其中,client 包下的实现类与 interfaces 下的对应接口组合起来,等同于上下文映射中“防腐层(ACL)”的概念

每个模块或包都是单一职责的设计,扮演着不同的角色,有的对应了分层架构的层,有的代表了领域驱动设计的设计要素,有的则是为了保证架构的松散耦合

进程间通讯代码模型

限界上下文的边界是进程间通信,则意味着每个限界上下文就是一个单独的部署单元,此即微服务的意义
架构的设计需要“恰如其分”,在不同的微服务中,各自的领域逻辑复杂程度亦不尽相同,故而不必严格遵循领域驱动设计的规范

有三种不同的领域建模模式,包括事务脚本(Transaction Script)、表模块(Table Module)或领域模型(Domain Model)
不同建模模式代码结构肯定有所不同,DDD主要关注领域模型模式(选择了事务脚本,领域模型就不一定要规避贫血模型,依赖注入也就未必成为必选项)

订单上下文和通知上下文例子:

由于限界上下文之间采用进程间通信,因此在 Notification Context 中,提供开放主机服务是必须的。倘若 NotificationController 以 RESTful 服务实现,则在 Order Context 发起对 RESTful 服务的调用属于基础设施的内容,因而必须定义 NotificationService 接口来隔离这种实现机制,使其符合整洁架构思想

进程内通讯代码模型

注意如何在代码模型中体现限界上下文的边界,更关键的则是要考虑两个处于相同进程中的限界上下文彼此之间该如何协作
下面是一些主要的考量因素:

  • 简单:在下游限界上下文的领域层直接实例化上游限界上下文的领域类
  • 解耦:在下游限界上下文的领域层通过上游限界上下文的接口和依赖注入进行调用
  • 迁移:在下游限界上下文中定义一个防腐层,而非直接调用
  • 清晰:要保证领域层代码的纯粹性,应该避免在当前限界上下文中依赖不属于自己的代码模型

如果确有迁移可能,且架构师需要追求一种纯粹的清晰架构,可以考虑在 interface 中定义自己的服务接口,然后在 gateway/client 中提供一个适配器,在实现该接口的同时,调用上游限界上下文的服务,无论这个服务是领域服务还是应用服务,甚至也可以是领域层的领域对象

订单上下文和通知上下文例子:

与进程间通信的唯一区别在于:NotificationClient 不再通过跨进程调用的方式发起对 RESTful 服务的调用,即使在 Notification Context 中定义了这样的开放主机服务。如上图所示,NotificationClient 直接通过实例化的方式调用了 Notification Context 应用层的 NotificationAppService。这是在 Order Context 中,唯一与 Notification Context 产生了依赖的地方

如果两个或多个限界上下文还存在共同代码,只能说明一点:那就是我们之前识别的限界上下文有问题!一旦提炼或发现了这个隐藏的限界上下文,就应该将它单列出来,与其他限界上下文享受相同的待遇,即处于代码模型的相同层次,然后再通过 interfaces 与 gateways/client 下的相关类配合完成限界上下文之间的协作即可

代码模型的架构决策

如果我们将限界上下文视为微服务,则该系统的架构风格就是微服务架构风格;果我们将上下文协作模式抽象为发布/订阅事件,则该系统的架构风格就是事件驱动架构风格;如果在限界上下文层面将查询与命令分为两种不同的实现模式,则该系统的架构风格就是命令查询职责分离(CQRS)架构风格

帮助设计元素的粒度和层次的C4模型:

  • 系统上下文:是最高的抽象层次,代表了能够提供价值的东西。一个系统由多个独立的容器构成。
  • 容器:是指一个在其内部可以执行组件或驻留数据的东西。作为整个系统的一部分,容器通常是可执行文件,但未必是各自独立的进程。从容器的角度理解一个软件系统的关键在于,任何容器间的通信可能都需要一个远程接口。(等同微服务概念)
  • 组件:可以想象成一个或多个类组成的逻辑群组。组件通常由多个类在更高层次的约束下组合而成。
  • 类:在一个面向对象的世界里,类是软件系统的最小结构单元

代码模型中公共组件分为两种:1 具有业务价值的作为独立的限界上下文(如规则引擎、消息验证、分析算法);2 非领域一部分,作为公共的基础设施功能,以独立项目依赖包方式进行设计(文件读写、FTP 传输、Telnet 通信等)

一个典型微服务架构:

优先考虑设计成微服务的前提条件:

  • 实现经常变更,导致功能需要频繁升级
  • 采用了完全不一样的技术栈
  • 对高并发与低延迟敏感,需要单独进行水平扩展
  • 是一种端对端的垂直业务体现(即需要与外部环境或资源协作)

单体架构提供一些与业务无关的 RESTful 服务,如健康检查、监控等时,可以采用一个统一RESTful组个多个限界上下文,如下图所示:

支持多终端展示的用户前端方案:

图中为浏览器 UI 调用提供的 UI Layer,即 BFF,它实则是在服务器与浏览器之间增加了一个 Node.js 中间层。各层的职责如下表所示

BFF(Backends For Frontends,为前端提供的后端) 的设计并不在领域驱动战略设计的考虑范围之内

系统架构层面需要的基础设施层方案思考,例如对 Excel 或 CSV 文件的导入导出,消息的发布与订阅、Telnet 通信等。理想状态下,这些公共组件的调用应由属于限界上下文自己的基础设施实现调用。倘若它们被限界上下文的领域对象或应用服务直接调用(即绕开自身的基础设施层),则应该遵循整洁架构思想,在系统架构层引入 interfaces 包,为这些具体实现定义抽象接口。

一个单体架构代码模型示意图:

如果选择微服务架构风格,通常不需要建立一个大一统的代码模型,而是按照内聚的职责将这些职责分别封装到各自的限界上下文中,又或者定义为公共组件以二进制依赖的方式被微服务调用。这些公共组件应该各自构建为单独的包,保证重用和变化的粒度。

如果选择 CQRS 架构风格,就可以在限界上下文的代码模型中为 command 和 query 分别建立 module(领域驱动设计中的设计要素),使得它们的代码模型可以独自演化,毕竟命令和查询的领域模型是完全不同的。

基于质量因素的考虑,我们甚至可以为同一个领域的 command 和 query 各自建立专有的限界上下文。在 command 上下文中,除了增加了 command 类和 event 类以及对应的 handler 之外,遵循前面讲述的限界上下文代码模型,而 query 上下文的领域模型就可以简化,例如直接运用事务脚本或表模块模式。

DDD领域驱动战略篇(3)

hzqiuxm阅读(10271)

DDD领域驱动战略篇(3)

上下文映射

理解上下文映射

上下文映射是为了用来描述限界上下文之间的协作问题,上下文映射是一种设计手段包含:共享内核、防腐层、开放式主机等多种协作模式
上下文映射是用于将限界上下文边界变得更清晰的重要工具。所以当我们正在为一些限界上下文的边界划分而左右为难时,不妨先放一放,在定下初步的限界上下文后,通过绘制上下文映射来检验,或许会有意外收获。

限界上下文之间关系是有方向的:

U 代表上游,D 代表下游,上游限界上下文作为下游限界上下文的功能支撑

用例图中的包含用例或扩展用例或许是一个不错的判断上下文协作关系的切入点,选择从包含或扩展关系切入,既可能确定了职责分离的逻辑边界,又可以确定协作关系的方向,这就是用例对领域驱动设计的价值所在了
上下文映射可以分为两大类:团队协作模式和通信集成模式

上下文映射的团队协作

领域驱动设计根据团队协作的方式与紧密程度,定义了五种团队协作模式

合作关系

合作关系:两个限界上下文同生共死的关系,合作越多依赖越多,耦合越严重,甚至出现双向依赖
不好的示例:图中出现了双向依赖,循环依赖,可谓是貌合神离,并没有真正的分开

解决之道:1 干脆合在一起;2 尽量减少一个方向上多余依赖;3 抽取双向依赖循环依赖原因,建立单独限界上下文(共享内核)

新引入的 Metadata 成为了其余限界上下文的上游,解除了 DataEngine 对 ReportEngine 的依赖,同样解除了 EntityEngine 以及 ReportDesigner 对 ReportEngine 的依赖

共享内核

共享内核是解除不必要依赖实现重用的重要手段,共享内核仍然属于领域的一部分,分离出来的共享内核属于上游团队的职责,因而需要处理好它与下游团队的协作
共享内核往往被用来解决合作关系引入的问题
要特别注意:共享内核可能是多个限界上下文共同的上游,每次修改都可能牵一发而动全身

客户方与供应方

正常情况下,这是团队合作中最为常见的合作模式,体现的是上游(供应方)与下游(客户方)的合作关系
面对多个下游时,如何排定不同领域需求的优先级,如何针对不同的领域需求建立统一的抽象,都是上游团队需要考虑的问题
采用持续集成为上下游建立集成测试和API测试等自动化测试的构建与发布,可以更好的规避风险

遵奉者

由上游团队来决定是响应还是拒绝下游团队提出的请求时,所谓的“遵奉者”模式就产生了
领域驱动设计提出的“限界上下文”实践,影响的不仅仅是设计决策与技术实现,还与企业文化、组织结构直接有关
遵奉者还有一层意思是下游限界上下文对上游限界上下文模型的追随
做出遵奉模型决策的前提是需要明确这两个上下文的统一语言是否存在一致性

分离方式

分离方式的合作模式就是指两个限界上下文之间没有哪怕一丁点儿的丝毫关系,就是无关系
这种“无关系”仍然是一种关系,而且是一种最好的关系
这种没有关系的关系似乎无足轻重,其实不然,它对改进设计质量以及团队组织都有较大帮助。两个毫无交流与协作关系的团队看似冷漠无情,然而,正是这种“无情”才能促进它们独立发展,彼此不受影响

上下文映射的通讯集成

防腐层ACL

防腐层其实是设计思想“间接”的一种体现,引入一个中间层,有效隔离限界上下文之间耦合
防腐层经常扮演:适配器、调停者、外观等角色(设计模式中常见几种结构型模式)
防腐层往往属于下游限界上下文,用以隔绝上游限界上下文可能发生的变化

对付遗留系统时,防腐层可谓首选利刃

开放主机服务OHS

开放主机服务就是上游服务用来吸引更多下游调用者的诱饵
设计开放主机服务,就是定义公开服务的协议,包括通信的方式、传递消息的格式(协议)。同时,也可视为是一种承诺,保证开放的服务不会轻易做出变化
因为开放主机服务一般位于上游,对应多个下游,所以不建议为每个下游都做一个防腐层,实践时候防腐层一般在下游

发布订阅事件

采用发布/订阅事件的方式可以在解耦合方面走得更远
当确定了消息中间件后,发布方与订阅方唯一存在的耦合点就是事件,准确地说,是事件持有的数据
一个电商购物流程发布订阅事件流程:


事件的发布和订阅并不一定是异步的或跨进程的

辨别限界上下文协作关系

首先我们需要确定是否存在关系,然后再确定是何种关系,最后再基于变化导致的影响来确定是否需要引入防腐层、开放主机服务等模式

通信边界对协作的影响

限界上下文的通信边界分为进程内边界与进程间边界,这种通信边界会直接影响到我们对上下文映射模式的选择

这里存在一个设计决策,即引入开放主机服务与防腐层是否必要?这就需要设计者权衡变化、代码重用、架构简单性的优先级

协作即依赖

从依赖的角度看,这种协作关系是因为一方需要“知道”另一方的知识(领域行为,领域模型,数据)
不要在领域建模过程中过多纠缠建模的细节,选择一个恰好合理的模型即可
从建模到设计,再从设计到编码开发,其实是一个迭代的过程,倘若在实现时确实发现模型存在瑕疵,再回过头来修改即可

领域行为产生的依赖

针对领域行为产生的依赖,我们可以通过抽象接口来解耦,通过引入防腐层(ACL)解除了对促销上下文的直接依赖
示例:根据促销策略计算订单的总价(上下文协作设计)

  • 订单上下文分别去调用客户上下文和促销上下文,客户侧调用1次,促销侧调用2次


  • 订单只调用促销上下文,由促销上下文再调用客户上下文,促销侧调用了2次

  • 可以对促销上下文的调用进一步进行封装,只调用1次,上下文协作图不变

我们应尽可能遵循“最小知识法则”,在保证职责合理分配的前提下,产生协作的限界上下文越少越好

领域模型产生的依赖

我们可以使用延迟加载来判断领域对象是否合理
不好的设计示例:在客户领域对象中添加了订单属性,正确姿势应该通过调用订单上下文的服务来获取

public class Customer { public List<SaleOrder> saleOrders() { // ... } }

在跨限界上下文消费领域模型的场景有二种解决方式(订单查询商品信息):

  • 第一种:采用遵奉者模式,重用(订单重用商品的领域模型)
  • 第二种:定义符合自己的领域模型,分离(订单定义自己和商品有关的领域模型)

在两个不同的限界上下文中为相同或相似的领域概念分别建立独立的领域模型为常见做法

销售和售后对商品的关注点其实是不同的,销售可能需要了解客户的性别、年龄与职业,以便于他更好地制定推销策略,售后支持则不必关心这些信息,只需要客户的住址与联系方式

推荐采用分离的方式进行领域模型设计,但需要注意以下几点:

  • 数据可以仍然存(持久化)在一处,领域模型仅仅是内存中的对象
  • 数据按照不同的业务边界分散存储,但它们之间用相同的 Identity 来保持关联
  • 数据虽然出现了冗余,但是导致它们产生变化的原因却不相同
数据产生的依赖

倘若严格遵循领域驱动设计,通常不会产生这种数据库层面的依赖
出于性能或其他原因的考虑,CQRS 模式就存在一个限界上下文去访问属于另外一个限界上下文边界的数据
没有绝对的理由,我们不要轻易做出这种数据依赖的妥协
数据分析用的数据库应该和业务数据库分离,通过对业务数据库的采集提供给数据分析使用

DDD领域驱动战略篇(2)

hzqiuxm阅读(8115)

DDD领域驱动战略篇(2)

领域知识

软件开发团队的沟通与协作

  • 组建好项目团队的第一件事:先识别问题域,进而为团队提炼达成共识的领域知识
  • 我们需要把需求看成一颗种子,技术人员要和领域专家一起共用培育
  • 在先启阶段,与提炼领域知识相关的活动有:

  • 每个活动是有顺序关系的,从上到下
  • 迭代开发阶段,针对迭代生命周期和用户故事生命周期可以开展不同形式的沟通与协作

  • 领域专家:介绍和解释该迭代需要完成的用户故事,包括用户故事的业务逻辑与验收标准
  • 开发人员:使用工时卡对用户故事进行评估,预估故事耗时
  • 项目经理:每天了解当前的迭代进度,并与产品负责人一起基于当前进度和迭代目标确定是否需要调整需求的优先级
  • 测试人员:迭代完成后需要召开演示会议进行功能演示,可以邀请实际客户参与
  • 用户故事指导着开发人员的开发、测试人员的测试,它是构成领域知识的最基本单元
  • 敏捷开发实践强调业务分析人员与测试人员共同编写验收测试的自动化测试脚本
  • 时刻牢记未经过测试用户故事价值为0

运用场景分析提炼领域知识

领域场景分析方法6W模型
  • 一种生动的方式是通过“场景”来展现领域逻辑
  • 组成场景的要素常常被称之为 6W 模型,即描写场景的过程必须包含 Who、What、Why、Where、When 与 How 这六个要素

首先需要识别参与该场景的用户角色(Who),通过分析该用户的特征与属性来辨别该角色在整个场景中参与的活动
这意味着我们需要明确业务功能(What),思考这一功能给该角色能够带来什么样的业务价值(Why)

  • 领域功能划分为三个层次,即业务价值(Why)、业务功能(What)和业务实现(How)
领域场景分析方法
  • 用例:每个用例都是系统中一个完整序列的事件(下图是一个下订单的用例示例)

转换成图形:

  • 用户故事:敏捷开发中的需求功能点,每次迭代由不同的用户故事组成,一个用户故事就是一个独立的业务场景
    用户故事的典型模板:

    不合格的用户故事:作为一名用户,我希望可以提供查询功能,以便于了解分配我的任务情况
    合格的用户故事:作为一名普通项目成员,我希望获取分配给自己的未完成任务,以便于跟踪自己的工作进度

  • 需求分析人员与测试人员结对编写用户故事,一个完整的用户故事必须是可测试的,有验收标准的
    注意:用户故事应该只受到业务规则与业务流程变化的影响,不要考虑任何UI操作

  • 测试驱动开发:测试先行,先写测试用例,然后将一个个测试用例跑通

建立统一语言

  • 获得统一语言就是需求分析的过程,也是团队中各个角色就系统目标、范围与具体功能达成一致的过程
  • 统一语言的体现:统一的领域术语,统一的领域行为描述
  • 在维护领域术语表时,一定需要给出对应的英文术语,否则可能直接影响到代码实现
  • 领域行为是对业务过程的描述,相对于领域术语而言,它体现了更加完整的业务需求以及复杂的业务规则
  • 领域行为注意点:强调动词的精确性,符合业务动作在该领域的合理性;要突出与领域行为有关的领域概念

磨刀不误砍柴工,多花一些时间去打磨统一语言,并非时间的浪费,相反还能改进领域模型乃至编码实现的质量,反过来,领域模型与实现的代码又能避免统一语言的“腐化”,保持语言的常新。重视统一语言,就能促成彼此正面影响的良性循环;否则领域模型与代码会因为沟通不明而泥足深陷,就真是得不偿失了。

限界上下文

理解限界上下文

限界上下文定义

什么是限界上下文(Bounded Context)?让我们来读一个句子:wǒ yǒu kuài dì
到底是:我有快递还是我有块地?如果没有说话的语境上下文,我们确定不了!当我们在理解系统的领域需求时,同样需要借助这样的上下文,而限界上下文的含义就是用一个清晰可见的边界(Bounded)将这个上下文勾勒出来,如此就能在自己的边界内维持领域模型的一致性与完整性。

在实际的业务场景中,不同的上下文,同一个人扮演的角色可能是不同的,如下图所示:

理解限界上下文时,要注意的几个关键点:

  • 知识:不同上下文领域知识不同,如果在一个上下文里某活动不具备对应知识,则活动分配不合理
  • 角色:深入思考参与到一个上下文中对象到底是什么角色,以及角色之间是如何协作的
  • 边界:按照不同关注点进行划分,越是关系弱,越是要划定边界
限界上下文价值
  • 领域逻辑层面:限界上下文确定了领域模型的业务边界,维护了模型的完整性与一致性,从而降低系统的业务复杂度
  • 团队合作层面:限界上下文确定了开发团队的工作边界,建立了团队之间的合作模式,避免团队之间的沟通变得混乱,从而降低系统的管理复杂度
  • 技术实现层面:限界上下文确定了系统架构的应用边界,保证了系统层和上下文领域层各自的一致性,建立了上下文之间的集成方式,从而降低系统的技术复杂度
  • 限界上下文本质:并不是像大多数程序员理解的那样,是模块、服务、组件或者子系统,而是你对领域模型、团队合作以及技术风险的控制
  • 醍醐灌顶:限界上下文目的不仅仅是为了划分边界,更是为了如何控制边界

限界上下文是“分而治之”架构原则的体现,我们引入它的目的其实为了控制(应对)软件的复杂度,它并非某种固定的设计单元,它可以成为系统、模块、服务或组件
理解限界上下文的三种境界:

  • 1 参悟之初:模块、服务或组件就是限界上下文(看山是山,看水是水)
  • 2 当有悟时:模块、服务或组件不是限界上下文(看山不是山,看水不是水)
  • 3 彻底悟透:模块、服务或组件仍然是限界上下文(看山仍然山,看水仍然是水)
限界上下文自治特点

  • 最小完备:根据业务价值的完整性进行设计,无需针对自己的信息去求助别的自治单元,这就避免了不必要的依赖关系
  • 稳定空间:减少外界变化对限界上下文内部的影响,符合OCP原则
  • 自我履行:自治单元自身决定要做什么,履行的职责一定是你掌握的知识范畴之内
  • 独立进化:减少限界上下文的变化对外界的影响,需要接口设计良好,符合标准规范,并在版本上考虑了兼容与演化

这四个要素又是高内聚低耦合思想的体现。我们需要根据业务关注点和技术关注点,尽可能将强相关性的内容放到同一个限界上下文中,同时降低限界上下文之间的耦合。对于整个系统架构而言,不同的限界上下文可以采用不同的架构风格与技术决策,而在每个限界上下文内部保持自己的技术独立性与一致性。

限界上下文控制力

限界上下文分离了业务边界

一个不好的示例:产品领域对象被多个子领域使用,但是每个子领域关心产品领域中的属性和行为却不相同,这将导致领域模型和数据模型耦合,违背了SRP原则,产品类变成了一个上帝类

正确的示例:虽然不同的限界上下文都存在相同的 Product 领域模型,但由于有了限界上下文作为边界,使得我们在理解领域模型时,是基于当前所在的上下文作为概念语境的。这样的设计既保证了限界上下文之间的松散耦合,又能够维持限界上下文各自领域模型的一致性,此时的限界上下文成为了保障领域模型不受污染的边界屏障。

限界上下文明确了工作边界

根据亚马逊公司提出的2PTs 规则,团队成员人数控制在 7~10 人左右比较合适

2PTs 规则自有其科学依据。如果我们将人与人之间的沟通视为一个“联结(link)”,则联结的数量遵守如下公式,其中 n 为团队的人数:

N(link) = n*(n-1)/2

联结的数量直接决定了沟通的成本,以 6 人团队来计算,联结的数量为 15;12人团队,则联结数陡增至 66;50人团队,联结数竟然达到了惊人的 1225

矩阵式组织结构,特性(项目)团队和组件(职能)团队

我们按照领域特性来组建团队,使得团队成员之间的沟通更加顺畅,至少针对一个领域而言,知识在整个特性团队都是共享的。二者的结合可以取长补短。

限界上下文封装了应用边界

从控制技术复杂度的角度来考虑技术实现,从而做出对系统质量属性的响应与承诺,通常体现在如下几个方面:

  • 高并发(临时性流量高峰的场景)
  • 功能重用(用户权限管理之类)
  • 实时性(大数据下的更新)
  • 第三方服务集成(支付,安全)
  • 遗留系统(整体作为一个上下文)

识别限界上下文

限界上下文的识别并不是一蹴而就的,需要演化和迭代。通过从业务边界到工作边界再到应用边界这三个层次抽丝剥茧,分别以不同的视角、不同的角色协作来运用对应的设计原则,会是一个可行的识别限界上下文的过程方法。

从业务边界识别
  • 对业务流程进行梳理,划分出业务场景
  • 从业务场景中识别业务活动(动宾结构)
  • 将业务活动根据语义相关性(包含同一个领域对象)和功能相关性(依赖关联关系)进行归类
  • 将识别出来的归类进行业务边界命名,命名的难易也是有效判断划分准确与否的一个标准
从工作边界识别
  • 根据团队人数,一般一个团队至多负责一个上下文
  • 根据预估工作量,当一个限界上下文工作量过大,就要考虑拆分了
  • “任劳任怨”的好团队也不是真正的好团队,边界内的要积极,边界外的要“抱有成见”
从应用边界识别
  • 关注系统的质量属性,不同质量属性要求的可以考虑划分成不同的限界上下文
  • 考虑重用和变化,可重用的部分作为独立的上下文,不同变化维度的划分为不同限界上下文

DDD领域驱动战略篇(1)

hzqiuxm阅读(9184)

DDD领域驱动战略篇(1)

前言

  • DDD可不是什么新玩意,它已经诞生十几年了,只是因为微服务流行的契机,焕发了第二春
  • DDD是一套软件工程方法(一种设计思想、一种开放的设计方法体系),微服务只是一种架构风格,二者关系主要体现在限界上下文
  • 推行DDD难的原因:1 技能门槛高;2 不经过时间推移无法彰显价值
  • DDD发展可能趋势:1 以 DDD 设计方法为基础的框架的出现,让微服务设计与领域建模变得更加容易,降低领域驱动设计的门槛;2 以函数式编程思想为基础的领域建模理念与事件驱动架构和响应式编程的结合,可能在低延迟高并发的项目中发挥作用
  • 更好进行领域驱动设计的前提就是解决团队成员协作与沟通
  • 树立属于自己的技术标签(DDD纳入个人标签规划)
  • 领域驱动设计能够带来的收获:1 使得你的设计思路能够更加清晰,设计过程更加规范;2 为你的产品建立一个核心而稳定的领域模型内核,有利于领域知识的传递与传承;3 与领域专家的合作,能够帮助团队建立一个沟通良好的团队组织,构建一致的架构体系; 4 善于处理系统架构的演进设计; 5 有助于提高团队成员的面向对象设计能力与架构设计能力; 领域驱动设计与微服务架构天生匹配,微服务可以遵循领域驱动设计的架构原则
  • DDD学习之路会很崎岖,但是怕什么真理无穷,进一寸就有一寸的欢喜

软件复杂度

领域驱动设计概览

  • 领域驱动设计是一套方法论,具有一定的开放性,可以使用用例(Use Case)、测试驱动开发(TDD)、用户故事(User Story)来帮助我们对领域建立模型;可以引入整洁架构思想及六边形架构,以帮助我们建立一个层次分明、结构清晰的系统架构;还可以引入函数式编程思想,利用纯函数与抽象代数结构的不变性以及函数的组合性来表达领域模型。
  • 领域驱动设计贯穿了整个软件开发的生命周期,包括对需求的分析、建模、架构、设计,甚至最终的编码实现,乃至对编码的测试与重构。

  • 战略设计阶段:1 确认问题域(限界上下文,核心域,子域,通用域);2 通过多种分层架构(六边型、微服务、整洁、CQRS等)隔离关注点(多种架构的原因是因为限界上下文之间物理边界分开后,架构就是针对具体某个限界上下文系统了)
  • 战术设计阶段:应对领域复杂性,识别出主要要素:值对象、实体、领域服务、领域事件、资源库、工厂、聚合、应用服务

  • 领域驱动演进的设计过程:战略设计会控制和分解战术设计的边界与粒度,战术设计则以实证角度验证领域模型的有效性、完整性与一致性,进而以演进的方式对之前的战略设计阶段进行迭代,从而形成一种螺旋式上升的迭代设计过程

深入分析软件复杂度

  • 复杂没有一个公认明确的定义,我们先从理解力(分为 Simple 与 Comlicated 两个层次)和预测能力(分为 Ordered、Complex 与 Chaotic混沌 三个层次)两个维度来分析

  • 软件系统的所谓“复杂”其实覆盖了 Complicated 与 Complex 两个方面
  • 影响理解力的因素:1 规模;2 结构;
  • 影响预测能力的因素:变化
  • 优雅的设计和拙劣的设计都会使结构变得复杂,前者是主动控制,后者是不可控制错误滋生,是技术债
  • 变化可能来自业务需求(业务和用户活动本身复杂),也可能来自质量属性(非功能性需求)
  • 软件设计者们就像走在高空钢缆的技巧挑战者,惊险地调整重心以维持行动的平衡。故而,变化之难,在于如何平衡

控制软件复杂度原则

  • 当然也是从模块、结构、变化三个主要因素去控制
  • 手段一:分而治之,控制规模

    Kiss原则,单一职责原则

  • 手段二:保持结构的清晰和一致

    整洁架构

  • 手段三:拥抱变化

    敏捷,快速迭代,可进化、可扩展、可定制

软件需求复杂性

  • 需求复杂度可以分为:技术复杂度(安全、高性能、高并发、高可用性等)和业务复杂度
  • 技术复杂度与业务复杂度并非完全独立,二者混合在一起产生的化合作用更让系统的复杂度变得不可预期,难以掌控
  • 二者变化维度、周期也不同,再加上团队规模和人员流动等因素,加剧了架构腐化和系统复杂性

  • 面临的典型问题:1 问题域庞大复杂,难以寻求解决方案(规模上问题);2 开发人员将业务逻辑的复杂度与技术实现的复杂度混淆在一起(结构上问题);3 随着需求的增长和变化,无法控制业务复杂度和技术复杂度(变化上问题)

DDD如何应对软件复杂性

隔离业务复杂度和技术复杂度
  • 确认业务逻辑和技术实现的边界,从而隔离各自的复杂度
  • 理想情况下,业务规则和技术实现应该是正交的
  • DDD通过分层架构和六边型架构来确保业务逻辑和技术实现的分离

  • 分层架构关注点分离原则:将业务逻辑关注点放在领域层,技术实现放在基础设施层

蓝色区域的内容与业务逻辑有关,灰色区域的内容与技术实现有关,二者泾渭分明,然后汇合在应用层。应用层确定了业务逻辑与技术实现的边界,通过直接依赖或者依赖注入(DI,Dependency Injection)的方式将二者结合起来

  • 六边型架构的内外分离:内部领域层为核心,技术实现以周边适配器方式出现

如果我们在领域层或应用层抽象了技术实现的接口,再通过依赖注入将控制的方向倒转,业务内核就会变得更加的稳定,不会因为技术选型或其他决策的变化而导致领域代码的修改。

数据库和缓存隔离访问例子:

注意:缓存抽象是放在应用层,实现放在基础设施层

限界上下文分而治之

上一小节中,缓存接口放在了应用层,从层次的职责来看,这样的设计是合理的,但它却使得系统的应用层变得更加臃肿,职责也变得不够单一了。这是分层架构与六边形架构的局限所在,因为这两种架构模式仅仅体现了软件系统的逻辑划分。

如果我们将缓存作为一个独立的上下文,它拥有自己的应用层、领域层、基础设置层,那么我们就可以将庞大的问题域划分为松散的耦合的多个小系统,即不同的限界上下文

XXXX外贸系统的研发中心系统问题域划分示例:

领域模型对领域知识的抽象
  • 领域模型是对业务需求的一种抽象,其表达了领域概念、领域规则以及领域概念之间的关系
  • 模型是封装,实现了对业务细节的隐藏;模型是抽象,提取了领域知识的共同特征,保留了面对变化时能够良好扩展的可能性

某项目管理系统领域模型建模示例:

Docker简明教程(12)

hzqiuxm阅读(3034)

基于vagrant和virtualbox虚拟机搭建docker环境

环境部分

vagrant下载和安装

  • 01 访问Vagrant官网:https://www.vagrantup.com/
  • 02 点击Download:Windows,MacOS,Linux等
  • 03 选择对应的版本
  • 04 傻瓜式安装
  • 05 命令行输入vagrant,测试是否安装成功

virtual box下载安装

  • 01 访问VirtualBox官网:https://www.virtualbox.org/
  • 02 选择左侧的“Downloads”
  • 03 选择对应的操作系统版本
  • 04 傻瓜式安装
  • 05 [win10中若出现]安装virtualbox快完成时立即回滚,并提示安装出现严重错误
    (1)打开服务
    (2)找到Device Install Service和Device Setup Manager,然后启动
    (3)再次尝试安装

CentOS安装

  • 01 创建centos7文件夹,并进入其中[目录全路径不要有中文字符]
  • 02 在此目录下打开cmd,运行vagrant init centos/7
    此时会在当前目录下生成Vagrantfile,同时指定使用的镜像为centos/7,关键是这个镜像在哪里,我已经提前准备好了,名称是virtualbox.box文件。大家可以前往百度网盘https://pan.baidu.com/s/1cIlUnjleWSDxVLx9cjsgxA 进行下载

  • 03 将virtualbox.box文件添加到vagrant管理的镜像中
    (1)下载网盘中的virtualbox.box文件
    (2)保存到磁盘的某个目录,比如D:\vm\virtualbox.box
    (3)添加镜像并起名叫centos/7(唯一):vagrant box add centos/7 D:\vm\virtualbox.box
    (4)vagrant box list 查看本地的box[这时候可以看到centos/7]

  • 04 centos/7镜像有了,根据Vagrantfile文件启动创建虚拟机
    来到centos7文件夹,在此目录打开cmd窗口,执行vagrant up[打开virtual box观察,可以发现centos7创建成功]

  • 05 以后大家操作虚拟机,还是要在centos文件夹打开cmd窗口操作
    vagrant halt 优雅关闭
    vagrant up 正常启动

  • 06 vagrant常用命令
    (1)vagrant ssh
    进入刚才创建的centos7中
    (2)vagrant status
    查看centos7的状态
    (3)vagrant halt
    停止/关闭centos7
    (4)vagrant destroy
    删除centos7
    (5)vagrant status
    查看当前vagrant创建的虚拟机
    (6)Vagrantfile中也可以写脚本命令,使得centos7更加丰富
    但是要注意,修改了Vagrantfile,要想使正常运行的centos7生效,必须使用vagrant reload

至此,使用vagrant+virtualbox搭建centos7完成,后面可以修改Vagrantfile对虚拟机进行相应配置

Vagrantfile 配置示例

# -*- mode: ruby -*-
# vi: set ft=ruby :

# All Vagrant configuration is done below. The "2" in Vagrant.configure
# configures the configuration version (we support older styles for
# backwards compatibility). Please don't change it unless you know what
# you're doing.
Vagrant.configure("2") do |config|
  # The most common configuration options are documented and commented below.
  # For a complete reference, please see the online documentation at
  # https://docs.vagrantup.com.

  # Every Vagrant development environment requires a box. You can search for
  # boxes at https://vagrantcloud.com/search.
  config.vm.box = "centos/7"

  # Disable automatic box update checking. If you disable this, then
  # boxes will only be checked for updates when the user runs
  # `vagrant box outdated`. This is not recommended.
  # config.vm.box_check_update = false

  # Create a forwarded port mapping which allows access to a specific port
  # within the machine from a port on the host machine. In the example below,
  # accessing "localhost:8080" will access port 80 on the guest machine.
  # NOTE: This will enable public access to the opened port
  # config.vm.network "forwarded_port", guest: 80, host: 8080

  # Create a forwarded port mapping which allows access to a specific port
  # within the machine from a port on the host machine and only allow access
  # via 127.0.0.1 to disable public access
  # config.vm.network "forwarded_port", guest: 80, host: 8080, host_ip: "127.0.0.1"

  # Create a private network, which allows host-only access to the machine
  # using a specific IP.
  # config.vm.network "private_network", ip: "192.168.33.10"

  # Create a public network, which generally matched to bridged network.
  # Bridged networks make the machine appear as another physical device on
  # your network.
  config.vm.network "public_network"

  # Share an additional folder to the guest VM. The first argument is
  # the path on the host to the actual folder. The second argument is
  # the path on the guest to mount the folder. And the optional third
  # argument is a set of non-required options.
  # config.vm.synced_folder "../data", "/vagrant_data"

  # Provider-specific configuration so you can fine-tune various
  # backing providers for Vagrant. These expose provider-specific options.
  # Example for VirtualBox:
  #
  # config.vm.provider "virtualbox" do |vb|
  # # Display the VirtualBox GUI when booting the machine
  # vb.gui = true
  #
  # # Customize the amount of memory on the VM:
  # vb.memory = "1024"
  # end
     config.vm.provider "virtualbox" do |vb|
        vb.memory = "4000"
        vb.name= "first-centos7"
        vb.cpus= 2
    end
  #
  # View the documentation for the provider you are using for more
  # information on available options.

  # Enable provisioning with a shell script. Additional provisioners such as
  # Puppet, Chef, Ansible, Salt, and Docker are also available. Please see the
  # documentation for more information about their specific syntax and use.
  # config.vm.provision "shell", inline: <<-SHELL
  # apt-get update
  # apt-get install -y apache2
  # SHELL
end

虚拟机访问配置

  • 01 使用centos7的默认账号连接
    在centos文件夹下执行vagrant ssh-config
    关注:Hostname Port IdentityFile
    IP:127.0.0.1
    port:2222
    用户名:vagrant
    密码:vagrant
    文件:Identityfile指向的文件private-key

  • 02 使用root账户登录(推荐)
    vagrant ssh 进入到虚拟机中
    sudo -i
    vi /etc/ssh/sshd_config
    修改PasswordAuthentication yes
    passwd修改密码,比如abc123
    systemctl restart sshd
    使用账号root,密码abc123进行登录

至此,可以使用你熟悉的ssh工具来进行访问,推荐几款常用的工具:xshell,Terminus,securtCRT

box的打包分发()

  • 01 退出虚拟机:vagrant halt

  • 02 打包:vagrant package --output first-docker-centos7.box

  • 03 得到first-docker-centos7.box

  • 04 将first-docker-centos7.box添加到其他的vagrant环境中
    vagrant box add first-docker-centos7 first-docker-centos7.box

  • 05 得到Vagrantfile:vagrant init first-docker-centos7

  • 06 根据Vagrantfile启动虚拟机
    vagrant up [此时可以得到和之前一模一样的环境,但是网络要重新配置]

docker部分

docker基本安装

  • 01 用你喜欢的方式,连接进入centos7
  • 02 卸载之前的docker
sudo yum remove docker \
                  docker-client \
                  docker-client-latest \
                  docker-common \
                  docker-latest \
                  docker-latest-logrotate \
                  docker-logrotate \
                  docker-engine
  • 03 安装必要的依赖
sudo yum install -y yum-utils \
    device-mapper-persistent-data \
    lvm2
  • 04 设置docker仓库
 sudo yum-config-manager \
      --add-repo \
      https://download.docker.com/linux/centos/docker-ce.repo

建议添加一个加速器:访问这个地址,使用自己的阿里云账号登录,查看菜单栏左下角,发现有一个镜像加速器:https://cr.console.aliyun.com/cn-hangzhou/instances/mirrors

例如我的如下:

sudo tee /etc/docker/daemon.json <<-'EOF' { "registry-mirrors": ["https://yzcs6yua.mirror.aliyuncs.com"] } EOF

  • 05 安装docker
 sudo yum install -y docker-ce docker-ce-cli containerd.io
  • 06 启动docker : sudo systemctl start docker
  • 07 设置开机启动: sudo systemctl enable docker
  • 08 测试docker安装是否成功 sudo docker run hello-world

能看到打印出hello from Docker!就表示成功了

具体详情可以参考官网介绍:https://docs.docker.com/install/linux/docker-ce/centos/

技术管理系列

hzqiuxm阅读(5587)

研发效能破局之道

研发效能综述

研发效能模型与理解

  • 简单来说,就是开发者是否能够长期既快又准地产生用户价值。
  • 包括有效性(Effectiveness)、效率(Efficiency)和可持续性(Sustainability)三方面
  • 优秀效能例子:Facebook 在 2012 年达到 10 亿月活的时候,部署人员只有 3 个
  • 提升研发效能使得开发者能够聚焦产出价值,更容易精进自己的技术,从而形成良性循环
  • 对于提升研发效能努力:最初的瀑布研发流程到敏捷到精益,从持续集成到持续发布到持续部署,从实体机到虚拟机到 Docker,从本地机器到数据中心再到云上部署,从单体应用到微服务再到无服务应用
  • 初创公司的普遍坑:盲目使用新技术:比如微服务
  • 不阻塞开发人员几点小建议:1 本地构建脚本要快;2 好的商用软件要买;
  • 研发效能模型:

1 优化流程
2 提升团队工程师实践
3 提升个人工程实践
4 通过管理和文化打造可持续学习型组织

效能度量困难

  • 一个事物,你如果无法度量它,就无法管理它;我们先要有量化标准(这也是欧洲制造高品质原因)
  • 效能度量的错误案例:1 全公司范围内推行一套效能度量指标(没有考虑不同团队不同特点);2 中型公司推行质量方面指标(只从QA角度量化);3 创业公司聚焦度量开发、测试、上线准确度指标(不能牺牲产品试错和快速迭代为代价);
  • 研发效能度量很难,首要原因:研发过程本身是一项创造性很强的知识性工作,非常复杂且伴随有大量不确定因素
  • 一个复杂的系统,如果过于关注某几个参数,那么度量过程很可能会沦为数字游戏

  • 研发效能度量很难的第二个原因:竖井效应(只考虑局部最优,使得在整个工程中,存在多处等待情况)

  • 研发效能度量很难的第三个原因:技术产品输出与最终用户价值之间沟壑很难打通

效能度量指标选择

  • 效能指标分类:1 速度;2 准确度;3 质量;4 个人效能;
  • 速度:主要是衡量团队研发产品的速率,从任务产生到交付
  • 准确度:需求和用户价值是否吻合;比如某个功能用户使用率
  • 质量:包含性能、功能、可靠性、安全等方面
  • 个人效能:开发环境构建速度、本地构建速度等

四类指标的参考项如下:

  • 只需要选取适合自己的就行,千万别面面俱到
  • 效能度量的原则:效能度量不要和绩效挂钩,只是作为参考和辅助工具来帮助团队提升效能

效能度量推荐方法

  • 目标驱动,度量对的事(用户满意度、系统稳定性、紧急bug修复上线时长等)
  • 先从全局找瓶颈,在深入细节(收集工程中每个阶段的时间,发现瓶颈再做优化)
  • 通过主观的方式来评价,提高效能(递归思想管理方式)

  • 关注个人维度的指标来提升效能(从本地提交到环境测试越快越好)

四种方法的总结如下:

1 度量只是工具,不是目的。切记度量的真正的目标是提高效能,不要舍本逐末。比如说,如果度量花费的时间超过了收益,那就不要去做。
2 虽然我们推崇数字驱动,但在效能的度量上,不要迷信数字,适当使用主观反馈效果反而更好

研发流程

流程优化

无论你们公司采用哪种开发流程(瀑布、迭代、敏捷、增量等),两个目标是要达成一致

  • 目标一:寻找用户价值,利用MVP(最小可行性产品的思想,使用用户价值来衡量阶段成果)
  • 目标二:提高用户价值的流动效率(小迭代,复盘,消除竖井,提倡全栈)

代码入库前

  • 规范化、自动化核心步骤,主要分为三大步:开发环境获取,本地开发,入库前系统检查

1 开发环境获取:有条件的话,可以像大厂一样自己实现一套虚拟机自动申请释放管理系统统一管理开发机器;没有条件的话可以做个基础镜像,能够快速搭建开发环境进行开发
2 本地开发:有条件的话,自动化测试,代码检查等可以使用共享服务的方式提供,没有条件的话至少要利用一些IDE的工具插件做一些代码检查和使用热启动来提升效率
3 入库前系统检查:流程做到自动化,人工进行审查反馈,有条件的话可以构建沙盒环境,使用脱敏过的生产数据进行验证

可持续集成和持续交付 CI/CD

  • 三条基本原则:1 测试尽量完整自动化;2 耗时少;3 环境架构尽量与生产保持一致
  • 不要教条,根据自己内部特点优化和调整

以下是几个持续性工作的关键点:

分支管理

  • Facebook的代码分支管理和部署流程(主干分支策略)

除了主干分支策略外,还有其它一些管理策略:Git-flow,Fork-merge工作流等

  • PS:之前团队采用的是git-flow流,后来根据多数开发者习惯都切换到了Fork-merge流

全栈思维

  • DevOps:打通开发和运维的文化和惯例
  • SRE:是 DevOps 的具体实践之一,软件工程师和系统管理员的结合
  • 全栈思维要解决的问题:使得多岗位的目标达成一致(前端,后端,运维,测试)
  • 利益的统一,开发人员职责要修改为快速开发和上线稳定的高质量产品
  • 优化开发到部署的整个上线流程,落地时首先要从人出发,然后是流程,最后才是工具
  • 具体落地措施:1 对团队目标达成共识,并重新定义职责;2 设计 CI、CD、快速反馈,以及团队沟通独立群等流程;3 引入工具,实现自动化

落地过程中的任务和推荐工具:

全栈开发就是让工程师不再只是对某一个单一职能负责,而是对最终产品负责。全栈开发是一个很好的抓手,逐步提高全栈开发的程度,大家的目标自然就会对齐,从而主动去提高,那其他方面的提高就容易得多了

懂得开发的运维人员,会越来越重要;同时,更关注部署、测试,甚至产品的全栈工程师,也会越来越受欢迎。

高效信息流通

  • 实现高效沟通首先要解决的,就是团队成员的意愿问题,让他们愿意沟通
  • 在团队内部建设机制,来鼓励共享的行为,从而形成共享的文化
  • 针对研发流程中流动的各种信息,我们要做好分类,针对性地设计合适的流程,并选用恰当的工具,最大程度地共享给团队成员

工程方法

研发环境

  • 配置出高效的研发环境
  • 必要的测试环境和类生产环境
  • 流程尽可能自动化与高效

代码审查

  • 作用:1 尽早发现问题;2 提高个人工程能力;3 知识共享;4 针对性提高;5 统一编码风格
  • 审查方式:1 面对面审查;2 线下异步审查
  • 代码审查的成功案例:1 由5个开发者组成的初创团队采用了1v1面对面代码审查;2 由30个人团队,使用Gerrit、Jenkins、SonarQube来管理代码质量和审查代码,以工具为辅助的进行线下1V1的代码审查,偶尔进行多对一审查;3 由百人以上的团队,GitLab来管理代码,Phabricatgor作为审查代码

各种审查方式的优缺点:

网上参考文章推荐:(Gerrit、Jenkins、Gitlab、SonarQube联动)
https://www.jianshu.com/p/160b260d8956
https://www.jianshu.com/p/e111eb15da90

  • 代码审查应该计入工作量,并且纳入绩效
  • 机器审查和人工审查相结合

  • 推进代码审查的关键操作:1 提高提交的原子性;2 提高提交说明的质量(标题,描述,测试情况,其它关联信息)
  • 可以使用git的提交模板功能来对提交说明格式进行限制
  • 成功推行代码审查两个关键原则:1 互相尊重,为对方考虑;2 基于讨论,而不是评判

质量与速度的均衡

  • 虽然天下武功为快不破,但是好的产品讲究的还是持久发展,不然开发的代码只有几个月的生命周期,开发者对工作的热情也会消散
  • 控制技术债,在适当的时候进行偿还
  • 养成良好的设计、编码、开发习惯,减少技术债生成

A 公司:只关注业务,不偿还技术债
B 公司:持续关注技术债,但对业务时机不敏感
C 公司:持续关注业务和技术债。对业务机会很敏感,敢放手一搏大量借贷,也知道什么时候必须偿还技术债

A 公司在开始的时候,业务产出会比较多,但由于技术债带来的影响,效率会逐渐降低
B 公司在开始的时候,业务产出比较少,但由于对技术债的控制,所以能够保持一个比较稳定的产出,在某一时间点超过 A 公司
C 公司在有市场机会的时候,大胆应用技术债,同时抽出一小部分时间精力做一些技术债预防工作。这样一来,在一开始的时候,C 的业务产出介于 A 和 B 之间,但和 A 的差距不大
随后,在抢占到一定的市场份额之后,C 公司开始投入精力去处理技术债,于是逐步超过 A。另外,虽然 C 公司此时的生产效率低于 B 公司,但因为市场份额的优势,所以总业绩仍然超过 B。在高优先级技术债任务处理好之后,C 公司的生产效率也得到了提升,将 B 公司也甩在了身后

测试变革

  • 传统的开发模式,测试只能被动(需求质量或开发质量差时,只能被动接收),但大锅却一般在测试头上,对于测试很不公平
  • 测试左移:让测试介入代码提测之前的部分,参与需求质量与合理性讨论,在架构设计时就考虑产品的可测试性,并尽量进行开发自测等
  • 测试右移:让测试介入代码提测之后的部分,利用线上的真实环境测试,通过线上监控和预警,及时发现问题并跟进解决,将影响范围降到最低
  • 测试左移的原则:调整测试人员的观念和态度;测试参与需求讨论和用户故事编写;频繁测试快速测试;

五颜六色的发布

  • 蓝绿部署:采用两个分开的集群对软件版本进行升级的一种方式(交替)
  • 红黑部署:与蓝绿部署类似,但是不同点是升级前的机器资源会被释放掉
  • 灰度发布:也被叫作金丝雀发布,属于增量发布,服务升级的过程中,新旧版本会同时为用户提供服务

研发流程未来趋势

  • 团队远程办公、灵活工时办公,会越来越普遍
  • 聊天工具和其他工具的集成,会越来越普遍
  • Docker 和 Kubernetes 带来的各种可能性

备注:CaaS(Containers as a Service),是允许用户通过基于容器的虚拟化来管理和部署容器、应用程序、集群,属于 IaaS 平台的范畴
Kubernetes 出现后,提供了强大的容器管理和编排功能,事实上是实现了一种基于容器的基础设施的抽象,也就是实现了 IaaS 的一个子类。所以通过它,我们终于可以方便地建设定制化的 PaaS 了,一个具体的例子是 FaaS(Function as a Service)。Kubernetes 的出现,极大地降低了建设 FaaS 的工作量,所以很快出现了基于它的实现。比如OpenFaaS、Fission。

正是基于 Kubernetes 提供的构建 PaaS 的能力,预期将来越来越的产品会构建在基于 Kubernetes 和 Docker 的 PaaS 之上。可能会出现整个公司运行一套 Kubernetes 作为 IaaS,上面运行多个不同的 PaaS 平台,支持各种服务的运行。

  • 分布式计算会越来越流行,从微服务演化到服务网格
  • AI技术应用会越来越普遍,门槛也将越来越低

个人效能

个人高效工作三原则

  • 抽象和分而治之,将复杂的任务或问题拆分处理
  • 快速迭代,不要过于追求完美
  • DRY,不要重复自己,对任何重复事情进行自动化

三个原则的实践举例:

聚焦与深度工作

  • 工作中常遇到问题:1 工作从早忙到晚,但一直被业务拖着跑,绩效一般,个人也得不到成长;2 碎片时间很努力地学习相关技术,似乎学了不少,但不成系统,学完也就完了,没什么效果;3 作太忙,没有时间锻炼、放松,效率越来越低,可能自己还察觉不到;4 工作总是被打断,无法静下心来工作和学习
  • 实现聚焦于深度工作的三个步骤:1 以终为始,寻找并聚焦最重要的任务;2 追根究底,寻找最高效的解决方案;3 安排时间和精力,高效执行解决方案
  • 以终为始:自己定义任务,聚焦目标,无情的筛选;将个人成长,团队提升,业务增长三个统一起来;例如:利用自己的技能帮助团队工作提升,带来业务增长;最保留最重要的3个任务
  • 寻找高效解决方案:对任务和问题多问几个为什么,找到根本原因和目标
  • 高效执行:安排整块的时间,排除手机等其它因素干扰,制订自己番茄钟,强制锻炼

效率工具

  • 熟悉操作系统的各种快捷键和常用技巧
  • 使用云笔记来安排与记录工作
  • 使用云盘来管理自己的资料和工具
  • 用一款好的鼠标与键盘(可以多买几套放置在不同地方)

  • 对浏览器网页标签收藏与管理(推荐google的papaly)

  • 尽量使用正版软件,一般正版软件能提供更多更好功能和可靠性,减少自己在这块的精力投入
  • 使用高效的工作工具,包含:IDE编辑器(VS CODE,vim,JetBrains系列产品),命令行工具(git shell,cmder),文件夹标签管理(clover),测试工具(postman),SSH工具(termius),VPN工具,linux下的快捷工具(目录查看tree,exa;zsh,tmux,mosh)
  • 推荐一个工具集参考:https://www.cnblogs.com/hi-linux/p/11580086.html
    一些工作场景和常用的工具列表:

GIT提高原子性提交技巧

  • 1 把工作区里的代码改动的一部分转变为提交(主要是为了按功能点提交,让每次的提交都完成一个小功能点)关键命令:git add -p xxx文件
    参考链接:https://johnkary.net/blog/git-add-p-the-most-powerful-git-feature-youre-not-using-yet/
  • 2 对当前提交进行拆分,解决一不小心已经把不同功能点改造代码一并提交了,关键命令:git reset HEAD^,注意创建一个备份分支
  • 3 修改当前的提交,可以是提交的备注或提交的文件,关键命令:git commit --amend git add git rm
  • 4 交换多个提交的先后顺序,解决只想把后面的提交推送到远程,关键命令:git rebase -i origin/master, 注意创建一个备份分支

提交了A和B两次

执行rebase后的示意图,可以选择基准后A和B的顺序,甚至舍弃某次提交

git rebase -i 的功能非常强大,除了交换提交的顺序外,还可以删除提交、和并多个提交
参考链接:https://git-scm.com/book/zh/v2/Git-%E5%B7%A5%E5%85%B7-%E9%87%8D%E5%86%99%E5%8E%86%E5%8F%B2

  • 5 修改非头部提交,为了方便实现原子性,我们常常需要修改历史提交,也就是修改非头部提交,关键命令:git rebase -i origin/master
    我要对A比较进行修改

修改后提交

Git 学习曲线比较陡而且长,帮助手册也可以说是晦涩难懂,但一旦弄懂,它能让你超级灵活地对本地代码仓进行处理,帮助你发现代码仓管理系统的新天地。git rebase -i 命令,就是一个非常典型的例子。一开始,你会觉得它有些难以理解,但搞懂之后就超级有用,可以帮助你高效地解决非常多的问题。所以,在我看来,在 Git 上投入一些时间绝对值得!

管理与文化

业务和技术两手抓

提高团队的研发效能,还要通过管理和文化让之前的原则和方法真正在团队落地。管理是提高团队研发效能的基石,而文化是持久高效的保障。同时,管理又决定了文化,如下图:

  • 技术管理的三个步骤:1 寻找目标;2 目标管理;3 计划并执行
  • 寻找目标:技术团队的根本目标就是业务目标,但为了支撑业务增长还需要有技术目标(重构、归还技术债,新技术推行)
  • 目标管理:用SMART 原则执行目标,用OKR对目标进行管理(OKR不是绩效管理方法,只是目标管理工具)
  • 任务执行:关键还是人,调用人的主观意愿;采用康威定律来组织团队结构

工程师文化

  • 定义自己需要的文化,别人家的不一定是适合你的
  • 一个团队能否高效产出,文化起到关键作用
  • 文化更像是潜规则,写到横幅上的标语并不一定是公司文化
  • 文化的建设,更是技术活和力气活的合体,绝不是喊几句口号就可以完成的
  • 工程师文化是创造力引擎
  • 工程师文化的特点:黑客之道,优化无止境,持续进步,代码为王,能力为王,打破常规,突破界限
  • 需要特别注意,公司自身发展要好,不然很难发展公司文化
  • 工程师文化实践三大支柱:1 做感兴趣的事;2 拥有信息和权限;3 绩效调节
  • 绩效调节方式:1 面对面沟通反馈;2 360度绩效考评(自己选评价同事,主管指派,自评,主管,所有直接下级)
  • 绩效评定原则:对公司的贡献而不是团队,保持客观与公正(事例或数据支撑,多人评价)

总结

超越昨天的自己,享受成长的快乐
国内的软件行业,值得优化的地方比比皆是。国内软件研发人员的能力和创造性,绝不亚于硅谷那些高效能公司。只要我们的方向对了,并不断提高,就一定可以大幅提高团队和个人的研发效能,从而把时间花在最值得的地方

PS:本文主要是对葛俊老师在极客时间专栏《研发效能破局之道》学习过程的整理,以及自己的一些感悟和体会与实践。推荐感兴趣的同学去订阅专栏进行更加系统和全面的学习

Springboot教程系列(3)

hzqiuxm阅读(4231)

Springboot的外部化配置

外部化配置概念理解

什么是外部化配置

这个名词来源于Springboot官方文档的某一个章节名称,官方并没有对其下过准确的定义。一般研发人员,运维人员之间沟通时,经常会提及它。

有外部化配置也就有内部化配置,一般我们把在代码中枚举类,或硬性编码的部分称之为内部化配置。内部化配置缺少灵活性。

一个很熟悉的场景:一般公司的系统都会划分为开发(dev),测试(test),生产(prod)三个环境,每个环境的数据库、参数配置肯定是不一样的。一般公司都会借助spring的profile结合maven或gradle构建软件实现灵活的构建,部署好的软件系统自动对应到相应的环境,不用进行源码的修改。

还有springcloud的微服务实践中,通常也会引入总线配置方式,实现在系统不重启的情况下,实现修改某些参数或配置的目的。这些业务场景下配置其实就是外部化配置思想。抽象下概念外部化配置可以理解为:对于可扩展性应用系统,其内部组件是可配置化的,比如:认证信息、端口范围、线程池属性等。

  • 官方链接:https://docs.spring.io/spring-boot/docs/2.0.2.RELEASE/reference/htmlsingle/#boot-features-external-config

springboot中外部化配置

springboot官方提供了三种外部化配置应用方式:

  • Bean的@Value注入
  • Spring Eviroment读取
  • @ConfigurationProperties綁定到结构化对象

外部化配置实际应用

XML Bean的属性占位符

  • 比如在spring的xml配置文件中添加如下配置:
<bean id="user" class="com.hzqiuxm.configuration.domain.User">
<property name="id" value="${user.id}"/>
<property name="name" value="${user.name}"/>
</bean>
  • 另外一个xml配置文件内容:
<!-- 属性占位符配置-->
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
    <!-- Properties 文件 classpath 路径 -->
    <property name="location" value="classpath:/config/application.properties"/>
    <!-- 文件字符编码 -->
    <property name="fileEncoding" value="UTF-8"/>
</bean>
  • 在属性配置文件(application.properties)中添加
# 用户配置属性
user.id = 10
user.name =临江仙2018

创建对应的实体类user,添加对应字段的get/set方法后,启动Spirng启动到类获取到实体类User的Bean,可以看到其id和name字段的值为配置文件中配置的值

public class SpringXmlConfigPlaceholderBootstrap {
    public static void main(String[] args) {
        String[] locations = {"META-INF/spring/spring-context.xml", "META-INF/spring/user-context.xml"};
        ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext(locations);
        User user = applicationContext.getBean("user", User.class);
        System.err.println("用户对象 : " + user);
        // 关闭上下文
        applicationContext.close();
    }
}

输出结果:id=10, name='临江仙 2018',符合预期和我们配置文件中的值一致。这种方式是SpringFrame中普遍使用的方式,springboot出现后使用频率已经越来越少了。

接下来我们来搞点事情,换成springboot的方式启动

@ImportResource("META-INF/spring/user-context.xml") // 加载 Spring 上下文 XML 文件
@EnableAutoConfiguration
public class XmlPlaceholderExternalizedConfigurationBootstrap {

    public static void main(String[] args) {

        ConfigurableApplicationContext context =
                new SpringApplicationBuilder(XmlPlaceholderExternalizedConfigurationBootstrap.class)
                        .web(WebApplicationType.NONE) // 非 Web 应用
                        .run(args);

        User user = context.getBean("user", User.class);

        System.err.println("用户对象 : " + user);
        // 关闭上下文
        context.close();
    }
}

输出结果:id=10, name='hzqiuxm',为什么不符合预期?id是能对应上的,name的值确不对了?其实是 PropertySources顺序问题捣的鬼,这也是本文需要介绍的内容之一,相信看完文章后你就恍然大悟了。

@Value的注解方式

这种应用在平时开发中非常常见,很多开发人员都采用这种方式注入自定义的一些配置属性值。主要有三种注入方式:

  • 字段注入
  • 构造器注入
  • 方法注入

主要的用法举例:

@Value("${user.id}") //普通属性注入
private Long userId;
@Value("${user.age:${my.user.age:32}}") //嵌套属性注入,非常适合新老API兼容的设计,user.age代表老的,my.user.age代表新的,32代表默认的
private int age;
@Value("#{'${list}'.split(',')}") //list注入
private List<String> list;
@Value("#{${maps}}")  //map注入
private Map<String,String> maps;
--------------------------------------
对应配置文件:
user.id:1
my.user.age:32
list: topic1,topic2,topic3
maps: "{key1: 'value1', key2: 'value2'}"

Eviroment方式读取

  • 方法/构造器依赖注入
@Override
public void setEnvironment(Environment environment) {
    if (this.environment != environment) {
        throw new IllegalStateException();
    }
}
  • @Autowired依赖注入
@Autowired
@Qualifier(ENVIRONMENT_BEAN_NAME)
private Environment environment;
  • EviromentAware 接口回调
    实现 EnvironmentAwarek接口

  • BeanFactory 依赖查找Environment
    实现 BeanFactoryAware接口

@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
    if (this.environment != beanFactory.getBean(ENVIRONMENT_BEAN_NAME, Environment.class)) {
        throw new IllegalStateException();
    }
}

三者的执行顺序:1 @Autowired; 2 BeanFactoryAware ;3 EviromentAware

@ConfigurationProperties Bean绑定

  • 类级别注入

  • @Bean方法声明

  • 嵌套类型绑定

外部化配置扩展

定义外部化属性源

  • PropertySources的顺序问题:官方提供的参考如下(Springboot版本需要1.5以上,低版本会缺少部分)


- 什么是PropertySource
带有名称的属性源,Properties文件、Map、YAML 文件等都可以称之为PropertySource。

  • 什么是Eviroment抽象
    Environment与PropertySources可以看成是一一对应的关系;PropertySource与PropertySources从单词的单数和复数关系也可以看的出是 1 对 多的关系;ConfigurableEnvironment与MutablePropertySources相对应。

PropertySources属性源使用时机

  • Spring Framework 中,尽量在org.springframework.context.support.AbstractApplicationContext#prepareBeanFactory方法前初始化。
  • Spring Boot 中,尽量在org.springframework.boot.SpringApplication#refreshContext(context)方法前初始化。

扩展外部化配置属性源

基于 SpringApplicationRunListener#environmentPrepared 扩展外部化配置属性源
  • 实现两个接口:SpringApplicationRunListener, Ordered
  • META-INF下新建spring.factories文件,添加如下配置
# Run Listeners
org.springframework.boot.SpringApplicationRunListener=\
com.imooc.diveinspringboot.externalized.configuration.configuration.ExtendPropertySourcesRunListener
  • 重写SpringApplicationRunListener#environmentPrepared 和 getOrder
@Override
public void environmentPrepared(ConfigurableEnvironment environment) {

    MutablePropertySources propertySources = environment.getPropertySources();
    Map<String, Object> source = new HashMap<>();
    source.put("user.id", "0"); //设置编号为0
    MapPropertySource propertySource = new MapPropertySource("from-environmentPrepared", source);
    propertySources.addFirst(propertySource);
}

@Override
public int getOrder() {
    return new EventPublishingRunListener(application,args).getOrder() + 1;//返回排在默认的后面
}

各个文件中的配置如下:

自定义environmentPrepared中: 0
application.properties : 10
META-INF/default.properties : 11
  • 定义引导类ExtendPropertySourcesBootstrap,并模拟一个Command line arguments(88) 和Default properties(99) 配置方式
@EnableAutoConfiguration
@Configuration
@PropertySource(name = "from default.properties", value = "classpath:META-INF/spring/default.properties")
public class ExtendPropertySourcesBootstrap {
    public static void main(String[] args) {
        ConfigurableApplicationContext context =
                new SpringApplicationBuilder(ExtendPropertySourcesBootstrap.class)
                        .web(WebApplicationType.NONE) // 非 Web 应用
                        .properties("user.id=99")        // Default properties
                        .run(of("--user.id=88")); // Command line arguments.
        // 获取 Environment 对象
        ConfigurableEnvironment environment = context.getEnvironment();
        System.err.printf("用户id : %d\n", environment.getProperty("user.id", Long.class));
        environment.getPropertySources().forEach(propertySource -> {
            System.err.printf("PropertySource[名称:%s] : %s\n", propertySource.getName(), propertySource);
        });

        // 关闭上下文
        context.close();
    }
    private static <T> T[] of(T... args) {
        return args;
    }

}

根据上一节提到的PropertySources的顺序问题,我们可以猜测 我们自定义的优先级应该最高,所以结果应该是0
输出结果如下,符合预期:

用户id : 0
PropertySource[名称:configurationProperties] : ConfigurationPropertySourcesPropertySource {name='configurationProperties'}
PropertySource[名称:from-environmentPrepared] : MapPropertySource {name='from-environmentPrepared'}
PropertySource[名称:commandLineArgs] : SimpleCommandLinePropertySource {name='commandLineArgs'}
PropertySource[名称:systemProperties] : MapPropertySource {name='systemProperties'}
PropertySource[名称:systemEnvironment] : OriginAwareSystemEnvironmentPropertySource {name='systemEnvironment'}
PropertySource[名称:random] : RandomValuePropertySource {name='random'}
PropertySource[名称:applicationConfig: [classpath:/config/application.properties]] : OriginTrackedMapPropertySource {name='applicationConfig: [classpath:/config/application.properties]'}
PropertySource[名称:from default.properties] : ResourcePropertySource {name='from default.properties'}
PropertySource[名称:defaultProperties] : MapPropertySource {name='defaultProperties'}

类似的我们还可以重载:contextPrepared和contextLoaded方式来实现自定义的配置。三个方法的执行顺序为:
1 environmentPrepared;2 contextPrepared;3 contextLoaded;因为我们采用的是addFirst方法,先执行会被后执行的覆盖,三者优先级是倒过来的,这点需要特别注意

执行顺序在SpringApplication#run中可以看到,可以翻看之前的一篇文章获得具体详情,这里给标注下源码和相应位置

基于 SpringApplicationRunListener#contextPrepared 扩展外部化配置属性源
  • 参考SpringApplicationRunListener#environmentPrepared
基于 SpringApplicationRunListener#contextLoaded 扩展外部化配置属性源
  • 参考SpringApplicationRunListener#environmentPrepared
基于 ApplicationEnvironmentPreparedEvent 扩展外部化配置属性源
  • 实现ApplicationListener接口,并重载onApplicationEvent方法
@Override
public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
    ConfigurableEnvironment environment = event.getEnvironment();
    MutablePropertySources propertySources = environment.getPropertySources();
    Map<String, Object> source = new HashMap<>();
    source.put("user.id", "9");//设置程9
    MapPropertySource propertySource = new MapPropertySource("from-ApplicationEnvironmentPreparedEvent", source);
    propertySources.addFirst(propertySource);
}
  • META-INF下新建spring.factories文件,添加如下配置
# Event Listeners
org.springframework.context.ApplicationListener=\
com.imooc.diveinspringboot.externalized.configuration.configuration.ExtendPropertySourcesEventListener
  • 启动之前引导类ExtendPropertySourcesBootstrap
    由于ApplicationListener是在SpringApplication构造的时候调用的,执行顺序肯定在SpringApplicationRunListener相关方法之前执行,根据执行被覆盖的原则,输出的值应该是0

  • 输出结果如下:

用户id : 0
PropertySource[名称:configurationProperties] : ConfigurationPropertySourcesPropertySource {name='configurationProperties'}
PropertySource[名称:from-environmentPrepared] : MapPropertySource {name='from-environmentPrepared'}
PropertySource[名称:from-ApplicationEnvironmentPreparedEvent] : MapPropertySource {name='from-ApplicationEnvironmentPreparedEvent'}
PropertySource[名称:commandLineArgs] : SimpleCommandLinePropertySource {name='commandLineArgs'}
PropertySource[名称:systemProperties] : MapPropertySource {name='systemProperties'}
PropertySource[名称:systemEnvironment] : OriginAwareSystemEnvironmentPropertySource {name='systemEnvironment'}
PropertySource[名称:random] : RandomValuePropertySource {name='random'}
PropertySource[名称:applicationConfig: [classpath:/config/application.properties]] : OriginTrackedMapPropertySource {name='applicationConfig: [classpath:/config/application.properties]'}
PropertySource[名称:from default.properties] : ResourcePropertySource {name='from default.properties'}
PropertySource[名称:defaultProperties] : MapPropertySource {name='defaultProperties'}

符合我们分析的结果

基于 EnvironmentPostProcessor 扩展外部化配置属性源
  • 实现 EnvironmentPostProcessor接口和Ordered接口,分别重载它们的postProcessEnvironment和getOrder方法
  • 在ApplicationEnvironmentPreparedEvent之前执行,所以如果实现了其它的自定义方式,它就会被覆盖
  • META-INF下新建spring.factories文件,添加如下配置
# EnvironmentPostProcessor
org.springframework.boot.env.EnvironmentPostProcessor=\
com.imooc.diveinspringboot.externalized.configuration.processor.ExtendPropertySourcesEnvironmentPostProcessor
  • 具体代码SpringApplicationRunListener#environmentPrepared
基于 ApplicationContextInitializer 扩展外部化配置属性源
  • 在ApplicationContextInitializer上下文初始化的时候进行配置,在SpringApplicationRunListener#environmentPrepared之后执行,所以会覆盖environmentPrepared
  • 不会覆盖contextPrepared和contextLoaded;
  • META-INF下新建spring.factories文件,添加如下配置
# ApplicationContextInitializer
org.springframework.context.ApplicationContextInitializer=\
com.imooc.diveinspringboot.externalized.configuration.initializer.ExtendPropertySourcesApplicationContextInitializer
  • 具体代码参考SpringApplicationRunListener#environmentPrepared

各自定义PropertySource优先级(从高到低)

PropertySource[名称:from-contextLoaded] : MapPropertySource {name='from-contextLoaded'}
PropertySource[名称:from-contextPrepared] : MapPropertySource {name='from-contextPrepared'}
PropertySource[名称:from-ApplicationContextInitializer] : MapPropertySource {name='from-ApplicationContextInitializer'}
PropertySource[名称:configurationProperties] : ConfigurationPropertySourcesPropertySource {name='configurationProperties'}
PropertySource[名称:from-environmentPrepared] : MapPropertySource {name='from-environmentPrepared'}
PropertySource[名称:from-ApplicationEnvironmentPreparedEvent] : MapPropertySource {name='from-ApplicationEnvironmentPreparedEvent'}
PropertySource[名称:from-EnvironmentPostProcessor] : MapPropertySource {name='from-EnvironmentPostProcessor'}
PropertySource[名称:commandLineArgs] : SimpleCommandLinePropertySource {name='commandLineArgs'}
PropertySource[名称:systemProperties] : MapPropertySource {name='systemProperties'}
PropertySource[名称:systemEnvironment] : OriginAwareSystemEnvironmentPropertySource {name='systemEnvironment'}
PropertySource[名称:random] : RandomValuePropertySource {name='random'}
PropertySource[名称:applicationConfig: [classpath:/config/application.properties]] : OriginTrackedMapPropertySource {name='applicationConfig: [classpath:/config/application.properties]'}
PropertySource[名称:from classpath:META-INF/default.properties] : ResourcePropertySource {name='from classpath:META-INF/default.properties'}
PropertySource[名称:defaultProperties] : MapPropertySource {name='defaultProperties'}

实时扩展外部化配置属性源:Eviroment支持,@Value和@ConfigurationProperties 不支持
理解清楚了各种自定义外部化配置的优先级,可以在自己设计框架的时候控制不想被开发人员影响到的配置。

Springboot教程系列(2)

hzqiuxm阅读(4830)

小谈SpringApplication启动

基于Springboot2.0+版本

前言

在Springboot装配入门指南中我们简单了解了下组合注解@SpringbootApplication,它的本质是一个配置角色注解模式,同时开启了自动装配等功能。那我们是如何启动一个Springboot项目的呢?

使用Spring官方提供的网页: https://start.spring.io/ 中生成的项目,都会自动生成一个启动类,该启动类都会使用@SpringbootApplication进行标注,main方法中会统一使用SpringApplication.run()方法来启动。

我们今天的主角就是SpringApplication,谈谈它的启动和运行过程,其中会涉及到上下文应用加载、应用事件加载、应用监听器,应用推断、引导类推断、应用广播等概念

SpringApplication启动

自定义启动

调用run方法启动,例如:SpringApplication.run(MocApplication.class, args);这个大家都很熟悉了,那如果我们自定义启动怎么去实现呢?

大概的步骤是定义一个SpringApplication实例,然后运行时传入run方法需要的两个参数即可。
我们自定义时有两种API方式进行选择:一种是通过SpringApplicationAPI 调整,一种是通过SpringApplicationBuilderAPI调整。二者实现方式分别如下:

第一种:SpringApplicationAPI 方式

public class MySpringApplication {
    public static void main(String[] args) {
        Set<String> sources = new HashSet();
        sources.add(ApplicationConfiguration.class.getName());
        SpringApplication springApplication = new SpringApplication();
        springApplication.setSources(sources);
        springApplication.setBannerMode(Banner.Mode.CONSOLE);//banner打印模式设置
        springApplication.setWebApplicationType(WebApplicationType.NONE);//web应用类型设置
        springApplication.setAdditionalProfiles("dev");//环境设置
        springApplication.setHeadless(true);//图形界面设置
        springApplication.run(args); //启动
    }
    @SpringBootApplication
    public static class ApplicationConfiguration {
        //故意不使用MySpringApplication类作为run的参数
    }
}

第二种:SpringApplicationBuilderAPI 方式,使用了生成器模式书写起来比较流畅

public class MySpringApplication {
    public static void main(String[] args) {
        new SpringApplicationBuilder(MySpringApplication.class)
            .bannerMode(Banner.Mode.CONSOLE)
            .web(WebApplicationType.NONE)
            .profiles("dev")
            .headless(true)
            .run(args);
    }
}

启动run方法源码简单说明

两种方式其实没有什么大的差别,只是书写的时候第二种采用了builder设计模式。我们跟踪原来可以发现,最后run方法返回的是一个ConfigurableApplicationContext,run方法的主要源码如下:

StopWatch stopWatch = new StopWatch(); //构造一个观察器,用来记录时间
  stopWatch.start();//启动观察器
  ConfigurableApplicationContext context = null; //最终返回的应用上下文
  Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();  //启动异常报告
  configureHeadlessProperty();//设置java.awt.headless系统属性为true (代表没有图形化界面)
  SpringApplicationRunListeners listeners = getRunListeners(args);// 获取应用监听器(注解一)
  listeners.starting(); //启动监听器
  try {
   ApplicationArguments applicationArguments = new DefaultApplicationArguments( //构造一个应用程序参数持有类
     args);
   ConfigurableEnvironment environment = prepareEnvironment(listeners,applicationArguments);// 准备Environment(注解二)
   configureIgnoreBeanInfo(environment);//过滤指定配置的Bean
   Banner printedBanner = printBanner(environment);//按指定的方式打印banner
   context = createApplicationContext(); //创建一个Spring应用上下文,即我们平时说的Spring容器(注解三)
   exceptionReporters = getSpringFactoriesInstances(
     SpringBootExceptionReporter.class,
     new Class[] { ConfigurableApplicationContext.class }, context);//准备异常报告
   prepareContext(context, environment, listeners, applicationArguments,printedBanner);//上下文前置处理
   refreshContext(context); //上下文刷新
   afterRefresh(context, applicationArguments);//上下文后置处理
   stopWatch.stop(); //停止观察器的计时
   ... ...
   listeners.started(context); //监听已经初始化完成启动的上下文
    ... ...
   listeners.running(context); //监听正在运行中的上下文
   ... ...
  return context; //返回Spring上下文容器

关于上面源码的额外注解会下面章节进行额外的说明,这里只是对启动过程有个大概了解,然后对主要的注解步骤有个印象。

配置Springboot Bean源

Java 配置 Class 或 XML 上下文配置文件集合,用于 Spring Boot BeanDefinitionLoader读取,并且将配置源解析加载为Spring Bean 定义,数量:一个或多个以上。

一般有两种来实现:一种是采用java配置class方式,就是使用 Spring 模式注解所标注的类,如@Configuration;另一种是传统XML方式,一般我们在新项目中使用第一种方式,无法支持或兼容老的XML配置可以使用@Import来导入XML配置文件。

推断Web应用类型

什么是推断Web应用类型?我们知道在Springboot2.0中(其实是Spring5.0中)加入了Reactive的异步编程模式,用来替代原来传统的servlet方式。所以我们的应用可以是新型的REACTIVE类型,可以是传统的SERVLET类型,还可以是不属于前二者的非WEB类型。

我们可以像之前例子中手动指定某个类型比如:WebApplicationType.NONE,你不指定的话,SpringApplication是会自动推断的。

怎么自动推断呢?在SpringApplication的构造函数中,我们可以看到一个方法 WebApplicationType.deduceFromClasspath(),这个方式就根据classpath中是否包含特定的类来推断属于哪一种,都没有特定类的时候为非WEB应用,SERVLET和REACTIVE以SERVLET为优先,具体逻辑大家可以查看源码。

三者对应关系类型如下:

Web Reactive:WebApplicationType.REACTIVE
Web Servlet:WebApplicationType.SERVLET
非 Web:WebApplicationType.NONE

推断引导类

除了对应用类型进行推断外,SpringApplication还会进行引导类(Main Class)推断。源码如下:

private Class deduceMainApplicationClass() {
  try {
   StackTraceElement[] stackTrace = new RuntimeException().getStackTrace();
   for (StackTraceElement stackTraceElement : stackTrace) {
    if ("main".equals(stackTraceElement.getMethodName())) {
     return Class.forName(stackTraceElement.getClassName());
    }
   }
  }
  catch (ClassNotFoundException ex) {
  }
  return null;
 }

```

从上面源码可以看出,它是根据 Main 线程执行堆栈信息来判断实际的引导类的,就像我们在自定义SpringApplicationAPI 方式时,故意将配置注解标注在新建了的一个类上。最后也是可以启动成功的。


#### 加载应用上下文初始器

在SpringApplication构造器中,除了上面介绍的二个推断外,另外一个重要的操作就是加载应用上下文初始器:ApplicationContextInitializer。其原理是利用 Spring 工厂加载机制,实例化ApplicationContextInitializer实现类,并排序对象集合。相关源码如下:
```
private  Collection getSpringFactoriesInstances(Class type,
   Class[] parameterTypes, Object... args) {
  ClassLoader classLoader = getClassLoader();
  Set<String> names = new LinkedHashSet<>(
    SpringFactoriesLoader.loadFactoryNames(type, classLoader));
  List<T> instances = createSpringFactoriesInstances(type, parameterTypes,
    classLoader, args, names);
  AnnotationAwareOrderComparator.sort(instances);
  return instances;
 }

实现类:SpringFactoriesLoader,在固定路径下配置相关资源:META-INF/spring.factories,顺序的设置依赖 AnnotationAwareOrderComparator#sort

加载应用事件监听器

我们自定义事件监听器的话也是利用 Spring 工厂加载机制,实例化ApplicationListener实现类,并采用排序对象AnnotationAwareOrderComparator来设置加载的顺序。下面是二个自定义ApplicationListener实现例子,顺序设置上分别采用@Order注解方式和实现Ordered接口方式。

第一种,@Order注解方式:

@Order(Ordered.HIGHEST_PRECEDENCE) //优先级最高,对应最小整数
public class OneApplicationListener implements ApplicationListener<ContextRefreshedEvent> {
    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        System.out.println("One: " + event.getApplicationContext().getId()
                + " , timestamp : " + event.getTimestamp());
    }
}

第二种,Ordered接口方式

public class TwoApplicationListener implements ApplicationListener<ContextRefreshedEvent>,Ordered {
    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        System.out.println("Two: " + event.getApplicationContext().getId()
                + " , timestamp : " + event.getTimestamp());
    }
    @Override
    public int getOrder() {
        return Ordered.LOWEST_PRECEDENCE; //最低优先级,对应最大整数
    }
}

对应的配置文件spring.factories内容:

org.springframework.context.ApplicationListener=\
com.imooc.diveinspringboot.listener.TwoApplicationListener,\
com.imooc.diveinspringboot.listener.OneApplicationListener

启动我们之前自定义的SpringApplication应用后,我们可以看到控制台日志中会输出两个应用事件监听器加载信息,虽然One配置在Two后面,但是最后打印的顺序是根据Ordered设置的值来决定的。

SpringApplication运行

至此介绍完了SpringApplication准备阶段的一些主要事情,接下来主要介绍运行阶段(run方法中)的一些主要事情

加载运行监听器(注解一)

类似ApplicationListener的实现,也利用 Spring 工厂加载机制,读取SpringApplicationRunListener对象集合,并且封装到组合类SpringApplicationRunListeners。

我们查看springboot的spring.factories可以看到SpringApplicationRunListeners 内部实现是EventPublishingRunListener,它利用 Spring Framework 事件API ,广播 Spring Boot 事件。

如果我们要自定义实现的话可以仿照EventPublishingRunListener去实现。
SpringApplicationRunListeners监听多个运行状态方法,具体如下:

监听事件

完成一个自定义监听事件的步骤分为三步: 1. 定义事件 2.注册到监听器 3.发布事件
Spring 应用事件可以分为两类:普通应用事件(ApplicationEvent)和应用上下文事件(ApplicationContextEvent),后者继承了前者。一般我们实现的时候继承前者即可。下面是一个自定义事件的例子:

public class MyEvent extends ApplicationEvent {
    public MyEvent(Object source) {
        super(source);
    }
}

注册到监听器有两种方式:接口编程(实现ApplicationListener,它是一个函数式接口)和注解编程(@EventListener),二者例子如下:
第一种,接口编程方式;

AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
// 注册应用事件监听器
        context.addApplicationListener(event -> {
            System.out.println("监听到事件: " + event);
        });

第二种,注解编程方式;

@Component
public class MyEventListener {
    @EventListener //注册应用事件监听器
    @Async //异步执行
    public void onListener(MyEvent event){
            System.out.println("The event is : " + event);
        }
    }
}

最后一步是发布事件,事件的刷新和关闭也属于一种事件,在上面第一种实现方式中增加下面的代码会触发5个事件:

 //会监听到5个事件
        context.refresh(); // 刷新上下文
        // 发送事件
        context.publishEvent("hello");
        context.publishEvent("world");
        context.publishEvent(new ApplicationEvent("hzqiuxm") {
        });
        context.close();  // 关闭上下文

EventPublishingRunListener监听方法与 Spring Boot 事件对应关系:

最后提下EventPublishingRunListener实现类中进行Spring 应用事件广播是通过SimpleApplicationEventMulticaster类来实现的,它的执行方式有同步和异步两种。
SimpleApplicationEventMulticaster实现的是Spring 广播器接口:ApplicationEventMulticaster。

创建应用上下文(注解三)

在注解三的代码处生成我们最终得到的应用上下文(ConfigurableApplicationContext)容器。
根据准备阶段的推断 Web 应用类型去创建对应的ConfigurableApplicationContext实例,不同类型对应的应用上下文也不同,具体关系如下:

Web Reactive:AnnotationConfigReactiveWebServerApplicationContext
Web Servlet:AnnotationConfigServletWebServerApplicationContext
非 Web:AnnotationConfigApplicationContext

具体细节这里就不叙述了。

创建Environment(注解二)

这里主要是根据准备阶段的推断 Web 应用类型创建对应的ConfigurableEnvironment实例,也有三种类型:

Web Reactive:StandardEnvironmentWeb
Servlet:StandardServletEnvironment
非 Web:StandardEnvironment

总结

经过介绍,我们大致清楚SpringApplication的启动主要分为两个阶段:启动阶段(准备阶段由构造方法完成)与运行阶段(调用run方法完成)。

启动阶段其中主要涉及到了web类型推断、引导类推断、初始化器以及监听器加载这几个概念,我们如果要实现自定义的监听器,它们都需要利用Spring工厂加载机制,再通过META-INF/spring.factories完成定义。

运行阶段其中主要有一个SpringApplicationRunListeners的概念,它作为Spring Boot容器初始化时各阶段事件的中转器,将事件派发给感兴趣的Listeners(启动阶段得到的)。这些阶段性事件将容器的初始化过程给构造起来,提供了比较强大的可扩展性。

如果作为应用开发者要对Spring Boot容器的启动阶段进行扩展会有哪些方式呢?我想至少有下面几种:

  • 自定义启动类
  • 自定义初始化器
  • 自定义监听器

玩转设计模式系列(1)

hzqiuxm阅读(4913)

OOD设计原则

面向对象的分析设计有很多的原则,这些原则从思想层面给我们以指导,是我们进行面向对象设计应该尽力遵守的体现。

学习设计模式之前,应该要对设计原则做个简单的了解,只有这样我们在学习设计模式的时候,才能把某个场景的具体解决方案与设计原则联系起来。

某个设计模式可能遵守了几个设计原则,也可能违背了某个设计原则。我们不要把设计模式看成是银弹,同样设计原则也不是。

下面介绍几个主流的OOD设计思想,希望对你学习设计模式或进行业务设计时有所帮助。

单一职责原则 SEP

核心思想

所谓单一职责其核心思想指的是:一个类应该只有一个引起它变化的原因

实际应用举例

上面这句话中“变化”就是代表职责,如果一个类有多个引起它变化的原因,那么就意味着这个类的职责太多了,职责耦合性太强,需要拆分。

换句话所有人口都会念但大多数都不知道怎么做的就是:高内聚低耦合。

这个职责理解起来好像很简单,但是在实际的业务场景中是很难完全做到的。难点就在于如何区分“职责”。这是一个没有标准量化的东西,哪些算职责?哪些职责属于一类?职责应该多大的粒度?怎么细化?所以这个原则也是最容易被违背的。

我们举一个用户服务例子来分析下:

public interface IuserService{
        void setHeight(double height);
        double getHeight();
        void setWeight(double weight);
        double getWeight();
        double updateHeight();
        boolean addRole();
    }

这个例子还是十分简单的,很明显身高height和体重weight是属于用户的对象属性,更新身高addRoleupdateHeight和增加角色是属于用户的行为属性,它们属于不同的职责,在实际实际中对象属性一般放在实体或值对象中,而后者一般是放在具体业务实现中。太简单了?你已经完全掌握了?我们再看一个例子:

public interface Iphone {
        //拨号
        public void dial(String phoneNumber);
        //通话
        public void chat(Object obj);
        //挂断
        public void hangup();
    }

这个Iphone有没有问题?一般人还真看不出来,很多源码和设计都是这样设计的。那么它满足我们单一职责的原则吗?其实是不满足的,拨号和挂断负责的是通讯协议管理(连接和断开),而通话负责的是数据传输(把通话内容进行传输与信号转换)。看来职责是不同的,那么职责之间会互相影响吗?

拨号连接的时候,只要能接通就行了,至于是电信的还是移动协议不用关心;电话连接好后,关心传递什么数据吗?不关心!所以我们的最佳选择是什么?拆!把负责通讯的放在一个接口,把负责数据传输的放在一个接口,然后用一个公共的实现类去实现这两个接口(不要单独实现接口然后使用组合模式,太复杂了)。

OK到目前为止,完美满足SRP了!但是有时候我们的业务真的都要做到这么完美?不一定。请结合项目的可变因素、不可变因素、项目工期、人员组成、成本收益等找到适合你的平衡点。

实践原则建议

几乎不可能做到一个类真的只有一个职责,但是我们可以区分一个类如果有多个职责的话,那么这些职责中哪些是变化的?

我们可以把业务上不变的职责放在一起,做到多个职责中只有一个职责才会变化(频繁),那原则上,这个设计也是满足单一职责原则的。进一步如果一个类有多个变化的职责,但是职责变化是会互相影响或者职责变化不会互相影响但二者变化频率差一个数量级,那也算是一种折中的满足单一职责原则。

接口层面,一定要做到满足单一原则;类的设计嘛,量力而行。
单一职责是接口或类的设计原则,单同时也试用于方法,一个方法尽可能只做一件事。
带来的好处:

  • 类的复杂度降低,实现上面职责都有清晰的定义
  • 可读性高,提升代码可维护性
  • 降低业务变更风险,职责分开修改范围和影响范围都降低了

可能存在的坏处:过于单一职责会导致接口或类关系异常复杂
使用难点:如何划分职责的粗细度,如何成本收益的平衡

里氏替换原则 LSP

核心思想

所谓里氏替换原则其核心思想指的是:子类型必须能够替换掉它的父类型。

实际应用举例

很明显,这是一种多态的使用情况,它可以避免在多态的使用中,出现某些隐蔽的错误。它其实包含了四层意思:

  • 1.子类必须完全实现父类的方法
  • 2.子类可以增加自己特有的方法
  • 3.覆盖或实现父类的方法时输入参数可以被放大
  • 4.覆盖或实现父类的方法时输出参数可以被缩小

我们看个例子,就拿之前的手机举例:

手机抽象类实现了一个默认的打电话的方法,并定义了一个操作接口,各种型号的手机继承它就可以了,他们直接就具备可以打电话功能;因为有操作接口,各个手机根据自己是触屏手机还是物理按键手机来进行实现,触屏还分为多种:电容式、电阻式、红外线式、表面声波式。

如果子类不实现父类的方法就无法进行对手机操作,那这样的手机还有是什么用?此外子类可以增加自己其它功能:指纹识别,虹膜识别,人脸识别等。

public abstract class Iphone{
        public void dial(String phoneNumber){
            checkPhoneNumber(phoneNumber);
            doDial(phoneNumber);
            ... ...
        }

假设我们来了手机模型类,这个类怎么处理?直接继承手机抽象类吗?显然是不可以的,手机模型能打电话?不能!那怎么办?

有两种解决办法:
第一种继承后,使用手机类的时候判断下是不是模型,是模型就不能用来打电话。这种方式听上去就不靠谱,每个用到手机类的地方都要加这个判断。
第二种方法,手机模型单独拿出来,它本来就不应该属于手机的一种!那如果是类似苹果推出的itouch(和智能手机功能类似,单不具备打电话,GPS等功能)产品呢?也不应该继承,而是单独作为一个接口或类,大部分手机能做它也能做的事情,可以采用委托的方式交给手机接口去做。

第3点和第4点比较好理解,假设父类入参是hashMap,子类实现的时候可以指定为map,因为map和hashMap也是符合里氏替换原则的,反过来的话就不行了;同理,父类的返回类型是map的话,子类实现的时候可以具体到hashMap,反之则不行。

原则建议

请严格遵守四条原则。

从另外一个角度来说,里氏替换其实是实现开闭原则的重要手段之一。

扩展的一个实现常用手段就是继承,里氏替换原则保证了子类型能够正确替换父类型,只有能正确替换,才能实现扩展,否则扩展了也会出现错误。

难点

带来好处:

  • 提高代码的重用性
  • 提高代码的扩展性

可能坏处:

  • 继承具有侵入性,子类必定拥有父类的属性与方法
  • 降低代码灵活性,子类必须要有父类的属性和方法,父类修改了子类就会受到影响

依赖倒装原则

核心思想

所谓依赖倒装原则其核心思想指的是:依赖抽象而不依赖具体类

原则建议

要做到依赖倒置,应该要做到:

  • 高层模块不应该依赖于底层模块,二者都应该依赖于抽象
  • 抽象不应该依赖于具体实现,具体实现应该依赖于抽象

该思想应该是几个里面最容易理解的,有点类似于面向接口编程,算是大多数项目里实践的最好的一个思想。
它又叫好莱坞原则:不要找我们,我们会联系你。架构设计中的组件解耦或边界单向跨越也体现了该思想

难点

用的好可以使依赖解耦,易于扩展。用的不好就会存在滥用问题,有的业务可能不会有不同的具体实现类,但是也一般会采用面向接口编程的方式

接口隔离原则 ISP

核心思想

所谓接口隔离原则其核心思想指的是:要多少给多少,不要强迫客户依赖他们不用的方法

实际应用举例

这个原则一般用来处理那种非常庞大的接口,这种接口基本也违反了单一职责原则。

客户可能只会使用该接口的部分方法,存在很多不要的方法,那这些方法其实就是接口污染,强迫客户在一堆方法中招自己需要的方法。应该按照不同的客户使用情况来进行分类,哪怕已经符合单一原则了。

这里举一个CTO来如何评价优秀开发工程师的例子,一开始的设计是这样子的:

我们定义了一个优秀开发工程师的接口,包含了四个方法,分别代表着优秀开发工程师的四个特点:编码速度快,bug数量少,独立完成复杂任务,技术攻关能力。

看起来一切都很完美,现任CTO通过这四个方法来评价也没啥问题。但是不就之后绩效考核方案调整了,大家都认为全部满足四个特点肯定是优秀的,但是不全部满足的也可以是优秀的。

于似乎优秀工程师分为了两类:编码速度快且bug数量少的和能独立完成复杂人数善于技术攻关的。我们的代码应该怎么修改呢?再写一个扩展类只实现编码速度快且bug数量少的方法?

很明显我们不能这么做,管理层是依赖所有方法的,如果扩展类只实现了部分方法,CTO肯定很懵逼,为什么有的方法没有打印任何信息?

究其原因,其实是优秀工程师的接口过于"庞大"了,其变化的因素包含了两个不同的维度工程师硬技能(编码相关)和软技能(解决问题相关)两部分,好的修改方案应该是把这个接口拆分掉,把硬技能和软技能的分来。

修改后的设计时这样子的:

这样重构后,以后你要硬技能优秀工程师还是软技能优秀工程师都可以保持接口不变,增加了灵活性和可维护性。

原则建议

  • 一个接口只服务于一个子模块或业务逻辑
  • 多注意压缩业务逻辑接口中的public方法
  • 已经被污染的接口,尽量去修改,若变更风险大,可以采用适配器模式进行处理
  • 合理使用委托、多重继承等方式对庞大的接口进行分离,那多少算庞大?当你的鼠标滚轮滑动3次时,你应该需要仔细考虑该问题了!
  • 最后深入了解业务逻辑,最好的接口设计师当然应该是你自己!

难点

接口的设计是有限度的,粒度越小系统越灵活,这是不争的事实。但是,灵活的同时肯定也带来了结构复杂化,开发难度增加。所以这个度的掌握得根据经验和常识判断了。

最少知识原则 LSP

核心思想

所谓最少知识原则其核心思想指的是:只和你的朋友(出现在成员变量、方法中的参数类)谈话。

实际应用举例

这个原则是指导我们尽量减少对象间的交互,对象之和自己的朋友谈话交互。减少类之间的耦合度,降低修改带来的风险。

我们看下面这个项目开发的例子,CTO发布指令给项目经理安排开发人员进行开发,开发步骤分别有3个步骤:设计、编码、测试,每个步骤通过后才能进行下一步,单元测试通过后就可以提交代码了。
根据上面设计,编码如下:

  • CTO发送指令给项目管理员
public class CTO {
    /**
     * 安排项目管理员去跟踪项目
     * @param projectLeader
     */
    public void command(ProjectLeader projectLeader){

        DevEngineer devEngineer = new DevEngineer("三台");
        projectLeader.notifyDev(devEngineer);
    }
}
  • 项目管理员通知开发人员进行开发,并根据结果控制步骤
public class ProjectLeader {
    /**
     * 通知开发人员进行相关任务步骤
     * @param engineer 开发人员
     */
    public void notifyDev(DevEngineer engineer){
        int design = engineer.design();
        if(design >= 60){
            int code = engineer.code();
            if(code >= 60){
                int test = engineer.test();
                if(test >= 60){
                    System.out.println("开发任务完成可以提交代码了!");
                }else {
                    System.out.println("不合格,单元测试覆盖率不足!");
                }
            }else {
                System.out.println("不合格,代码需要重构!");
            }
        }else {
            System.out.println("不合格,重新回去设计!");
        }
    }
}
  • 开发人员进行开发
public class DevEngineer {

    private String name;
    private Random rand = new Random(System.currentTimeMillis());
    public DevEngineer(String name) {
        this.name = name;
    }
    /**
     * 设计
     * @return 任务得分
     */
    public int design(){
        System.out.println(this.name + " 进行开发任务的设计和文档编写工作......");
        return rand.nextInt(100);
    }
    /**
     * 编码
     * @return 任务得分
     */
    public int code(){
        System.out.println(this.name + " 进行编码,愉快的编码中......");

        return rand.nextInt(100);
    }
    /**
     * 单元测试
     * @return 任务得分
     */
    public int test(){
        System.out.println(this.name + " 编码完成进行单元测试......");
        return rand.nextInt(100);
    }
}
  • 客户端测试下这个场景
public class Client {
    public static void main(String[] args) {
        CTO cto = new CTO();
        cto.command(new ProjectLeader());
    }
}

上面的代码满足我们定义的需求,但是有没有存在一些问题呢?类图关系如下:

很明显它违背了最少知识原则,具体体现在二个方面:

  • 开发人员不是CTO的朋友,项目管理员才是
  • 项目管理员这位朋友太亲密了,简直手把手指导开发了

对上述代码改造如下:

  • CTO类只和项目管理员打交道
public class CTO {
    /**
     * 安排项目管理员去跟踪项目
     * @param projectLeader
     */
    public void command(ProjectLeader projectLeader){
        projectLeader.notifyDev();
    }
}
  • 项目管理员去通知具体的开发人员,只要通知他们开始开发工作就行了
public class ProjectLeader {

    /**
     * 通知开发人员进行相关任务步骤
     */
    public void notifyDev(){
        DevEngineer devEngineer = new DevEngineer("三台");
        devEngineer.work();
    }
}
  • 开发人员只提供一个对外的方法,就是要不要开始进行工作,工作步骤按照固定流程来即可
//篇幅原因,具体步骤等方法就不展示了,主要从public换成了private
......
public void work(){
    int design = this.design();
    if(design >= 60){
        int code = this.code();
        if(code >= 60){
            int test = this.test();
            if(test >= 60){
                System.out.println("开发任务完成可以提交代码了!");
            }else {
                System.out.println("不合格,单元测试覆盖率不足!");
            }
        }else {
            System.out.println("不合格,代码需要重构!");
        }
    }else {
        System.out.println("不合格,重新回去设计!");
    }
}
.......

通过修改后的关系类图如下:

修改后我们的代码很好的符合了最少知识原则,只有朋友之间才会打交道,打交道的朋友之间也保持了一定的距离。

原则建议

要正确使用这个原则,首先要弄清楚哪些是朋友呢?:

  • 当前对象本身
  • 通过方法参数传递进来的对象
  • 当前对象所创建的对象
  • 当前对象实例所引用的对象
  • 方法内创建或实例化的对象

上面的指导告诉我们如何区分朋友,但是朋友的关系也不能太近,就如两个刺猬取暖:太远取不到暖,太近容易互相伤害。

人类交朋友有个邓巴数(150人左右)和形容关系连接的六度空间理论,那么最少知识原则是否也能给我们提供一个适合OOD邓巴数呢?

这里推荐一个自己自创的概念:迪比特二度空间理论,意思就是跳转二次才访问到一个类时,就需要考虑重新设计你的代码了。最后请心里时刻谨记:尽量减少对象的依赖。

开放关闭原则

核心思想

所谓开闭原则其核心思想指的是:对修改关闭,对扩展开放。

它要求类的行为是可以扩展的,但是不能对其修改。怎么听起来有种又想马儿不吃草,又想马儿跑的快的赶脚?

实现其关键方法就是合理地抽象、分离出变化和不变化的部分。为变化的部分预留可扩展的方式,比如:钩子方法或动态组合对象等

实际应用举例

比如我们有一个纺织类ERP销售系统,正在售卖一些刺绣类的商品,目前实现类图如下:

现在公司业务发展需要,进行一波打折促销,我们如何实现该需求呢?大致上有三种方法可以实现:

  • 修改现有的接口:在IGoods中增加一个打折的方法,专门用来打折,所有的实现类都要实现该方法。后果是类图中的所有角色参与修改,如果其他商品不打折也得实现打折方法。
  • 修改实现类:直接在刺绣类的逻辑中修改,这样也仅仅修改一个类,不对其他商品类产生影响。后果:如果有其他人员要查看原价,调用这个获取价格的方法就有问题了。
  • 通过扩展实现类:用新的实现类继承原来的实现类,覆盖getPrice方法,修改少风险小,推荐。

修改后的类图如下:

  • 其他设计方法:接口一开始就预留打折的方法,具体计算看配置;模板方法获取原价;修饰器模式等

原则建议

不要追求完美的开闭原则,就像现实不存在又帅又有钱又专一又浪漫又整天陪你又事业有成的男人。适度的抽象可以提高系统的灵活性,过度的抽象会大大提升系统的复杂度。

一般情况下我们会采用继承的方式来进行扩展。

带来的好处:在不用修改源代码(原来代码)的情况下,对功能进行扩展意味着不会影响到以前的功能代码,保障了系统升级新功能的可靠性。也大大减轻了测试的工作量。

可能的坏处:把握不当会陷入过度设计

使用难点:看起来是很简单,但事实上,一个系统要全部遵守开闭原则,几乎是不可能的,也没有这个必要。在对核心业务上进行适度的抽象,运用二八法则进行开闭原则的实施是一个不错的选择。

小结

除了以上介绍的六大原则,其实还设有其它一些OOD的设计思想,比如面向接口编程,优先使用组合而不是继承,数据应该隐藏在类内部,类之间最好只有传到耦合,在水平方向上尽可能统一地分布系统功能等等。

但是我们要清楚认识到设计原则是思想层面的高度概括,也只是一个建议指导。请结合自己实际情况根据系统规模和业务特点合理的使用它们。

Springboot教程系列(1)

hzqiuxm阅读(4027)

Springboot装配指南

要深刻了解Springboot的自动装配,我们还得从spring的各种装配开始讲起。

模式注解

模式注解概念

什么叫模式注解?点击查看官方介绍。
简而言之:模式注解是一种用于声明在应用中扮演“组件”角色的注解。如 Spring Framework 中的 @Service标注在任何类上 ,用于扮演服务角色的模式注解。
我们可以看下面几个常见的模式注解:

@Component 作为一种由 Spring 容器托管的通用模式组件,任何被 @Component 标注的组件均为组件扫描的候选对象。

其它几个组件我们看下他们的源码,以@Service为例子:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component //作为Service注解的元标注
public @interface Service {
 @AliasFor(annotation = Component.class) //属性互为别名
 String value() default "";
}

看来@Service 其实和@Component基本是等价的,就像类的继承派生一样,更具里氏替换原则,在使用@Service的地方我们完全都可以使用@Component

不过为了使我们Bean具备不同的角色,我们还是要按照它们扮演的角色那样去使用它们。其实我们Springboot工程启动类注解@SpringBootApplication也是模式注解,它也是基于@Component 多层次“派生”出来的。关系如下:

@SpringBootApplication => @SpringBootConfiguration => @Configuration => @Component
当然@SpringBootApplication其实是一个比较复杂的组合注解,其它注解就不展开叙述了,后面会单独谈一谈@SpringBootApplication

模式注解的装配

上面只是讲了通过@Component 及派生出来的其它模式注解是用来告诉Spring容器,将被它们标注过的类或方法等作为一个组件(Bean),那这些组件怎么被扫描装配呢?
在Spring中一般有两种方式:

  • 方式,通过xml配置文件

<beans ... 
<!-- 激活注解驱动 -->
<context:annotation-config />

<context:component-scan base-package="com.imooc.dive.in.spring.boot" />
</beans>
  • @ComponentScan 方式,通过注解
@ComponentScan(basePackages = "com.hzqiuxm.app")
public class SpringConfiguration {
...
}

自定义模式注解

熟悉了@Component作用原理和@Service等派生注解的作用,我们要实现一个自定义模式注解,可谓信手拈来:

/**
 * Copyright © 2018年 moc. All rights reserved.
 *
 * @author 临江仙 hzqiuxm@163.com
 * 自定义注解
 * @date 2019/1/17 11:30
 * @Since 1.0.0
 */
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component //替换成@Service 等也一样
public @interface MyFirstLevelComponent {
    String value() default "";
}

你还可以再定义个注解MySceondLevelComponent,使用@MyFirstLevelComponent 去标注它,这样你就派生出了一个有层次的自定义注解了:
@Component => @MyFirstLevelComponent => @MySceondLevelComponent 你可以规定不同业务层级使用不同的注解来规范架构设计

模块装配

试想一下,如果只有模式注解,那得一个个声明很多角色Bean,Spring Framework 3.1 开始支持”@Enable 模块驱动“。

所谓“模块”是指具备相同领域的功能组件集合, 组合所形成一个独立的单元。比如 Web MVC 模块、AspectJ代理模块、Caching(缓存)模块、JMX(Java 管 理扩展)模块、Async(异步处理)模块等。

Spring中常见框架@Enable模块举例:

蓝色属于Spring Framework ,绿色属于Springboot

模块装配实现方式

  • 注解驱动方式 ,比如:@EnableWebMvc,可以查看源码跟踪其实现

    EnableWebMvc注解类通过@Import一个@Configuration标注类,在@Configuration中通过@Bean来声明要生成的Bean,特点是比较简单固定

  • 接口编程的方式,比如:@EnableCaching,可以查看源码跟踪其实现

    EnableCaching注解类通过@Import一个实现了ImportSelector接口的某类,某类实现selectImports()方法完成Bean的生成,特点是叫复杂,可以根据逻辑来选择返回多个Bean

自定义Enable模块装配

  • 第1步:编写一个生成Bean的类
public class OneBeanConfiguration {
    @Bean
    public String oneBean(){
        return "one Bean is created!";
    }
}
  • 第2步:如果是接口编程方式还需要实现一个ImportSelector接口实现类,不是的话跳过这步
public class OneBeanImportSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[]{OneBeanConfiguration.class.getName()};
    }
}
  • 第3步:编写EnableXXX注解类,Import导入前面的实现类
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(OneBeanConfiguration.class) //注解方式
//@Import(OneBeanImportSelector.class) //接口实现方式
public @interface EnableOneBean {
}
  • 第4步:测试
@EnableOneBean  //加上Enable模块注解
public class OneBeanBootstrap {
    public static void main(String[] args) {
        ConfigurableApplicationContext context = new SpringApplicationBuilder(OneBeanBootstrap.class)
                .web(WebApplicationType.NONE)
                .run(args);
        String oneBean = context.getBean("oneBean", String.class);
        System.out.println("oneBean = " + oneBean);
        context.close();
    }
}

条件装配

接下来讲一下功能强大的条件装配,从 Spring Framework 3.1 开始,允许在 Bean 装配时增加前置条件判断。不过3.1的时候只支持@Profile注解方式,这种配置型的条件装配功能还不是很强大。到了4.0,引入了@Conditional编程条件方式,就相当灵活了

  • Profile的配置条件:通过在具体Bean上标注@Profile(”参数“),根据Spring容器根据参数来选择是否初始化该Bean
  • Conditional编程条件:通过实现Condition接口,通过内部的matches()方法来判断是否初始化,matches()方法返回boolean值

自定义条件装配(Conditional方式)

  • 第1步:自定义实现Condition接口的matches()方法,作为一种判断机制
public class OnCheckNameCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        //获取某注解类的属性值
        Map<String, Object> attributes = metadata.getAnnotationAttributes(ConditionalCheckName.class.getName());
        //获取name属性值
        String name = String.valueOf(attributes.get("name"));
        //为了实现方便我们直接拿name的值来和某个固定字符串做比较
        return "hzqiuxm".equals(name);
    }
}
  • 第2步:实现自定义条件注解类,使用@Conditional引入上一步的具体条件判断
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
@Documented
@Conditional(OnCheckNameCondition.class)
public @interface ConditionalCheckName {
    String name() default "";
}
  • 第3步:测试使用自定义注解类,构造条件是否满足判断机制来验证
public class ConditionBootstrap {
    @Bean
    @ConditionalCheckName(name="hzqiuxm") 
    public String testCondition(){
        return "测试条件装配";
    }
    public static void main(String[] args) {
        ConfigurableApplicationContext context = new SpringApplicationBuilder(ConditionBootstrap.class)
                .web(WebApplicationType.NONE)
                .run(args);
        String testCondition = context.getBean("testCondition", String.class);
        System.out.println("testCondition = " + testCondition);
        context.close();
    }
}

自动化装配

从模式注解装配到模块装配再到条件装配,我们总算快凑齐了自动化装配的所有龙珠。在 Spring Boot 中,自动装配是其三大特性之一。它基于约定大于配置的原则,实现中使用了:

  • Spring 模式注解装配
  • Spring @Enable 模块装配
  • Spring 条件装配
  • Spring 工厂加载机制

看完之后大家是不是觉得我们就差最后一块拼图工厂加载机制了?
工厂加载机制的机制也很简单:它由SpringFactoriesLoader类实现,在使用时需要进行资源配置(就是META-INF/spring.factories文件配置)

自动化配置实现

在前面几个例子的基础上,我们来做一个springboot自动化配置的例子(starter pom原理也是如此)

  • 第1步:激活自动装配 - @EnableAutoConfiguration
    写一个引导类,引导上加上@EnableAutoConfiguration
@EnableAutoConfiguration
public class EnableMyAutoConfigurationBootstrap {
    public static void main(String[] args) {
        ConfigurableApplicationContext context = new SpringApplicationBuilder(EnableMyAutoConfigurationBootstrap.class)
                .web(WebApplicationType.NONE)
                .run(args);
        //检查Enable装配的oneBean
        String oneBean = context.getBean("oneBean", String.class);
        System.out.println("oneBean = " + oneBean);
        context.close();
    }
}
  • 第2步:实现自动装配 - XXXAutoConfiguration
    创建MyAutoConfiguration类,使用上模式注解、模块装配、条件装配
@Configuration //模式注解装配
@EnableOneBean //模块装配
@ConditionalCheckName(name = "hzqiuxm")//条件装配满足条件才会去自动装配
public class MyAutoConfiguration {
}
  • 第3步:配置自动装配实现 - META-INF/spring.factories
    在resource目录下新建目录和文件META-INF和spring.factories,在文件中添加一对key/value值,key是固定的,value是第2步自动装配类的全路径
#自动装配
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.springboot2.moc.configuration.MyAutoConfiguration

使用第1步的引导类进行测试

小结

从整理装配的历程来看,我们可以看到任何事务的发展都不是一蹴而就的,springboot的自动装配经历了最早的模式注解装配,让各组件的角色可以分开。

在工程架构中,我们可以使用不同的模式注解来代表不同的组件,和我们分层架构互相结合;因为考虑到功能中模块化,所以发展除了Enable为前缀的模块装配方式,使得一组功能相近的Bean可以一起初始化生成;再后来为了灵活性引入了@Profile条件配置,但是因为Profile灵活性不够,只能已配置方式进行,所以后来又加入了@Conditional编程条件配置,最大化话满足条件配置。

最后结合Spring 工厂加载机制,实现了目前的自动化配置。弄清楚装配发展历程,对我们阅读spring源码也是十分有帮助,我们会清楚的知道各个地方这么实现的原因与局限性,也是我们以后做自定义扩展的基础。

欢迎加入极客江湖

进入江湖关于作者