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

DDD领域驱动战略篇(2)

hzqiuxm阅读(8058)

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阅读(9082)

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阅读(3008)

基于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阅读(5546)

研发效能破局之道

研发效能综述

研发效能模型与理解

  • 简单来说,就是开发者是否能够长期既快又准地产生用户价值。
  • 包括有效性(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阅读(4204)

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阅读(4796)

小谈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阅读(4868)

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阅读(3985)

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源码也是十分有帮助,我们会清楚的知道各个地方这么实现的原因与局限性,也是我们以后做自定义扩展的基础。

Elasticsearch简明教程(1)

hzqiuxm阅读(3724)

ElasticSearch简介与安装入门

ElasticSearch简介

概述

Elasticsearch是一个基于Lucene实现的、(准)实时、分布式的全文搜索和分析引擎。

准实时,意味着有轻微的延迟(通常为1秒)就可以从入库建索引文件到能够进行关键字搜索。

作用

ES主要提供全文搜索、结构化搜索以及分析的功能,并能将这三者混合使用

特性

  • 支持RESTful风格的接口
  • 输入输出支持JSON风格
  • 分布式索引、搜索
  • 索引自动分片、负载均衡
  • 自动发现机器、组建集群
  • 高性能、高可扩展性、高可用提供复制机制
  • 使用简单,快速上手

ElasticSearch 安装

1:去官网下载最新的版本:https://www.elastic.co/products/elasticsearch,这里用的是目前最新版6.4.1
2:Windows下直接解压后就能使用
3:在CentOS上安装ES
(1)解压,然后拷贝到你要放置的位置
(2)ES在linux上不能用root启动,创建ES的用户和组:
groupadd es
useradd es -g es -p es

(3)把ES安装的文件夹的所属用户和组修改为上面创建的用户和组:chown -R es:es elasticsearch-2.3.4
(4)切换用户到es,然后就可以启动ES了: su es
(5)如果想要外部能访问,需要修改es绑定的network.host地址为你安装的服务器地址,想要后台运行,可以用-d

检查是否安装成功:访问host:9200,例如:http://192.168.52.128:9200/
看到返回以下类似内容就表示安装成功了:

{
  "name" : "iYLxRzi",
  "cluster_name" : "elasticsearch",
  "cluster_uuid" : "SXIJqTdrRDiI2p5enqO7Dw",
  "version" : {
    "number" : "5.6.8",
    "build_hash" : "688ecce",
    "build_date" : "2018-02-16T16:46:30.010Z",
    "build_snapshot" : false,
    "lucene_version" : "6.6.1"
  },
  "tagline" : "You Know, for Search"
}

启动中可能出现的错误参考下面几个博文来进行解决,链接如下:
https://blog.csdn.net/u012371450/article/details/51776505
https://www.jianshu.com/p/4c6f9361565b
https://www.cnblogs.com/yidiandhappy/p/7714481.html

常用插件安装

Head

1:直接到ES安装路径下的bin里,运行
./plugin install mobz/elasticsearch-head
2:打开http://server的ip:9200/_plugin/head/ 就可以看效果了

注意:6开头的版本后面不支持命令行安装了,不要参考以上操作
6开头版本以上的请参考:
http://www.mamicode.com/info-detail-2105773.html
https://blog.csdn.net/dyangel2013/article/details/79504516

安装好node后几个关键命令:

npm install -g grunt-cli
grunt -version 检测下安装成功没
wget  https://github.com/mobz/elasticsearch-head/archive/master.zip
unzip master.zip
npm install -g cnpm --registry=https://registry.npm.taobao.org  //安装依赖的时候用国内淘宝的镜像比较快
nohup grunt server & //后台启动,elasticsearch-head-master目录下
//elasticsearch.yml中最后添加
http.cors.enabled: true
http.cors.allow-origin: "*"

head插件展示示意图:

IK分词器

默认的分词器standard对中文分词效果不好,只是把所有中文字一个个分开而已
1:下载对应版本的ik:https://github.com/medcl/elasticsearch-analysis-ik

版本v5.5.1之前的

然后自己编译打包,生成jar包,需要修改一下pom文件,把最下面的

<executions>
<execution>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>

全部注释掉
2:把下载到ik解压到plugins
3:把生成的jar拷贝到plugins/elasticsearch-analysis-ik-master下面,同时还要拷入需要的依赖jar,commons-codec-1.9.jar、commons-logging-1.2.jar、httpclient-4.4.1.jar、httpcore-4.4.1.jar
4:在ik源码的main/resources里面,拷贝plugin-descriptor.properties到plugins/elasticsearch-analysis-ik-master下面,然后把里面的参数数据修改一下,参考如下:

description=ik_analyzer
version=1.9.4
name=ik_analyzer
site=false
jvm=true
classname=org.elasticsearch.plugin.analysis.ik.AnalysisIkPlugin
java.version=1.8
elasticsearch.version=2.3.4
isolated=false

5:修改es的config/elasticsearch.yml,在最后添加:

index.analysis.analyzer.ik.type : 'ik'
index.analysis.analyzer.default.tokenizer : 'ik'

然后就可以按照ik官方给的测试进行测试了

版本v5.5.1之后的

直接参考官方示例即可,安装很方便一个命令即可。注意插件的版本号和Elastic的版本号要一致。

./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v6.4.1/elasticsearch-analysis-ik-6.4.1.zip
IKAnalyzer.cfg.xml配置说明
<?xml version="1.0" encoding="UTF-8"?> 
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd"> 
<properties>
<comment>IK Analyzer 扩展配置</comment>
<!--用户可以在这里配置自己的扩展字典 -->
<entry key="ext_dict">custom/mydict.dic;custom/single_word_low_freq.dic</entry>
<!--用户可以在这里配置自己的扩展停止词字典-->
<entry key="ext_stopwords">custom/ext_stopword.dic</entry>
<!--用户可以在这里配置远程扩展字典 -->
<entry key="remote_ext_dict">location</entry>
<!--用户可以在这里配置远程扩展停止词字典-->
    <entry key="remote_ext_stopwords">http://xxx.com/xxx.dic</entry></properties>

注意事项

请确保你的扩展词典的文本格式为 UTF8 编码,每个词以换行符相隔。
IK 分词从 5.0.0 版本开始使用 ik_smart 和 ik_max_word 两种分词方式

  • ikmaxword:表示最细粒度拆分。优点是查询效果比较好。缺点是会产生很多碎片,对于大文本字段不建议使用 ik_max_word

例子:
将“中华人民共和国国歌”拆分为“中华人民共和国,中华人民,中华,华人,人民共和国,人民,人,民,共和国,共和,和,国国,国歌”,会穷尽各种可能的组合

  • ik_smart:表示最粗粒度拆分,优点是降低了索引存储。缺点是查询效果不好
    例:将“中华人民共和国国歌”拆分为“中华人民共和国,国歌”。这个时候输入“中华”是匹配不到的,只能匹配“中华人民共和国”或“国歌”。

pinyin分词器

对于很多的搜索场景,用户输入的有时候并非汉字,可能是拼音或者拼音首字母,这个时候我们同样要匹配到数据,就需要引入 pinyin 分词器。

./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-pinyin/releases/download/v6.4.1/elasticsearch-analysis-pinyin-6.4.1.zip

mavel

1: 在ES里面安装Marvel插件

./plugin install license
./plugin install marvel-agent

2:安装Kibana,解压然后拷贝到要放置的位置即可修改一下配置文件里面的elasticsearch.url
3:在Kibana里面安装Marvel插件
./kibana plugin --install elasticsearch/marvel/latest
4: 启动ES和Kibana
5: 然后就可以到http://server的ip:5601/app/marvel

简单官网示例测试步骤

  • 创建索引
curl -XPUT http://192.168.0.57:9200/index 
  • 创建映射
curl -XPOST http://192.168.0.57:9200/index/fulltext/_mapping -H 'Content-Type:application/json' -d'
{
        "properties": {
            "content": {
                "type": "text",
                "analyzer": "ik_max_word",
                "search_analyzer": "ik_max_word"
            }
        }

}'
  • 创建数据
curl -XPOST http://192.168.0.57:9200/index/fulltext/1 -H 'Content-Type:application/json' -d'
{"content":"美国留给伊拉克的是个烂摊子吗"}
'


curl -XPOST http://192.168.0.57:9200/index/fulltext/2 -H 'Content-Type:application/json' -d'
{"content":"公安部:各地校车将享最高路权"}
'

curl -XPOST http://192.168.0.57:9200/index/fulltext/3 -H 'Content-Type:application/json' -d'
{"content":"中韩渔警冲突调查:韩警平均每天扣1艘中国渔船"}
'
curl -XPOST http://192.168.0.57:9200/index/fulltext/4 -H 'Content-Type:application/json' -d'
{"content":"中国驻洛杉矶领事馆遭亚裔男子枪击 嫌犯已自首"}
'
  • 执行查询
curl -XPOST http://192.168.0.57:9200/index/fulltext/_search  -H 'Content-Type:application/json' -d'
{
    "query" : { "match" : { "content" : "中国" }},
    "highlight" : {
        "pre_tags" : ["<tag1>", "<tag2>"],
        "post_tags" : ["</tag1>", "</tag2>"],
        "fields" : {
            "content" : {}
        }
    }
}
'
  • 返回结果
{
    "took": 14,
    "timed_out": false,
    "_shards": {
        "total": 5,
        "successful": 5,
        "failed": 0
    },
    "hits": {
        "total": 2,
        "max_score": 2,
        "hits": [
            {
                "_index": "index",
                "_type": "fulltext",
                "_id": "4",
                "_score": 2,
                "_source": {
                    "content": "中国驻洛杉矶领事馆遭亚裔男子枪击 嫌犯已自首"
                },
                "highlight": {
                    "content": [
                        "<tag1>中国</tag1>驻洛杉矶领事馆遭亚裔男子枪击 嫌犯已自首 "
                    ]
                }
            },
            {
                "_index": "index",
                "_type": "fulltext",
                "_id": "3",
                "_score": 2,
                "_source": {
                    "content": "中韩渔警冲突调查:韩警平均每天扣1艘中国渔船"
                },
                "highlight": {
                    "content": [
                        "均每天扣1艘<tag1>中国</tag1>渔船 "
                    ]
                }
            }
        ]
    }
}

Docker简明教程(11)

hzqiuxm阅读(2762)

Docker容器链接与编排

容器链接

前面学习到的端口映射,并不是唯一把docker连接到另一个容器的方法。

docker有一个连接系统允许将多个容器连接在一起,共享连接信息。
docker连接会创建一个父子关系,其中父容器可以看到子容器的信息。
有时出于安全原因,可以强制docker只允许有连接的容器之间互相通信,可以在启动docker守护进程的时候,加上--icc=false,关闭没有连接的容器间通信。

在docker run的时候, 指定--link :标志创建了两个容器间的父子连接,这样容器就不用暴露端口了,大大增加安全性。

基本使用

基本语法:--link 要连接的容器的名字:这个链接的别名

1:执行连接需要依靠容器的名字,因此创建每一个容器的时候,请使用--name来命名。

注意:容器的名称必须是唯一的。如果想使用重复的名称来命名容器,需要使用docker rm命令删除以前的容器。

2:被连接的容器必须运行在同一个Docker宿主机上
示例:

docker run --name db -e MYSQL_ROOT_PASSWORD=123321qq -d mysql
docker run -d -p 9080:8080 --name web --link db:dblink cctomcat:9.0

3:可以通过docker inspect查看里面的Links,如: "/db:/web/dblink"
如果启动的时候,出现类似如下的错误:

COMMAND_FAILED: '/usr/sbin/iptables -w2 -t nat -A DOCKER -p tcp -d 0/0 --dport
9080 -j DNAT --to-destination 172.17.0.4:8080 ! -i docker0' failed:

这可能是网络问题造成,解决方法如下:
1:首先先验证docker容器内部网络是否能ping通宿主机如果能ping通,即可通过重建docker0网络恢复
2:先停掉宿主机上运行的docker容器,然后执行以下命令

iptables -t nat -F
ifconfig docker0 down
brctl delbr docker0

3:重启docker服务

使用容器连接来通信

最简单的方法就是在子容器里面,也就是web里面,直接使用link的别名来代替具体的host或者是ip地址,比如:
jdbc:mysql://dblink:3306/mydb

容器编排

编排简介

Docker的最佳实践建议:一个容器只运行一个进程。而实际的应用会由多个组件构成,要运行多个组件就需要运行多个容器,这就需要对这多个容器进行编排。
所谓编排:主要就是多个docker容器的自动配置、协作和管理服务的过程。Docker提供了docker-compose工具来实现。

Docker-compose简介

compose是用来定义和运行一个或多个容器应用的工具,使用python开发,通过yml文件来定义多个容器应用,非常适合在单机环境下部署一个或多个容器,并自动把多个容器互相关联起来。

其实,docker-compose做的就相当于解析配置文件,然后按照配置去执行一系列的docker命令。

Docker-compose安装

官方安装文档:https://docs.docker.com/compose/install/

Docker-compose基本示例

1:准备好要启动的镜像,虽然可以直接在compose里面build镜像,建议还是先准备好
2:编写docker-compose.yml
3:然后就docker-compose up -d,启动就好了
4:docker-compose.yml示例如下:

version: '2'
services:
  mysqldb:
    image: 'mysql:5.7'
    environment:
     - MYSQL_ROOT_PASSWORD=123321qq
    volumes:
     - /home/dev/mysqldata:/var/lib/mysql
    privileged: true
web:
  image: 'cctomcat:9.0'
  ports:
   - "9080:8080"
  volumes:
   - /home/dev/tomcat9docker/webapps/test:/usr/local/tomcat/webapps/test
  privileged: true
  links:
   - mysqldb:dblink

Docker-compose yml文件的配置

1:一份标准配置文件可以包含version、services、networks 三大部分,详细的参照指南见官方网站:https://docs.docker.com/compose/compose-file/
2:version目前是有1,2,3这么三个
3:srvices常见的配置有:
(1)服务名称:用来表示一个服务,自定义的
(2)image:指定服务的镜像名称或镜像ID。如果镜像在本地不存在,Compose 将会尝试拉取这个镜像,Build和image必须使用一个。
(3)build:服务除了可以基于指定的镜像,还可以基于一份Dockerfile,在使用up 启动之时执行构建任务,这个构建标签就是build,它可以指定Dockerfile 所在文件夹的路径。Compose 将会利用它自动构建这个镜像,然后使用这个镜像启动服务容器。如果你同时指定了image 和build 两个标签,那么Compose 会构建镜像并且把镜像命名为image 后面的那个名字。
(4)args:类似Dockerfile 中的ARG 指令,可以在构建过程中指定环境变量,构建成功后取消
(5)command:使用command 可以覆盖容器启动后默认执行的命令
(6)container_name:自定义容器的名称
(7)links:指定与其它容器的连接,与Docker client的--link一样效果
(8)volumes:将host主机上的路径或文件,挂载到容器中
(9)ports:将host主机的端口映射到容器的某个端口
(10)environment:设置环境变量, 与Dockerfile 中的ENV 指令一样会把变量一直保存在镜像、容器中,类似docker run -e 的效果
(11)privileged:设置挂载目录的权限
(12)depends_on:一般项目容器启动的顺序是有要求的,可以用depends_on来解决容器的依赖、启动先后的问题。

Docker-compose 的networks配置

容器间的通讯,除了使用--link外,现在更推荐使用自定义网络,然后利用服务名进行通讯。每个自定义网络都可以配置很多东西,包括网络所使用的驱动、网络地址范围等设置。例如:

networks:
frontend:
backend:

1:你会看到frontend、backend后面是空的,这是指一切都使用默认,换句话说,在单机环境中,将意味着使用bridge 驱动;而在Swarm 环境中,使用overlay 驱动,而且地址范围完全交给Docker 引擎决定。

2:然后在每个services配置里面,也有一个networks,用来指定服务要连接到哪些网络上,可以指定多个,例如:

services:
  nginx:
...
  networks:
   - frontend
web:
...
  networks:
   - frontend
   - backend
mysql:
...
  networks:
   - backend

3:连接到同一个网络的容器,可以进行互连;而不同网络的容器则会被隔离。
4:处于同一网络的容器,可以使用服务名访问对方
5:给前面的例子添加networks的配置,如下:

version: '2'
services:
  mysqldb:
    image: 'mysql:latest'
    environment:
     - MYSQL_ROOT_PASSWORD=cc
    volumes:
     - /home/dev/mysqldata:/var/lib/mysql
    privileged: true
    networks:
     - frontend
web:
  image: 'cctomcat:9.0'
  ports:
   - "9080:8080"
  volumes:
   - /home/dev/tomcat9docker/webapps/test:/usr/local/tomcat/webapps/test
  privileged: true
  links:
   - mysqldb:dblink
  networks:
   - frontend
networks:
  frontend:
  backend:

欢迎加入极客江湖

进入江湖关于作者