欢迎光临
我们一直在努力

拆解大数据总线平台DBus的系统架构

mumupudding阅读(4)

拓展阅读:

如何基于日志,同步实现数据的一致性和实时抽取?

快速部署DBus体验实时数据流计算

Dbus所支持两类数据源的实现原理与架构拆解。

大体来说,Dbus支持两类数据源:

  • RDBMS数据源
  • 日志类数据源

一、RMDBMS类数据源的实现

以mysql为例子. 分为三个部分:

  • 日志抽取模块
  • 增量转换模块
  • 全量拉取模块

1.1 日志抽取模块(Extractor)

mysql 日志抽取模块由两部分构成:

  • canal server:负责从mysql中抽取增量日志。
  • mysql-extractor storm程序:负责将增量日志输出到kafka中,过滤不需要的表数据,保证at least one和高可用。

我们知道,虽然mysql innodb有自己的log,mysql主备同步是通过binlog来实现的。而binlog同步有三种模式:Row 模式,Statement 模式,Mixed模式。因为statement模式有各种限制,通常生产环境都使用row模式进行复制,使得读取全量日志成为可能。

通常我们的mysql布局是采用 2个master主库(vip)+ 1个slave从库 + 1个backup容灾库 的解决方案,由于容灾库通常是用于异地容灾,实时性不高也不便于部署。

为了最小化对源端产生影响,我们读取binlog日志从slave从库读取。

读取binlog的方案比较多,DBus也是站在巨人的肩膀上,对于Mysql数据源使用阿里巴巴开源的Canal来读取增量日志。这样做的好处是:

  • 不用重复开发避免重复造轮子
  • 享受canal升级带来的好处

关于Canal的介绍可参考:https://github.com/alibaba/canal/wiki/Introduction 由于canal用户抽取权限比较高,一般canal server节点也可以由DBA组来维护。

日志抽取模块的主要目标是将数据从canal server中读出,尽快落地到第一级kafka中,避免数据丢失(毕竟长时间不读日志数据,可能日志会滚到很久以前,可能会被DBA删除),因此需要避免做过多的事情,主要就做一下数据拆包工作防止数据包过大。

从高可用角度考虑,在使用Canal抽取过程中,采用的基于zookeeper的Canal server高可用模式,不存在单点问题,日志抽取模块extractor也使用storm程序,同样也是高可用架构。

不同数据源有不同的日志抽取方式,比如oracle,mongo等都有相应的日志抽取程序。

DBus日志抽取模块独立出来是为了兼容这些不同数据源的不同实现方式。

1.2 增量转换模块(Stream)

增量数据处理模块,根据不同的数据源类型的格式进行转换和处理。

1)分发模块dispatcher

  • 将来自数据源的日志按照不同的schema分发到不同topic上。这样做的目的
  • 是为了数据隔离(因为一般不同的shema对应不同的数据库)
  • 是为了分离转换模块的计算压力,因为转换模块计算量比较大,可以部署多个,每个schema一个提高效率。

2)转换模块appender

  • 实时数据格式转换:Canal数据是protobuf格式,需要转换为我们约定的UMS格式,生成唯一标识符ums_id和ums_ts等;
  • 捕获元数据版本变更:比如表加减列,字段变更等,维护版本信息,发出通知触发告警
  • 实时数据脱敏:根据需要对指定列进行脱敏,例如替换为***,MD5加盐等。
  • 响应拉全量事件:当收到拉全量请求时为了保证数据的相应顺序行,会暂停拉增量数据,等全量数据完成后,再继续。
  • 监控数据:分发模块和转换模块都会响应心跳event,统计每一张表在两次心跳中的数据和延时情况,发送到statistic作为监控数据使用。
  • 分发模块和转换模块都会相应相关reload通知事件从Mgr库和zk上进行加载配置操作。

1.3 全量拉取模块(FullPuller)

全量拉取可用于初始化加载(Initial load), 数据重新加载,实现上我们借鉴了sqoop的思想。将全量过程分为了2 个部分:

1)数据分片

分片读取max,min,count等信息,根据片大小计算分片数,生成分片信息保存在split topic中。下面是具体的分片策略:

以实际的经验,对于mysql InnDB,只有使用主键索引进行分片,才能高效。因为mysql innDB的主键列与数据存储顺序一致。

2)实际拉取

每个分片代表一个小任务,由拉取转换模块通过多个并发度的方式连接slave从库进行拉取。 拉取完成情况写到zookeeper中,便于监控。

全量拉取对源端数据库是有一定压力的,我们做法是:

  • 从slave从库拉取数据
  • 控制并发度6~8
  • 推荐在业务低峰期进行

全量拉取不是经常发生的,一般做初始化拉取一次,或者在某种情况下需要全量时可以触发一次。

1.3 全量和增量的一致性

在整个数据传输中,为了尽量的保证日志消息的顺序性,kafka我们使用的是1个partition的方式。在一般情况下,基本上是顺序的和唯一的。 但如果出现写kafka异步写入部分失败, storm也用重做机制,因此,我们并不严格保证exactly once和完全的顺序性,但保证的是at least once。

因此ums_id_变得尤为重要。 对于全量抽取,ums_id是一个值,该值为全量拉取event的ums_id号,表示该批次的所有数据是一批的,因为数据都是不同的可以共享一个ums_id_号。ums_uid_流水号从zk中生成,保证了数据的唯一性。 对于增量抽取,我们使用的是 mysql的日志文件号 + 日志偏移量作为唯一id。Id作为64位的long整数,高6位用于日志文件号,低13位作为日志偏移量。 例如:000103000012345678。 103 是日志文件号,12345678 是日志偏移量。 这样,从日志层面保证了物理唯一性(即便重做也这个id号也不变),同时也保证了顺序性(还能定位日志)。通过比较ums_id_就能知道哪条消息更新。

ums_ts_的价值在于从时间维度上可以准确知道event发生的时间。比如:如果想得到一个某时刻的快照数据。可以通过ums_ts 来知道截断时间点。

二、日志类数据源的实现

业界日志收集、结构化、分析工具方案很多,例如:Logstash、Filebeat、Flume、Fluentd、Chukwa. scribe、Splunk等,各有所长。在结构化日志这个方面,大多采用配置正则表达式模板:用于提取日志中模式比较固定、通用的部分,例如日志时间、日志类型、行号等。对于真正的和业务比较相关的信息,这边部分是最重要的,称为message部分,我们希望使用可视化的方式来进行结构化。

例如:对于下面所示的类log4j的日志:

如果用户想将上述数据转换为如下的结构化数据信息:

我们称这样的日志为“数据日志”

DBUS设计的数据日志同步方案如下:

  • 日志抓取端采用业界流行的组件(例如Logstash、Flume、Filebeat等)。一方面便于用户和业界统一标准,方便用户的整合;另一方面也避免无谓的重造轮子。抓取数据称为原始数据日志(raw data log)放进Kafka中,等待处理。
  • **提供可视化界面,配置规则来结构化日志。**用户可配置日志来源和目标。同一个日志来源可以输出到多个目标。每一条“日志源-目标”线,中间数据经过的规则处理用户根据自己的需求来自由定义。最终输出的数据是结构化的,即:有schema约束,可以理解为类似数据库中的表。
  • 所谓规则,在DBUS中,即**“规则算子”**。DBUS设计了丰富易用的过滤、拆分、合并、替换等算子供用户使用。用户对数据的处理可分多个步骤进行,每个步骤的数据处理结果可即时查看、验证;可重复使用不同算子,直到转换、裁剪得到自己需要的数据。
  • 将配置好的规则算子组运用到执行引擎中,对目标日志数据进行预处理,形成结构化数据,输出到Kafka,供下游数据使用方使用。

系统流程图如下所示:

根据配置,我们支持同一条原始日志,能提取为一个表数据,或者可以提取为多个表数据。

每个表是结构化的,满足相同的schema。

  • 每个表是一个规则 算子组的合集,可以配置1个到多个规则算子组
  • 每个规则算子组,由一组规则算子组合而成

拿到一条原始数据日志, 它最终应该属于哪张表呢?

每条日志需要与规则算子组进行匹配:

  • 符合条件的进入规则算子组的,最终被规则组转换为结构化的表数据。
  • 不符合的尝试下一个规则算子组。
  • 都不符合的,进入unknown_table表。

2.1 规则算子

规则算子是对数据进行过滤、加工、转换的基本单元。常见的规则算子如下:

算子之间是独立的,通过组合不同的算子达到更复杂的功能,对算子进行迭代使用最终达到对任意数据进行加工的目的。

我们试图使得算子尽量满足正交性或易用性(虽然正则表达式很强大,但我们仍然开发一些简单算子例如trim算子来完成简单功能,以满足易用性)。

三、UMS统一消息格式

无论是增量、全量还是日志,最终输出到结果kafka中的消息都是我们约定的统一消息格式,称为UMS(unified message schema)格式。如下图所示:

3.1 Protocol

数据的类型,被UMS的版本号

3.2 schema

1)namespace 由:类型. 数据源名.schema名 .表名.表版本号. 分库号 .分表号 组成,能够描述所有表。

例如:mysql.db1.schema1.testtable.5.0.0

2)fields是字段名描述。

  • ums_id_ 消息的唯一id,保证消息是唯一的
  • ums_ts_ canal捕获事件的时间戳;
  • ums_op_ 表明数据的类型是I (insert),U (update),B (before Update),D(delete)
  • ums_uid_ 数据流水号,唯一值

3)payload是指具体的数据。

一个json包里面可以包含1条至多条数据,提高数据的有效载荷。

四、心跳监控和预警

RDBMS类系统涉及到数据库的主备同步,日志抽取,增量转换等多个模块等。

日志类系统涉及到日志抽取端,日志转换模模块等。

如何知道系统正在健康工作,数据是否能够实时流转? 因此对流程的监控和预警就尤为重要。

4.1 对于RDBMS类系统

心跳模块从dbusmgr库中获得需要监控的表列表,以固定频率(比如每分钟)向源端dbus库的心跳表插入心跳数据(该数据中带有发送时间),该心跳表也作为增量数据被实时同步出来,并且与被同步表走相同的逻辑和线程(为了保证顺序性,当遇到多并发度时是sharding by table的,心跳数据与table数据走同样的bolt),这样当收到心跳数据时,即便没有任何增删改的数据,也能证明整条链路是通的。

增量转换模块和心跳模块在收到心跳包数据后,就会发送该数据到influxdb中作为监控数据,通过grafana进行展示。 心跳模块还会监控延时情况,根据延时情况给以报警。

4.2 对于日志类系统

从源端就会自动产生心跳包,类似RDBMS系统,将心跳包通过抽取模块,和算子转换模块同步到末端,由心跳模块负责监控和预警。

来源:宜信技术学院

Flutter之 State 生命周期

mumupudding阅读(4)

State 的生命周期,指的是在用户参与的情况下,其关联的 Widget 所经历的,从创建到显示,再到更新最后到停止,直至销毁等各个阶段

不同的阶段涉及到特定的任务处理

State 的生命周期流程如下图所示

file

由图可知:State 的生命周期可以分为三个阶段:创建(插入视图树)、更新(在视图树中存在)、销毁(从视图树中移除)

创建

State 初始化时会依次执行:构造方法 -> initState -> didChangeDependencies -> build,随后完成页面渲染

  • 构造方法:State 生命周期的起点,Flutter 会通过调用 StatefulWidget.createState() 来创建一个 State。可以通过构造方法,来接收父 Widget 传递的初始化 UI 配置数据,而这些配置数据,决定了 Widget 最初的呈现状态
  • initState:在 State 对象被插入视图树时调用。在 State 的生命周期中只会被调用一次,因此可以在 initState 函数中做一些初始化操作
  • didChangeDependencies:专门用来处理 State 对象依赖关系变化,会在 initState() 调用结束后调用
  • build:构建视图。经过构造方法、initState、didChangeDependencies 后,Framework 认为 State 已经准备就绪,于是便调用 build。在 build 中,需要根据父 Widget 传递过来的初始化配置数据及 State 的当前状态,创建一个 Widget 然后返回
更新

Widget 的状态更新,主要由 setState、didChangeDependencies 和 didUpdateWidget 触发

  • setState:当状态数据发生变化时,可以通过调用 setState 方法告诉 Flutter 使用更新后数据重建 UI
  • didChangeDependencies:State 对象的依赖关系发生变化后,Flutter 会回调该方法,随后触发组件构建。State 对象依赖关系发生变化的典型场景:系统语言 Locale 或应用主题改变时,系统会通知 State 执行 didChangeDependencies 回调方法
  • didUpdateWidget:Widget 的配置发生变化时,或热重载时,系统会回调该方法

一旦这三个方法被调用,Flutter 随后便会销毁旧的 Widget,并调用 build 方法重建 Widget

销毁

组件销毁相对创建和更新而言更简单。比如页面销毁时或是组件被移除时,系统会调用 deactivate 和 dispose 这两个方法,来移除或销毁组件

  • 当组件的可见状态发生变化时,deactivate 方法会被调用,这时 State 会被暂时从视图树中移除。注意:页面切换时,由于 State 对象在视图树中的位置发生了变化,需要先暂时移除后再重新添加,重新触发组件构建,因此也会调用 deactivate 方法
  • 当 State 被永久地从视图树中移除时,Flutter 会调用 dispose 方法,而一旦 dispose 方法被调用,组件就要被销毁了,因此可以在 dispose 方法中进行最终的资源释放、移除监听、清理环境等工作

file

file

全方位详解Service Mesh(服务网格)

mumupudding阅读(6)

Service mesh是近几年才出现的一个新兴概念。它可以解决微服务之间通信愈发复杂的问题。那么什么是Service mesh?它有什么具体的功能?它的架构又是如何的呢?它与Kubernetes的关系是怎样的?所有答案戳文了解!


在数字化转型的旗帜下,IT界的一大变化是大型单体应用程序被分解为微服务架构,即小型、离散的功能单元,并且这些应用程序在容器中运行。包含所有服务代码以及依赖项的软件包被隔离起来,并且能轻松从一个服务器迁移到另一个。

像这样的容器化架构很容易在云中扩展和运行,并且能够快速迭代和推出每个微服务。然而,当应用程序越来越大并且在同一个服务上同时运行多个实例时,微服务之间通信将会变得愈发复杂。Service mesh的出现将解决这一问题,它是一个新兴的架构形式,旨在以减少管理和编程开销的形式来连接这些微服务。

什么是Service mesh?

关于Service mesh的定义,最为广泛接受的观点是:它是一种控制应用程序不同部分彼此共享数据的方式。这一描述包含了service mesh的方方面面。事实上,它听起来更像是大多数开发人员从客户端-服务器应用程序中熟悉的中间件。

Service mesh也有其独特之处:它能够适应分布式微服务环境的独特性质。在搭建在微服务中的大规模应用程序中,有许多既定的服务实例,它们跨本地和云服务器运行。所有这些移动部件显然使得各个微服务难以找到他们需要与之通信的其他服务。Service mesh可以在短时间内自动处理发现和连接服务,而无需开发人员以及各个微服务自行匹配。

我们可以将service mesh等同为软件定义网络(SDN)的OSI网络模型第7层。正如SDN创建一个抽象层后网络管理员不必处理物理网络连接,service mesh将解耦在抽象架构中的与你交互的应用程序的底层基础架构。

随着开发人员开始努力解决真正庞大的分布式架构的问题,service mesh的概念适时地出现了。这一领域的第一个项目是Linkerd,它一开始是Twitter内部项目的一个分支。Istio是另一个十分流行的service mesh项目,它起源于Lyft,现在这一项目获得了许多企业的支持。

Service mesh负载均衡

Service mesh其中一个关键功能是负载均衡。我们常常将负载均衡视为网络功能——你想要防止服务器或网络链接被流量淹没,因此相应地你会路由你的数据包,而Service mesh在应用程序层面也在执行类似的事情。

本质上,Service mesh的工作之一是跟踪分布在基础设施上的各种微服务的哪些实例是“最健康的”。它可能对他们进行调查来查看它们如何工作的或跟踪哪些实例对服务请求响应缓慢并将后续请求发送到其他实例。此外,service mesh也会为网络路由做类似的工作,如果发现当消息需要很长时间才能送达,那么service mesh将会采用其他路由进行补偿。这些减速可能是由于底层硬件出现问题,或者仅仅是由于服务因请求过载或处理能力不足导致的。但没有关系,service mesh会找到另一个相同服务的实例,然后将其路由以替代响应缓慢的实例,高效利用了整个应用程序的资源。

Service mesh vs Kubernetes

如果你稍微熟悉基于容器的架构,你可能会想Kubernetes这个流行的开源容器编排平台能否适合这种情况。毕竟,Kubernetes不就是管理着你的容器之间如何互相通信的吗?你可将Kubernetes“服务”资源视为非常基础的service mesh,因为它提供服务发现和请求的轮询调度均衡。但是完整的service mesh则提供更丰富的功能,如管理安全策略和加密、“断路”以暂停对缓慢响应的实例的请求以及如上所述的负载均衡等。

请记住,大多数service mesh确实需要像Kubernetes这样的编排系统。Service mesh只是提供扩展功能,而非替代编排平台。

Service mesh vs API 网关

每个微服务都会提供一个API,它会作为其他服务与其通信的手段。这引发了service mesh与其他更传统的API管理形式(如API网关)之间的差异问题。API网关位于一组微服务和“外部”世界之间,它根据需要路由服务请求,以便请求者不需要知道它正在处理基于微服务的应用程序即可完成请求。而service mesh调解微服务应用程序内部的请求,各种组件完全了解其环境。

另一方面,service mesh用于优化集群内东西流量(server-server流量),API网关用于进出集群的南北流量(server-client流量)。但service mesh目前依旧处于早期阶段还在不断发展变化中。许多service mesh(包括Linkerd和Istio)现在已经可以提供南北功能。

Service mesh 架构

Service mesh这一概念其实出现的时间并不长,并且已经有相当数量的不同的方法来解决“service mesh”的问题,如管理微服务通信。目前,确定了三种service mesh创建的通信层可能存在的位置:

  • 每个微服务导入的library

  • 在特定节点提供服务给所有容器的节点agent

  • 与应用程序容器一起运行的sidecar容器

基于sidecar的模式目前是service mesh最受欢迎的模式之一,以至于它在某种程度上已经成为了service mesh的代名词。尽管这种说法并不严谨,但是sidecar已经引起了很大的关注,我们将在下文更详细地研究这一架构。

Sidecar

Sidecar容器与你的应用程序容器一起运行意味着什么呢?在这类service mesh中每个微服务容器都有另一个proxy容器与之相对应。所有的服务间通信的需求都会被抽象出微服务之外并且放入sidecar。

这似乎很复杂,毕竟你有效地将应用程序中的容器数量增加了1倍。但你使用的这一种设计模式对于简化分布式应用程序至关重要。通过将所有的网络和通信代码放到单独的容器中,将其作为基础架构的一部分,并使开发人员无需将其作为应用程序的一部分实现。

本质上,你所留下的是一个聚焦于业务逻辑的微服务。这个微服务不需要知道如何在其运行的环境中与所有其他服务进行通信。它只需要知道如何与sidecar进行通信即可,剩下的将由sidecar完成。

Service mesh明星项目:Linkerd、Envio、Istio、Consul

那么说了这么多,什么是可用的service mesh呢?目前,这一领域还没有出现完全现成的商业产品。大部分的service mesh只是开源项目,需要通过一定的操作步骤才能实现,现在比较知名的项目有:

  • Linkerd:2016年发布,是这些项目中最老的。Linkerd是从Twitter开发的library中分离出来的。在这一领域另一位重量型选手,Conduit,已经进入了Linkerd项目并构成了Linkerd 2.0的基础。

  • Envoy:由Lyft创建,为了能够提供完整的service mesh功能,Envoy占据“数据平面”的部分,与其进行匹配。

  • Istio:由Lyft、IBM与google联合开发,Istio可以在不修改微服务源代码的情况下,轻松为其加上如负载均衡、身份验证等功能,它可以通过控制Envoy等代理服务来控制所有的流量。此外,Istio提供容错、金丝雀部署、A/B测试、监控等功能,并且支持自定义的组件和集成。Rancher 2.3 Preview2版本上开始支持Istio,用户可以直接在UI界面中启动Istio并且可以为每个命名空间注入自动sidecar。Rancher内置了一个支持Kiali的仪表盘,简化Istio的安装和配置。这一切让部署和管理Istio变得简单而快速。

  • HashiCorp Consul:与Consul 1.2一起推出了一项名为Connect的功能,为HashiCorp的分布式系统添加了服务加密和基于身份的授权,可用于服务发现和配置。

哪个service mesh适合你?如果要进行一个全面的比较的话,超出了本文所涉及的范围。但上述的所有产品都已经在大型且严苛的环境中得到验证。目前,Linkerd和Istio包含最丰富的功能集,但一切都还在迅速发展中,现在下定论还为时过早。

网络七层模型与TCP/UDP

mumupudding阅读(5)

        为了使全球范围内不同的计算机厂家能够相互之间能够比较协调的进行通信,这个时候就有必要建立一种全球范围内的通用协议,以规范各个厂家之间的通信接口,这就是网络七层模型的由来。本文首先会对网络七层模型的功能进行介绍,然后会讲解传输层的两个重要协议:TCP和UDP协议,并且会着重讲解TCP协议中的三次握手和四次挥手的过程。

1. 网络七层模型

        关于网络七层模型,我们首先以一个图例来展示其功能:

  • 应用层:主要指的是应用程序部分,比如我们的Java程序,应用层所产生的数据成为应用层数据,典型的应用层协议,比如有HTTP协议,dubbo的rpc协议,这些都是由我们的应用层程序自己定义的;
  • 表示层:这一层主要是对应用层的数据进行一些格式转换,加解密或者进行压缩和解压缩的功能;
  • 会话层:会话层的主要作用是负责进程与进程之间会话的建立、管理以及终止的服务;
  • 传输层:传输层提供了两台机器之间端口到端口的一个数据传输服务,因为应用层、表示层和会话层所针对的都是某个应用进程,而进程是和端口绑定的,但是同一台服务器上是可以有多个进程的,因而传输层提供的就是这种不同的端口到端口的访问,以实现区分不同进程之间的通信服务。在传输层最典型的协议有TCP和UDP协议,TCP提供的是面向连接的、可靠的数据传输服务,而UDP则是无连接的、不可靠的数据传输服务。在上面的图中我们也可以看出,经过传输层之后,数据会被加上TCP或者UDP头部,用以实现不同传输层协议的功能;
  • 网络层:传输层提供的是同一台主机上的端口到端口的传输服务,而网络层则提供的是不同主机之间的连接服务,最典型的网络层协议就是IP协议,网络层会将当前的数据包加上一个IP头部,从而实现目标机器的寻址;
  • 数据链路层:这一层是承接软件和硬件的一层,由于其会将当前的数据报发送到不稳定的物理层硬件上进行传输,因而为了保障数据的完整性和可靠性,数据链路层就提供了校验、确认和反馈等机制,用以提供可靠的数据报传输服务;
  • 物理层:物理层的主要作用就是将0101这种二进制的比特流数据转换为光信号,用以在物理介质上进行传输。

        网络七层模型主要是提供的一种规范,而在这每一层上为了实现不同的功能,各个计算机厂商都会实现自己的协议,这些协议的标识就是通过一些协议头和进行的,比如上面图中,数据在经过每一层的封装之后都会为其加上自己的协议头部,当数据经过屋里介质传输到目标机器上后,其就会反过来,将数据进行一层一层的解析,解析的过程其实就是根据其每一层头部信息来实现该层的相关功能。

        另外,网络七层模型是一种比较理想化的模型,现在应用比较广泛的是网络五层模型,五层模型与七层模型的主要区别在于将应用层、表示层和会话层统一划分到应用层中了,由应用程序实现其相关的功能。

2. TCP与UDP

        在我们的应用开发过程中,我们其实不需要太过于关注底层相关的功能,这些只需要相关的服务提供商提供相应的功能即可。不过在传输层之中,我们需要特别关注一下现在广泛使用的两个协议:TCP和UDP协议。这两个协议之间的主要区别如下:

TCP UDP
面向连接 无连接
提供数据可靠保证 不提供数据可靠性保证
速度相对较慢 速度较快
占用资源较多 占用资源较少

        关于TCP和UDP,可以看到,这两个协议各自分别有非常鲜明的特点:TCP虽然占用资源较多,速度相对较慢,但是提供了可靠的数据传输服务,这在大多数的互联网业务中是非常必要的;而UDP虽然不提供可靠性的数据保证,但是其速度非常快,而且占用资源较小,这在一些对数据可靠性较低的场景中是非常有用的,比如音视频服务,物联网数据上报服务等等,这些情况下,数据丢失一两帧都是可以接受的。

        TCP和UDP在资源占用上的区别,不仅体现在数据传输方式上,还体现在了数据的传输格式上。对于数据传输方式,TCP每次发送数据的方式都是按照时间窗口的方式一个数据报一个数据报的发送,并且需要等待每个数据报都给数据发送方响应ACK,这个时候才会发送下一个数据窗口的数据,如果当前窗口内有任意一个数据报没有发送成功,那么整个窗口内的数据都会重新发送;而UDP则没有窗口的概念和对应的ACK机制,其获取到每一个数据报之后,都只是简单的为其封装UDP协议头,然后将其发送出去,其不会管这个数据是否发送成功,因而UDP传输比TCP是要快很多的。对于数据传输格式,这里我们以TCP和UDP的数据报的格式进行讲解,如下是TCP的数据报格式:

        可以看到,TCP数据报的头部中不仅包含了源端口号和目的端口号,还包含了序号、确认序号、首部长度、标志位等等信息,总的来看,除去真正的数据部分,头部信息占用的字节数就达到了192字节,当然,这么多字段主要的作用是为了实现TCP面向连接的可靠性传输的功能。如下则是UDP数据报的格式:

        可以看到,这里UDP的数据包格式相对于TCP就非常的精简了,其头部主要就只有源端口号、目的端口号、长度和校验和字段,这些总共占用的字节数是8个字节。这也就是UDP协议传输速率非常快的另一个原因。

2.1 三次握手和四次挥手

        TCP是一个提供可靠传输服务、面向连接的的传输层协议,其可靠性保证主要是通过每次数据报发送时的ACK机制实现的,而其连接的建立和释放则主要是通过三次握手和四次挥手的方式实现的。如下是其三次握手和四次挥手的过程:

        对于三次握手,其整体过程如下:

  • 首先客户端会发送一个建立连接的请求,其标志位中会带上SYN=1, seq=x,这里的SYN=1根据前面TCP头部信息的讲解中我们知道,其表示建立连接的请求,而seq=x则只是当前请求的一个序号,不同的请求是有不同的序号的,加这个序号的原因也是为了将其与服务端的响应请求关联起来;
  • 在服务端接收到客户端建立连接的请求之后,其就会返回SYN=1, ACK=1, seq=y, ack_seq=x+1,这里的SYN=1, ACK=1表示的是对客户端建立连接的请求的同意响应,seq=y则标识了这是服务端的一次数据发送,而ack_seq=x+1则表示其是对客户端的seq=x的请求的一个响应;
  • 在客户端接收到服务端的响应的时候,客户端就能够确认服务端是能够正常接收和发送数据的,而服务端在接收到客户端的第一次请求的时候也能够确认客户端能够正常的发送请求。这个时候,客户端就会发送一个ACK=1, seq=x+1, ack_seq=y+1给服务器,服务器接收到后就会完成连接的建立。

        可以看到,前两次请求都是建立连接所必要的,而客户端要发送第三次请求的原因主要有两点:

  • 可以让服务器确保客户端是能够正常发送和接收请求的;
  • 由于连接的建立是在不稳定的网络上进行的,因而这里有可能第一次请求是由于客户端在某个时间点发送的,但是由于网络延迟,导致很久之后服务器才接收到该请求,但此时服务器并不知道这个连接建立的请求是否是正常请求,其还是会正常发送一个同意建立连接的响应给客户端,如果第一请求是由于网络延迟造成的,那么客户端是不会再发送第三次握手给服务器的,这个时候服务器等待超时后也就不会建立这一次的连接了。

        对于四次挥手,其是在客户端与服务器交互完成之后,由客户端发起的。四次挥手的主要流程如下:

  • 客户端首先会发送一个FIN=1, seq=u给服务器,根据前面TCP头部信息的讲解,我们知道FIN=1表示这是一个断开连接的请求,而seq=u则标识了这次请求的一个序号;
  • 服务器接收到客户端的断开连接的请求后,其就会向客户端发送一个ACK=1, seq=v, ack_seq=u+1的响应,这里的seq=v还是表示当前请求的序号,而ack_seq=u+1则表示这是对客户端发送的seq=u的断开连接的请求的响应,但是需要注意的是,这个请求并不表示服务器同意断开连接,此时还只是一个半关闭的状态,因为此时服务器可能还有数据在进行处理没有发送给客户端,此时服务器就会完成这些断开连接的工作;
  • 待服务器完成了断开连接的准备工作之后,其就会给客户端发送一个FIN=1, ACK=1, seq=w, ack_seq=u+1的响应,注意,这个过程中客户端一直都处于等待状态的。这里相对于前一次响应,多了一个FIN=1,就是表示当前是确认断开连接的请求;
  • 客户端在接收到服务器的响应之后,其就会给服务器发送一个ACK=1, seq=u+1, ack_seq=w+1的响应,表示同意断开连接,服务器接收到后就会断开连接,而客户端则会等待一小段时间后自行断开连接。

3. 小结

        本文首先讲解了OSI网络七层模型,详细讲解了模型中每一层的作用,然后讲解了传输层中TCP和UDP的主要区别,从传输方式和传输数据格式上对两种协议进行了对比,最后讲解了TCP协议中三次握手和四次挥手的主要过程,并且详细讲解了每一步的作用。

JDK13新特性详解

mumupudding阅读(6)

      JDK8新特性详解

      JDK9新特性详解

      JDK10新特性详解

      JDK11新特性详解

      JDK12新特性详解

      简介:JDK13于2019-09-17正式发布

1、switch优化更新

      JDK11以及之前的版本:

switch (day) {
    case MONDAY: 
    case FRIDAY:
    case SUNDAY:
         System.out.println(6); 
         break; 
    case TUESDAY: 
        System.out.println(7); 
        break; case THURSDAY: 
    case SATURDAY: 
        System.out.println(8);
         break; 
    case WEDNESDAY:
         System.out.println(9);
         break; 
}

       JDK12版本

switch (day) {
    case MONDAY, FRIDAY, SUNDAY -> System.out.println(6); 
    case TUESDAY -> System.out.println(7); 
    case THURSDAY, SATURDAY -> System.out.println(8); 
    case WEDNESDAY -> System.out.println(9);
 }

      JDK13版本

static void howMany(int k) {
    System.out.println(
        switch (k) {
            case  1 -> "one"
            case  2 -> "two"
            default -> "many"
        }
    );}

2、文本块升级

      2.1 html例子

      JDK13之前

String html = "<html>\n" +
              "    <body>\n" +
              "        <p>Hello, world</p>\n" +
              "    </body>\n" +
              "</html>\n";

      JDK13优化的:

String html = """
              <html>
                  <body>
                      <p>Hello, world</p>
                  </body>
              </html>
              """;

      2.2、SQL变化

      JDK13之前:

String query = "SELECT `EMP_ID`, `LAST_NAME` FROM `EMPLOYEE_TB`\n" +
               "WHERE `CITY` = 'INDIANAPOLIS'\n" +
               "ORDER BY `EMP_ID`, `LAST_NAME`;\n";

      JDK13:

String query = """
               SELECT `EMP_ID`, `LAST_NAME` FROM `EMPLOYEE_TB`
               WHERE `CITY` = 'INDIANAPOLIS'
               ORDER BY `EMP_ID`, `LAST_NAME`;

      2.3 解释

      文本块:

"""
line 1
line 2
line 3
"""

      相当于字符串文字:

"line 1\nline 2\nline 3\n"

3、动态CDS档案

    目标:

提高应用程序类 - 数据共享(AppCDS)的可用性。消除了用户进行试运行以创建每个应用程序的类列表的需要。-Xshare:dump使用类列表由该选项启用的静态归档应继续工作。这包括内置类加载器和用户定义的类加载器的类。

4、取消使用未使用的内存

      摘要:

  增强ZGC以将未使用的堆内存返回给操作系统。

      动机:

    ZGC目前没有取消提交并将内存返回给操作系统,即使该内存长时间未使用。对于所有类型的应用程序和环境,此行为并非最佳,尤其是那些需要关注内存占用
的应用程序和环境 例如:通过使用支付资源的容器环境。应用程序可能长时间处于空闲状态并与许多其他应用程序共享或竞争资源的环境。应用程序在执行期间可能
具有非常不同的堆空间要求。
    例如,启动期间所需的堆可能大于稳态执行期间稍后所需的堆。HotSpot中的其他垃圾收集器,如G1和Shenandoah,今天提供了这种功能,某些类别的用户发
现它非常有用。将此功能添加到ZGC将受到同一组用户的欢迎。

5、重新实现旧版套接字API    

      摘要:

使用更简单,更现代的实现替换java.net.Socket和java.net.ServerSocketAPI 使用的底层实现,易于维护和调试。新的实现很容易适应用户模式线程,也就是光纤,目前正在Project Loom中进行探索。

      动机:

    在java.net.Socket和java.net.ServerSocketAPI,以及它们的底层实现,可以追溯到JDK 1.0。实现是遗留Java和C代码的混合,
维护和调试很痛苦。该实现使用线程堆栈作为I/O缓冲区,这种方法需要多次增加默认线程堆栈大小。该实现使用本机数据结构来支持异步
关闭,这是多年来微妙可靠性和移植问题的根源。该实现还有几个并发问题,需要进行大修才能正确解决。在未来的光纤世界环境中,而不是
在本机方法中阻塞线程,当前的实现不适用于目的。

 

Spring Boot 配置文件中的花样,看这一篇足矣!

mumupudding阅读(15)

快速入门一节中,我们轻松的实现了一个简单的RESTful API应用,体验了一下Spring Boot给我们带来的诸多优点,我们用非常少的代码量就成功的实现了一个Web应用,这是传统的Spring应用无法办到的,虽然我们在实现Controller时用到的代码是一样的,但是在配置方面,相信大家也注意到了,在上面的例子中,除了Maven的配置之后,就没有引入任何的配置。

这就是之前我们所提到的,Spring Boot针对我们常用的开发场景提供了一系列自动化配置来减少原本复杂而又几乎很少改动的模板化配置内容。但是,我们还是需要去了解如何在Spring Boot中修改这些自动化的配置内容,以应对一些特殊的场景需求,比如:我们在同一台主机上需要启动多个基于Spring Boot的web应用,若我们不为每个应用指定特别的端口号,那么默认的8080端口必将导致冲突。

如果您还有在读我的Spring Cloud系列教程,其实有大量的工作都会是针对配置文件的。所以我们有必要深入的了解一些关于Spring Boot中的配置文件的知识,比如:它的配置方式、如何实现多环境配置,配置信息的加载顺序等。

配置基础

快速入门示例中,我们介绍Spring Boot的工程结构时,有提到过 src/main/resources目录是Spring Boot的配置目录,所以我们要为应用创建配置个性化配置时,就是在该目录之下。

Spring Boot的默认配置文件位置为: src/main/resources/application.properties。关于Spring Boot应用的配置内容都可以集中在该文件中了,根据我们引入的不同Starter模块,可以在这里定义诸如:容器端口名、数据库链接信息、日志级别等各种配置信息。比如,我们需要自定义web模块的服务端口号,可以在application.properties中添加server.port=8888来指定服务端口为8888,也可以通过spring.application.name=hello来指定应用名(该名字在Spring Cloud应用中会被注册为服务名)。

Spring Boot的配置文件除了可以使用传统的properties文件之外,还支持现在被广泛推荐使用的YAML文件。

YAML(英语发音:/ˈjæməl/,尾音类似camel骆驼)是一个可读性高,用来表达资料序列的格式。YAML参考了其他多种语言,包括:C语言、Python、Perl,并从XML、电子邮件的数据格式(RFC 2822)中获得灵感。Clark Evans在2001年首次发表了这种语言,另外Ingy döt Net与Oren Ben-Kiki也是这语言的共同设计者。目前已经有数种编程语言或脚本语言支援(或者说解析)这种语言。YAML是"YAML Ain’t a Markup Language"(YAML不是一种标记语言)的递回缩写。在开发的这种语言时,YAML 的意思其实是:"Yet Another Markup Language"(仍是一种标记语言),但为了强调这种语言以数据做为中心,而不是以标记语言为重点,而用反向缩略语重新命名。AML的语法和其他高阶语言类似,并且可以简单表达清单、散列表,标量等资料形态。它使用空白符号缩排和大量依赖外观的特色,特别适合用来表达或编辑数据结构、各种设定档、倾印除错内容、文件大纲(例如:许多电子邮件标题格式和YAML非常接近)。尽管它比较适合用来表达阶层式(hierarchical model)的数据结构,不过也有精致的语法可以表示关联性(relational model)的资料。由于YAML使用空白字元和分行来分隔资料,使得它特别适合用grep/Python/Perl/Ruby操作。其让人最容易上手的特色是巧妙避开各种封闭符号,如:引号、各种括号等,这些符号在巢状结构时会变得复杂而难以辨认。 —— 维基百科

YAML采用的配置格式不像properties的配置那样以单纯的键值对形式来表示,而是以类似大纲的缩进形式来表示。比如:下面的一段YAML配置信息

environments:    dev:        url: http://dev.bar.com        name: Developer Setup    prod:        url: http://foo.bar.com        name: My Cool App

与其等价的properties配置如下。

environments.dev.url=http://dev.bar.comenvironments.dev.name=Developer Setupenvironments.prod.url=http://foo.bar.comenvironments.prod.name=My Cool App

通过YAML的配置方式,我们可以看到配置信息利用阶梯化缩进的方式,其结构显得更为清晰易读,同时配置内容的字符量也得到显著的减少。除此之外,YAML还可以在一个单个文件中通过使用spring.profiles属性来定义多个不同的环境配置。例如下面的内容,在指定为test环境时,server.port将使用8882端口;而在prod环境,server.port将使用8883端口;如果没有指定环境,server.port将使用8881端口。

server:    port: 8881---spring:    profiles: testserver:    port: 8882---spring:    profiles: prodserver:    port: 8883

注意:YAML目前还有一些不足,它无法通过@PropertySource注解来加载配置。但是,YAML加载属性到内存中保存的时候是有序的,所以当配置文件中的信息需要具备顺序含义时,YAML的配置方式比起properties配置文件更有优势。

自定义参数

我们除了可以在Spring Boot的配置文件中设置各个Starter模块中预定义的配置属性,也可以在配置文件中定义一些我们需要的自定义属性。比如在application.properties中添加:

book.name=SpringCloudInActionbook.author=ZhaiYongchao

然后,在应用中我们可以通过@Value注解来加载这些自定义的参数,比如:

@Componentpublic class Book {    @Value("${book.name}")    private String name;    @Value("${book.author}")    private String author;    // 省略getter和setter}

@Value注解加载属性值的时候可以支持两种表达式来进行配置:

  • 一种是我们上面介绍的PlaceHolder方式,格式为 ${...} ,大括号内为PlaceHolder
  • 另外还可以使用SpEL表达式(Spring Expression Language), 格式为 #{...} ,大括号内为SpEL表达式

参数引用

application.properties中的各个参数之间,我们也可以直接通过使用PlaceHolder的方式来进行引用,就像下面的设置:

book.name=SpringCloudbook.author=ZhaiYongchaobook.desc=${book.author}  is writing《${book.name}》

book.desc参数引用了上文中定义的book.namebook.author属性,最后该属性的值就是ZhaiYongchao is writing《SpringCloud》

使用随机数

在一些特殊情况下,有些参数我们希望它每次加载的时候不是一个固定的值,比如:密钥、服务端口等。在Spring Boot的属性配置文件中,我们可以通过使用${random}配置来产生随机的int值、long值或者string字符串,这样我们就可以容易的通过配置来属性的随机生成,而不是在程序中通过编码来实现这些逻辑。

${random}的配置方式主要有一下几种,读者可作为参考使用。

# 随机字符串com.didispace.blog.value=${random.value}# 随机intcom.didispace.blog.number=${random.int}# 随机longcom.didispace.blog.bignumber=${random.long}# 10以内的随机数com.didispace.blog.test1=${random.int(10)}# 10-20的随机数com.didispace.blog.test2=${random.int[10,20]}

该配置方式可以用于设置应用端口等场景,避免在本地调试时出现端口冲突的麻烦

命令行参数

回顾一下在本章的快速入门中,我们还介绍了如何启动Spring Boot应用,其中提到了使用命令java -jar命令来启动的方式。该命令除了启动应用之外,还可以在命令行中来指定应用的参数,比如:java -jar xxx.jar --server.port=8888,直接以命令行的方式,来设置server.port属性,另启动应用的端口设为8888。

在命令行方式启动Spring Boot应用时,连续的两个减号--就是对application.properties中的属性值进行赋值的标识。所以,java -jar xxx.jar --server.port=8888命令,等价于我们在application.properties中添加属性server.port=8888

通过命令行来修改属性值是Spring Boot非常重要的一个特性,通过此特性,理论上已经使得我们应用的属性在启动前是可变的,所以其中端口号也好、数据库连接也好,都是可以在应用启动时发生改变,而不同于以往的Spring应用通过Maven的Profile在编译器进行不同环境的构建。其最大的区别就是,Spring Boot的这种方式,可以让应用程序的打包内容,贯穿开发、测试以及线上部署,而Maven不同Profile的方案每个环境所构建的包,其内容本质上是不同的。但是,如果每个参数都需要通过命令行来指定,这显然也不是一个好的方案,所以下面我们看看如果在Spring Boot中实现多环境的配置。

多环境配置

我们在开发任何应用的时候,通常同一套程序会被应用和安装到几个不同的环境,比如:开发、测试、生产等。其中每个环境的数据库地址、服务器端口等等配置都会不同,如果在为不同环境打包时都要频繁修改配置文件的话,那必将是个非常繁琐且容易发生错误的事。

对于多环境的配置,各种项目构建工具或是框架的基本思路是一致的,通过配置多份不同环境的配置文件,再通过打包命令指定需要打包的内容之后进行区分打包,Spring Boot也不例外,或者说更加简单。

在Spring Boot中多环境配置文件名需要满足application-{profile}.properties的格式,其中{profile}对应你的环境标识,比如:

  • application-dev.properties:开发环境
  • application-test.properties:测试环境
  • application-prod.properties:生产环境

至于哪个具体的配置文件会被加载,需要在application.properties文件中通过spring.profiles.active属性来设置,其值对应配置文件中的{profile}值。如:spring.profiles.active=test就会加载application-test.properties配置文件内容。

下面,以不同环境配置不同的服务端口为例,进行样例实验。

  • 针对各环境新建不同的配置文件application-dev.propertiesapplication-test.propertiesapplication-prod.properties

  • 在这三个文件均都设置不同的server.port属性,如:dev环境设置为1111,test环境设置为2222,prod环境设置为3333

  • application.properties中设置spring.profiles.active=dev,就是说默认以dev环境设置

  • 测试不同配置的加载

  • 执行java -jar xxx.jar,可以观察到服务端口被设置为1111,也就是默认的开发环境(dev)

  • 执行java -jar xxx.jar --spring.profiles.active=test,可以观察到服务端口被设置为2222,也就是测试环境的配置(test)

  • 执行java -jar xxx.jar --spring.profiles.active=prod,可以观察到服务端口被设置为3333,也就是生产环境的配置(prod)

按照上面的实验,可以如下总结多环境的配置思路:

  • application.properties中配置通用内容,并设置spring.profiles.active=dev,以开发环境为默认配置
  • application-{profile}.properties中配置各个环境不同的内容
  • 通过命令行方式去激活不同环境的配置

加载顺序

在上面的例子中,我们将Spring Boot应用需要的配置内容都放在了项目工程中,虽然我们已经能够通过spring.profiles.active或是通过Maven来实现多环境的支持。但是,当我们的团队逐渐壮大,分工越来越细致之后,往往我们不需要让开发人员知道测试或是生成环境的细节,而是希望由每个环境各自的负责人(QA或是运维)来集中维护这些信息。那么如果还是以这样的方式存储配置内容,对于不同环境配置的修改就不得不去获取工程内容来修改这些配置内容,当应用非常多的时候就变得非常不方便。同时,配置内容都对开发人员可见,本身这也是一种安全隐患。对此,现在出现了很多将配置内容外部化的框架和工具,后续将要介绍的Spring Cloud Config就是其中之一,为了后续能更好的理解Spring Cloud Config的加载机制,我们需要对Spring Boot对数据文件的加载机制有一定的了解。

Spring Boot为了能够更合理的重写各属性的值,使用了下面这种较为特别的属性加载顺序:

  1. 命令行中传入的参数。
  2. SPRING_APPLICATION_JSON中的属性。SPRING_APPLICATION_JSON是以JSON格式配置在系统环境变量中的内容。
  3. java:comp/env中的JNDI属性。
  4. Java的系统属性,可以通过System.getProperties()获得的内容。
  5. 操作系统的环境变量
  6. 通过random.*配置的随机属性
  7. 位于当前应用jar包之外,针对不同{profile}环境的配置文件内容,例如:application-{profile}.properties或是YAML定义的配置文件
  8. 位于当前应用jar包之内,针对不同{profile}环境的配置文件内容,例如:application-{profile}.properties或是YAML定义的配置文件
  9. 位于当前应用jar包之外的application.propertiesYAML配置内容
  10. 位于当前应用jar包之内的application.propertiesYAML配置内容
  11. @Configuration注解修改的类中,通过@PropertySource注解定义的属性
  12. 应用默认属性,使用SpringApplication.setDefaultProperties定义的内容

优先级按上面的顺序有高到低,数字越小优先级越高。

可以看到,其中第7项和第9项都是从应用jar包之外读取配置文件,所以,实现外部化配置的原理就是从此切入,为其指定外部配置文件的加载位置来取代jar包之内的配置内容。通过这样的实现,我们的工程在配置中就变的非常干净,我们只需要在本地放置开发需要的配置即可,而其他环境的配置就可以不用关心,由其对应环境的负责人去维护即可。

2.x 新特性

在Spring Boot 2.0中推出了Relaxed Binding 2.0,对原有的属性绑定功能做了非常多的改进以帮助我们更容易的在Spring应用中加载和读取配置信息。下面本文就来说说Spring Boot 2.0中对配置的改进。

配置文件绑定

简单类型

在Spring Boot 2.0中对配置属性加载的时候会除了像1.x版本时候那样移除特殊字符外,还会将配置均以全小写的方式进行匹配和加载。所以,下面的4种配置方式都是等价的:

  • properties格式:
spring.jpa.databaseplatform=mysqlspring.jpa.database-platform=mysqlspring.jpa.databasePlatform=mysqlspring.JPA.database_platform=mysql
  • yaml格式:
spring:  jpa:    databaseplatform: mysql    database-platform: mysql    databasePlatform: mysql    database_platform: mysql

Tips:推荐使用全小写配合-分隔符的方式来配置,比如:spring.jpa.database-platform=mysql

List类型

在properties文件中使用[]来定位列表类型,比如:

spring.my-example.url[0]=http://example.comspring.my-example.url[1]=http://spring.io

也支持使用逗号分割的配置方式,上面与下面的配置是等价的:

spring.my-example.url=http://example.com,http://spring.io

而在yaml文件中使用可以使用如下配置:

spring:  my-example:    url:      - http://example.com      - http://spring.io

也支持逗号分割的方式:

spring:  my-example:    url: http://example.com, http://spring.io

注意:在Spring Boot 2.0中对于List类型的配置必须是连续的,不然会抛出UnboundConfigurationPropertiesException异常,所以如下配置是不允许的:

foo[0]=afoo[2]=b

在Spring Boot 1.x中上述配置是可以的,foo[1]由于没有配置,它的值会是null

Map类型

Map类型在properties和yaml中的标准配置方式如下:

  • properties格式:
spring.my-example.foo=barspring.my-example.hello=world
  • yaml格式:
spring:  my-example:    foo: bar    hello: world

注意:如果Map类型的key包含非字母数字和-的字符,需要用[]括起来,比如:

spring:  my-example:    '[foo.baz]': bar

环境属性绑定

简单类型

在环境变量中通过小写转换与.替换_来映射配置文件中的内容,比如:环境变量SPRING_JPA_DATABASEPLATFORM=mysql的配置会产生与在配置文件中设置spring.jpa.databaseplatform=mysql一样的效果。

List类型

由于环境变量中无法使用[]符号,所以使用_来替代。任何由下划线包围的数字都会被认为是[]的数组形式。比如:

MY_FOO_1_ = my.foo[1]MY_FOO_1_BAR = my.foo[1].barMY_FOO_1_2_ = my.foo[1][2]

另外,最后环境变量最后是以数字和下划线结尾的话,最后的下划线可以省略,比如上面例子中的第一条和第三条等价于下面的配置:

MY_FOO_1 = my.foo[1]MY_FOO_1_2 = my.foo[1][2]

系统属性绑定

简单类型

系统属性与文件配置中的类似,都以移除特殊字符并转化小写后实现绑定,比如下面的命令行参数都会实现配置spring.jpa.databaseplatform=mysql的效果:

-Dspring.jpa.database-platform=mysql-Dspring.jpa.databasePlatform=mysql-Dspring.JPA.database_platform=mysql

List类型

系统属性的绑定也与文件属性的绑定类似,通过[]来标示,比如:

-D"spring.my-example.url[0]=http://example.com"-D"spring.my-example.url[1]=http://spring.io"

同样的,他也支持逗号分割的方式,比如:

-Dspring.my-example.url=http://example.com,http://spring.io

属性的读取

上文介绍了Spring Boot 2.0中对属性绑定的内容,可以看到对于一个属性我们可以有多种不同的表达,但是如果我们要在Spring应用程序的environment中读取属性的时候,每个属性的唯一名称符合如下规则:

  • 通过.分离各个元素
  • 最后一个.将前缀与属性名称分开
  • 必须是字母(a-z)和数字(0-9)
  • 必须是小写字母
  • 用连字符-来分隔单词
  • 唯一允许的其他字符是[],用于List的索引
  • 不能以数字开头

所以,如果我们要读取配置文件中spring.jpa.database-platform的配置,可以这样写:

this.environment.containsProperty("spring.jpa.database-platform")

而下面的方式是无法获取到spring.jpa.database-platform配置内容的:

this.environment.containsProperty("spring.jpa.databasePlatform")

注意:使用@Value获取配置内容的时候也需要这样的特点

全新的绑定API

在Spring Boot 2.0中增加了新的绑定API来帮助我们更容易的获取配置信息。下面举个例子来帮助大家更容易的理解:

例子一:简单类型

假设在propertes配置中有这样一个配置:com.didispace.foo=bar

我们为它创建对应的配置类:

@Data@ConfigurationProperties(prefix = "com.didispace")public class FooProperties {    private String foo;}

接下来,通过最新的Binder就可以这样来拿配置信息了:

@SpringBootApplicationpublic class Application {    public static void main(String[] args) {        ApplicationContext context = SpringApplication.run(Application.class, args);        Binder binder = Binder.get(context.getEnvironment());        // 绑定简单配置        FooProperties foo = binder.bind("com.didispace", Bindable.of(FooProperties.class)).get();        System.out.println(foo.getFoo());    }}

例子二:List类型

如果配置内容是List类型呢?比如:

com.didispace.post[0]=Why Spring Bootcom.didispace.post[1]=Why Spring Cloudcom.didispace.posts[0].title=Why Spring Bootcom.didispace.posts[0].content=It is perfect!com.didispace.posts[1].title=Why Spring Cloudcom.didispace.posts[1].content=It is perfect too!

要获取这些配置依然很简单,可以这样实现:

ApplicationContext context = SpringApplication.run(Application.class, args);Binder binder = Binder.get(context.getEnvironment());// 绑定List配置List<String> post = binder.bind("com.didispace.post", Bindable.listOf(String.class)).get();System.out.println(post);List<PostInfo> posts = binder.bind("com.didispace.posts", Bindable.listOf(PostInfo.class)).get();System.out.println(posts);

代码示例

本教程配套仓库:

如果您觉得本文不错,欢迎Star、Follow支持!您的关注是我坚持的动力!

基于Jenkins Pipeline自动化部署

mumupudding阅读(9)


微信公众号「后端进阶」,专注后端技术分享:Java、Golang、WEB框架、分布式中间件、服务治理等等。老司机倾囊相授,带你一路进阶,来不及解释了快上车!

最近在公司推行Docker Swarm集群的过程中,需要用到Jenkins来做自动化部署,Jenkins实现自动化部署有很多种方案,可以直接在jenkins页面写Job,把一些操作和脚本都通过页面设置,也可以在每个项目中直接写Pipeline脚本,但像我那么优秀,那么追求极致的程序员来说,这些方案都打动不了我那颗骚动的心,下面我会跟你们讲讲我是如何通过Pipeline脚本实现自动化部署方案的,并且实现多分支构建,还实现了所有项目共享一个Pipeline脚本。

使用Jenkins前的一些设置

为了快速搭建Jenkins,我这里使用Docker安装运行Jenkins:

$ sudo docker run -it -d \  --rm \  -u root \  -p 8080:8080 \  -v jenkins-data:/var/jenkins_home \  -v /var/run/docker.sock:/var/run/docker.sock \  -v "$HOME":/home \  --name jenkins jenkinsci/blueocean

初次使用jenkins,进入Jenkins页面前,需要密码验证,我们需要进入docker容器查看密码:

$ sudo docker exec -it jenkins /bin/bash$ vi /var/jenkins_home/secrets/initialAdminPassword

Docker安装的Jenkins稍微有那么一点缺陷,shell版本跟CenOS宿主机的版本不兼容,这时我们需要进入Jenkins容器手动设置shell:

$ sudo docker exec -it jenkins /bin/bash$ ln -sf /bin/bash /bin/sh

由于我们的Pipeline还需要在远程服务器执行任务,需要通过ssh连接,那么我们就需要在Jenkins里面生成ssh的公钥密钥:

$ sudo docker exec -it jenkins /bin/bash$ ssh-keygen -C "root@jenkins"

在远程节点的~/.ssh/authorized_keys中添加jenkins的公钥(id_rsa.pub)

还需要安装一些必要的插件:

  1. Pipeline Maven Integration
  2. SSH Pipeline Steps

安装完插件后,还需要去全局工具那里添加maven:

maven

这里后面Jenkinsfile有用到。

mutiBranch多分支构建

由于我们的开发是基于多分支开发,每个开发环境都对应有一条分支,所以普通的Pipeline自动化构建并不能满足现有的开发部署需求,所以我们需要使用Jenkins的mutiBranch Pipeline。

首先当然是新建一个mutiBranch多分支构建job:

maven

接着设置分支源,分支源就是你项目的git地址,选择Jenkinsfile在项目的路径

maven

接下来Jenkins会在分支源中扫描每个分支下的Jenkinsfile,如果该分支下有Jenkinsfile,那么就会创建一个分支Job

maven

该job下的分支job如下:

maven

这里需要注意的是,只有需要部署的分支,才加上Jenkinsfile,不然Jenkins会将其余分支也创建一个分支job。

通用化Pipeline脚本

到这里之前,基本就可以基于Pipeline脚本自动化部署了,但如果你是一个追求极致,不甘于平庸的程序员,你一定会想,随着项目的增多,Pipeline脚本不断增多,这会造成越来越大的维护成本,随着业务的增长,难免会在脚本中修改东西,这就会牵扯太多Pipeline脚本了,而且这些脚本基本都相同,那么对于我这么优秀的程序员,怎么会想不到这个问题呢,我第一时间就想到通用化pipeline脚本。所幸,Jenkins已经看出了我不断骚动的心了,Jenkins甩手就给我一个Shared Libraries。

Shared Libraries是什么呢?顾名思义,它就是一个共享库,它的主要作用是用于将通用的Pipeline脚本放在一个地方,其它项目可以从它那里获取到一个全局通用化的Pipeline脚本,项目之间通过不通的变量参数传递,达到通用化的目的。

接下来我们先创建一个用于存储通用Pipeline脚本的git仓库:

maven

仓库目录不是随便乱添加了,Jenkins有一个严格的规范,下面是官方说明:

maven

官方已经讲得很清楚了,大概意思就是vars目录用于存储通用Pipeline脚本,resources用于存储非Groovy文件。所以我这里就把Pipeline需要的构建脚本以及编排文件都集中放在这里,完全对业务工程师隐蔽,这样做的目的就是为了避免业务工程师不懂瞎几把乱改,导致出bug。

创建完git仓库后,我们还需要在jenkins的Manage Jenkins » Configure System » Global Pipeline Libraries中定义全局库:

maven

这里的name,可以在jenkinsfile中通过以下命令引用:

@Library 'objcoding-pipeline-library'

下面我们来看通用Pipeline脚本的编写规则:

#!groovydef getServer() {    def remote = [:]    remote.name = 'manager node'    remote.user = 'dev'    remote.host = "${REMOTE_HOST}"    remote.port = 22    remote.identityFile = '/root/.ssh/id_rsa'    remote.allowAnyHosts = true    return remote}def call(Map map) {    pipeline {        agent any        environment {            REMOTE_HOST = "${map.REMOTE_HOST}"            REPO_URL = "${map.REPO_URL}"            BRANCH_NAME = "${map.BRANCH_NAME}"            STACK_NAME = "${map.STACK_NAME}"            COMPOSE_FILE_NAME = "docker-compose-" + "${map.STACK_NAME}" + "-" + "${map.BRANCH_NAME}" + ".yml"        }        stages {            stage('获取代码') {                steps {                    git([url: "${REPO_URL}", branch: "${BRANCH_NAME}"])                }            }            stage('编译代码') {                steps {                    withMaven(maven: 'maven 3.6') {                        sh "mvn -U -am clean package -DskipTests"                    }                }            }            stage('构建镜像') {                steps {                    sh "wget -O build.sh https://git.x-vipay.com/docker/jenkins-pipeline-library/raw/master/resources/shell/build.sh"                    sh "sh build.sh ${BRANCH_NAME} "                }            }            stage('init-server') {                steps {                    script {                        server = getServer()                    }                }            }            stage('执行发版') {                steps {                    writeFile file: 'deploy.sh', text: "wget -O ${COMPOSE_FILE_NAME} " +                        " https://git.x-vipay.com/docker/jenkins-pipeline-library/raw/master/resources/docker-compose/${COMPOSE_FILE_NAME} \n" +                        "sudo docker stack deploy -c ${COMPOSE_FILE_NAME} ${STACK_NAME}"                    sshScript remote: server, script: "deploy.sh"                }            }        }    }}
  1. 由于我们需要在远程服务器执行任务,所以定义一个远程服务器的信息其中remote.identityFile就是我们上面在容器生成的密钥的地址;
  2. 定义一个call()方法,这个方法用于在各个项目的Jenkinsfile中调用,注意一定得叫call;
  3. 在call()方法中定义一个pipeline;
  4. environment参数即是可变通用参数,通过传递参数Map来给定值,该Map是从各个项目中定义的传参;
  5. 接下来就是一顿步骤操作啦,“编译代码”这步骤需要填写上面我们在全局工具类设置的maven,“构建镜像”的构建脚本巧妙地利用wget从本远程仓库中拉取下来,”执行发版“的编排文件也是这么做,“init-server”步骤主要是初始化一个server对象,供“执行发版使用”。

从脚本看出来Jenkins将来要推崇的一种思维:配置即代码。

写完通用Pipeline脚本后,接下来我们就需要在各个项目的需要自动化部署的分支的根目录下新建一个Jenkinsfile脚本了:

maven

接下来我来解释一下Jenkinsfile内容:

#!groovy// 在多分支构建下,严格规定Jenkinsfile只存在可以发版的分支上// 引用在jenkins已经全局定义好的librarylibrary 'objcoding-pipeline-library'def map = [:]// 远程管理节点地址(用于执行发版)map.put('REMOTE_HOST','xxx.xx.xx.xxx')// 项目gitlab代码地址map.put('REPO_URL','https://github.com/objcoding/docker-jenkins-pipeline-sample.git')// 分支名称map.put('BRANCH_NAME','master')// 服务栈名称map.put('STACK_NAME','vipay')// 调用library中var目录下的build.groovy脚本build(map)
  1. 通过library 'objcoding-pipeline-library'引用我们在Jenkins定义的全局库,定义一个map参数;
  2. 接下来就是将项目具体的参数保存到map中,调用build()方法传递给通用Pipeline脚本。

Shared Libraries共享库极大地提升了Pipeline脚本的通用性,避免了脚本过多带来的问题,也符合了一个优秀程序员的审美观,如果你是一个有追求的程序员,你一定会爱上它。

流程图:

maven

demo git 地址:

pipeline:https://github.com/objcoding/jenkins-pipeline-library

单项目部署:https://github.com/objcoding/docker-jenkins-pipeline-sample

多项目部署:https://github.com/objcoding/docker-jenkins-pipeline-sample2

公众号「后端进阶」,专注后端技术分享!

RPC的负载均衡策略

mumupudding阅读(12)

抽空自己写了个简易版的rpc框架,想了下怎么搞负载均衡,最简单的方式就是搞个配置文件放置服务地址,直接读配置文件,转而想到配置文件可以放zk,相当于用zk来做配置中心或者服务发现。优秀的dubbo项目就可以这么做,马上参考了下谷歌的grpc,发现了一篇谷歌很棒的文章,拜读了下(也借用了谷歌这篇文章的图片),很不错,想写一些我自己的见解。

传送门:https://grpc.io/blog/loadbalancing/

rpc通信本身并不复杂,只要定好协议怎么处理问题不大,但是负载均衡的策略是值得推敲的。

一般情况下,负载均衡的策略有以下两种

1. 代理服务

客户端并不知道服务端的存在,它所有的请求都打到代理服务,由代理服务去分发到服务端,并且实现公平的负载算法。客户机可能不可信,这种情况通过用户面向用户的服务,类似于我们的nginx将请求分发到后端机器。

缺点:客户端不知道后端的存在,且客户端不可信,延迟会更高且代理服务会影响服务本身的吞吐量

优点:在中间层做监控等拦截操作特别棒。

如图:

2. 客户端负载均衡

客户端知道有多个后端服务,由客户端去选择服务端,并且客户端可以从后端服务器中自己总结出一份负载的信息,实现负载均衡算法。这种方式最简单的实现就是我上面说的直接搞个配置文件,调用的时候随机或者轮询服务端即可。

如图:

优点:

高性能,因为消除了第三方的交互

缺点:

客户端会很复杂,因为客户端要跟踪服务器负载和健康状况,客户端实现负载均衡算法。多语言的实现和维护负担也很麻烦,且客户端需要被信任,得是靠谱的客户端。

以上是介绍了两种负载均衡的方案,下面要说的就是使用代理方式负载均衡的几种详细的方案

代理方式的负载均衡有很多种

代理负载平衡可以是 L3/L4(传输级别)L7(应用程序级别)

L3/L4 中,服务器终止TCP连接并打开另一个连接到所选的后端。

L7 只是在客户端连接到服务端端连接之间搞一个应用来做中间人。

L3/L4 级别的负载均衡按设计只做很少的处理,与L7级别的负载均衡相比的延迟会更少,而且更便宜,因为它消耗更少的资源。

在L7(应用程序级)负载平衡中,负载均衡服务终止并解析协议。负载均衡服务可以检查每个请求并根据请求内容分配后端。这就意味监控拦截等操作可以非常方便快捷的做在这里。

L3/L4 vs L7

正确的打开方式有一下几种

  1. 这些连接之间的RPC负载变化很大: 建议使用L7.
  2. 存储或计算相关性很重要 :建议使用L7,并使用cookie或类似的路由请求来纠正服务端.
  3. 设备资源少(缺钱): 建议使用 L3/L4.
  4. 对延迟要求很严格(就是要快): L3/L4.

下面要说的就是客户端实现负载均衡方式的详细方案:

1. 笨重的客户端

这就意味着客户端中实现负载平衡策略,客户端负责跟踪可用的服务器以及用于选择服务器的算法。客户端通常集成与其他基础设施(如服务发现、名称解析、配额管理等)通信的库,这就很复杂庞大了。

2. Lookaside 负载均衡 (旁观?)

旁观式负载平衡也称为外部负载平衡,使用后备负载平衡,负载平衡的各种功能智能的在一个单独的特殊的负载均衡服务中实现。客户端只需要查询这个旁观式的负载均衡服务, 这个服务就能给你最佳服务器的信息,然后你拿这个数据去请求那个服务端。就像我一开说的比如把服务端的信息注册到zk,由zk去做负载均衡的事情,客户端只需要去zk取服务端数据,拿到了服务端地址后,直接向服务端请求。

如图:

以上说了这么多,到底服务间的负载均衡应该用哪个,总结以下几点:

  1. 客户端和服务器之间非常高的流量,且客户端是可信的,建议使用‘笨重’的客户端 或者 Lookaside 负载均衡

  2. 传统设置——许多客户端连接到代理背后的大量服务,需要服务端和客户端之间有一个桥梁,建议使用代理式的负载均衡

  3. 微服务- N个客户端,数据中心有M个服务端,非常高的性能要求(低延迟,高流量),客户端可以不受信任,建议使用 Lookaside 负载均衡

分布式项目:根据工作时间设置对请求拦截并跳转

mumupudding阅读(10)


1 问题描述

如题目所示,即实现根据工作时间的设置,创建一个拦截器将用户操作限定在工作时间范围内.
逻辑本身不复杂,但由于项目使用了很多之前没接触过的技术栈,所以写起来有点缺乏信心.好在最后写完了,故将之总结下来.
先列举项目中涉及到的技术栈:

  • 前台: 组件vue,vuex,iview,axios
  • 后台(分布式): SpringBoot,redis

之后是根据技术栈,列举大致开发步骤:

  1. 创建拦截器,拦截请求
  2. 将工作时间的数据放到缓存中
  3. 逻辑判断:当前时间是否在(缓存中取到的)工作时间内
  4. (如果不在允许工作时间范围内)跳转至登录页面并提示

2 创建拦截器

由于此前没有创建过拦截器,在做这一步的时候还是很担心能否成功的.
好在这一步比想象中要更轻松,也是多些各位大佬的分享.大致而言,主要分两个步骤:

  1. 创建拦截器
@Componentpublic class WorkHourInterceptor implements HandlerInterceptor {    private static Logger logger= LoggerFactory.getLogger(WorkHourInterceptor.class);    @Override    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {        // 如果不是映射到方法直接通过        if(!(handler instanceof HandlerMethod)){            return true;        }        logger.info("============================拦截器启动==============================");        request.setAttribute("starttime",System.currentTimeMillis());                //TODO 这里是逻辑代码,放在逻辑判断中说        return true;    }    @Override    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {        logger.info("===========================执行处理完毕=============================");        long starttime = (long) request.getAttribute("starttime");        request.removeAttribute("starttime");        long endtime = System.currentTimeMillis();        logger.info("============请求地址:/cf"+request.getRequestURI()+":处理时间:{}",(endtime-starttime)+"ms");    }    @Override    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {        logger.info("============================拦截器关闭==============================");    }}
  1. 创建拦截器配置
@Configurationpublic class WebConfigurer implements WebMvcConfigurer {    @Override    public void addInterceptors(InterceptorRegistry registry) {        //工作时间拦截器        registry.addInterceptor(workHourInterceptor()).addPathPatterns("/**");    }    @Bean    public WorkHourInterceptor workHourInterceptor(){        return new WorkHourInterceptor();    }}

对于SpringBoot而言,创建拦截器就是这么简单.
以下参考链接是我在查询相关链接时比较精华的,当然代码和以上是没有太大区别的.

参考链接:

  1. Spring Boot 优雅的配置拦截器方式
  2. SpringBoot 2.X配置登录拦截器
  3. Spring Boot 拦截器
  4. Spring Boot配置拦截器

3 将工作时间数据放到缓存中

对于redis在java中的使用,项目中使用的是二级缓存j2cache.
其具体的原理本文不再详述,这里只谈使用.(由于该代码与逻辑代码是放在一个类中的,故不再分开.其中可能涉及到一些在其他项目中没有必要追加的计算,会简要说明)

public class WorkHourUtil {    private static final String WORK_HOUR="WorkHour";    private static final String SERVICE_NAME="CfApi";    private static final String COMMON="Common";    public static boolean isWorkHourEmpty(String strCompanyId){        //工作时间缓存数据是否为空        return getCacheData(strCompanyId)==null;    }    /**     * Is in work hour boolean.     *     * @param strCompanyId the str company id     * @return the boolean     * @author YangYishe     * @date 2019年06月27日 16:08     */    static boolean isInWorkHour(String strCompanyId){        //是否在工作时间内(含获取数据过程)        List<WorkingHours> lstWh=getCacheData(strCompanyId);        return isInWorkHour(lstWh);    }    private static boolean isInWorkHour(List<WorkingHours> lstWorkHour){        //是否在工作时间内(仅需逻辑有关)        //此处的代码在下面的逻辑判断中有用到,不再额外贴出        if(lstWorkHour==null||lstWorkHour.size()==0){            return true;        }        Date mToday=new Date();        //此处获取到的星期是把周日当第1天算的        //hutool的DateUtil        int intWeekDay= DateUtil.dayOfWeek(mToday)-1;        if(intWeekDay<=0){            intWeekDay+=7;        }        DateTime mNow=DateUtil.parseTime(DateUtil.formatTime(mToday));        for(WorkingHours mWh:lstWorkHour){            if(mWh.getSort().equals(intWeekDay)){                boolean blnIsEnable=mWh.getDayState();                DateTime mStartTime=DateUtil.parseTime(mWh.getStartTime());                DateTime mEndTime=DateUtil.parseTime(mWh.getEndTime());                boolean blnAfterStart=mNow.isAfterOrEquals(mStartTime);                boolean blnBeforeEnd=mNow.isBeforeOrEquals(mEndTime);                return (!blnIsEnable)||(blnAfterStart&&blnBeforeEnd);            }        }        //此处一般不会触发,如果触发了,就需要检查代码看哪儿有问题了.        return false;    }    private static String getRegion(String strCompanyId){        //由于项目是分企业的,每个企业都有自己独自的工作时间缓存数据        return WORK_HOUR+"∥"+strCompanyId;    }    public static void setCacheData(String strCompanyId,List<WorkingHours> lstWh){        CacheChannel channel = J2Cache.getChannel();        String cacheKey = StrUtil.format("{}{}",SERVICE_NAME, COMMON);        String strRegion=getRegion(strCompanyId);        //不确定缓存能否保存WorkHours的结合,以防万一,这里直接转换成了字符串        //这里的JSON是FastJson,我目前用过最方便的JSON解析包        String strWhList=JSON.toJSONString(lstWh);        channel.set(strRegion,cacheKey,strWhList);    }    private static List<WorkingHours> getCacheData(String strCompanyId){        List<WorkingHours> lstWh=null;        CacheChannel channel = J2Cache.getChannel();        //这里的次级索引合并在其他项目中作用也不是很大,可只取一个字符串        String cacheKey = StrUtil.format("{}{}",SERVICE_NAME, COMMON);        Object object=channel.get(getRegion(strCompanyId),cacheKey).getValue();        if(ObjectUtil.isNotNull(object)){            String strWhList= (String) object;            lstWh=JSON.parseArray(strWhList,WorkingHours.class);        }        return lstWh;    }}

一二级缓存的名称应该是不需要讲究太多.

参考链接:

  1. J2Cache官方API
  2. java项目集成J2Cache

4 逻辑判断

这里首先要参考的是,获取当前时间是否在工作时间内,直接调用WorkHourUtil的isInWorkHour方法即可.
该方法要在拦截器中拦截.代码大致如下(以下方法写在WorkHourInterceptor类preHandle方法的TODO处):

List<String> lstUrlLogin= Arrays.asList("/login","/logout");//这里用lambda表达式判断当前地址是否为登录或登出地址boolean blnIsLogin=lstUrlLogin.stream().anyMatch(m->m.equals(request.getRequestURI()));if(!blnIsLogin){    //获取企业id(这里的getCompanyId是获取公司id的方法,其他项目可以不关注,或者有这里的获取逻辑)    String strCompanyId=getCompanyId(request);    //此处判断是否在工作时间    boolean blnIsInWorkHour=WorkHourUtil.isInWorkHour(strCompanyId);    if(!blnIsInWorkHour){        Map<String,Object> mapResult=new HashMap<>();        mapResult.put("logout",true);        mapResult.put("message","当前时间不允许使用系统!");        //注:returnJson是判断后的页面跳转,放在第5大点说        returnJson(response,JSON.toJSONString(mapResult));        return false;    }}

5 判断后的页面跳转

这里先贴上后台的returnJson方法(同样在WorkHourInterceptor中),用以表示请求被拦截:

private void returnJson(HttpServletResponse response, String json) throws Exception {    response.setCharacterEncoding("UTF-8");    response.setContentType("text/html; charset=utf-8");    try (PrintWriter writer = response.getWriter()) {        writer.print(json);    } catch (IOException e) {        logger.error("response error", e);    }}

不同于以前开发的页面和后台放在一起的项目,当前项目只有后台,发送回的请求也全部都是JSON数据,换言之,并不关联页面.
所以,想要判断后进行页面跳转,必须在前台项目中的拦截器中拦截该请求再进行跳转.(这里前台页面拦截器的代码很长,但相当多的部分与本文所述的要求并无直接关系,同时不方便删除,故保留,阅读时请注意甄别)
登录拦截js(需在axios.js中调用)

import store from '../../store/index.js'import router from '../../router/index.js'import { isEmpty } from '../../view/components/about-text/about-string'import { Message } from 'iview'/** * 登出拦截器(结果是登出的拦截器) * @param res */export const logoutInterceptor = (res) => {  // 判断,当返回结果中含有返回值提示应当跳转到logout页面时,则跳转logout页面  if (typeof res.data !== 'undefined') {    if (res.data.logout) {    //这里的handleLogOut是发送登出请求的mapAction中的方法,用以去除一些session和token数据      store.dispatch('handleLogOut').then(() => {        router.push({          name: 'login'        })        //登录页面的同时提示是由于何原因登出        if (!isEmpty(res.data.message)) {          Message.warning(res.data.message)        }      })    }  }}

前台拦截器(axios),注意loginInterceptor仅在相应拦截中调用了,其他地方均与本文要说的内容无关,只是为了避免个别人摸不清头脑所以把全部代码都写上了:

import axios from 'axios'import { getToken } from '@/libs/util'import { isEmpty } from '../view/components/about-text/about-string'import { logoutInterceptor } from '../api/system/login'let arrRequestUrl = {}const CancelToken = axios.CancelTokenclass HttpRequest {  constructor (baseUrl = baseURL) {    this.baseUrl = baseUrl    this.queue = {}  }  getInsideConfig () {    const config = {      baseURL: this.baseUrl,      headers: {        Authorization: getToken()      }    }    return config  }  destroy (url) {    delete this.queue[url]    if (!Object.keys(this.queue).length) {      // Spin.hide()    }  }  interceptors (instance, url) {    // 请求拦截    instance.interceptors.request.use(config => {      // 发起请求时,如果正在发送的请求中,已有对应的url,则驳回,否则记录      // 此处的get方法,不加拦截也ok,加之领导要求,于是改为get方法不加拦截.      if (arrRequestUrl[url] && config.method !== 'get') {        return Promise.reject(new Error('repeatSubmit'))      } else {        arrRequestUrl[url] = true      }      // 添加全局的loading...      if (!Object.keys(this.queue).length) {        // Spin.show() // 不建议开启,因为界面不友好      }      this.queue[url] = true      return config    }, error => {      return Promise.reject(error)    })    // 响应拦截    instance.interceptors.response.use(res => {      // 去掉正在发送请求的记录      delete arrRequestUrl[url]      // 结果是登出的拦截器!!!      logoutInterceptor(res)      this.destroy(url)      const { data, status } = res      return { data, status }    }, error => {      // 对重复提交的错误信息进行解析      if (error.message === 'repeatSubmit') {        throw new Error('请不要重复提交')      } else {        delete arrRequestUrl[url]      }      this.destroy(url)      let errorInfo = error.response      if (!errorInfo) {        const { request: { statusText, status }, config } = JSON.parse(JSON.stringify(error))        errorInfo = {          statusText,          status,          request: { responseURL: config.url }        }      }      // addErrorLog(errorInfo)      return Promise.reject(error)    })  }  request (options) {    const instance = axios.create()    options = Object.assign(this.getInsideConfig(), options)    this.interceptors(instance, options.url)    return instance(options)  }}export default HttpRequest

说明:

  1. 由于后台是分布式项目,所以对于每一个api项目都可能需要添加拦截器(WorkHourInterceptor)和拦截器(WebConfigurer)配置文件.
  2. 本文没有对设置缓存数据进行说明,在本项目中有两个对应场景,一是登录时判断有无当前公司的工作时间设置数据,如无则查询并将之放在缓存中,二是修改工作时间设置时,会覆盖本公司当前的工作时间设置缓存数据.

Monkey命令参数详解

mumupudding阅读(7)


什么是monkey

Monkey是Android中的一个命令行工具,可以运行在模拟器里或实际设备中。它向系统发送伪随机的用户事件流(如按键输入、触摸屏输入、手势输入等),实现对正在开发的应用程序进行压力测试.

基本语法

$ adb shell monkey [options]

如果不指定options,Monkey将以无反馈模式启动,并把事件任意发送到安装在目标环境中的全部应用程序

$ adb shell monkey -p package -v 500

指定对package这个应用程序进行monkey测试,并向其发送500个伪随机事件。其中 -p 表示对象包包,–v 表示反馈信息级别

命令参数

可以使用命令 adb shell monkey -help 查看命令参数

1、参数: -p

用于约束限制,用此参数指定一个或多个应用。指定应用之后,monkey将只允许系统启动指定的app;如果不指定应用,将允许系统启动设备中的所有应用

  • 指定一个应用: adb shell monkey -p com.ifeng.news2 100
  • 指定多个应用:adb shell monkey -p com.ifext.news –p com.ifeng.news2  100
  • 不指定应用:adb shell monkey 100
2、参数:-c

用于约束限制,用此参数指定了一个或几个类别,Monkey将只允许系统启动被这些类别中的某个类别列出的Activity。如果不指定任何类别,Monkey将选择下列类别中列出的Activity:Intent.CATEGORY.LAUNCHER 或 Intent.CATEGORY.MONKEY。要指定多个类别,需要使用多个-c选项,每个-c选项只能用于一个类别。

3、参数:-v

用于指定反馈信息级别(信息级别就是日志的详细程度),总共分3个级别:

  • 默认级别 Level 0:-v
    • adb shell monkey -p com.ifeng.news2 –v 100:说明仅提供启动提示、测试完成和最终结果等少量信息
  • 日志级别 Level 1:-v -v
    • adb shell monkey -p com.ifeng.news2 –v -v 100:说明提供较为详细的日志,包括每个发送到Activity的事件信息
  • 日志级别 Level 2:-v -v -v
    • adb shell monkey -p com.ifeng.news2 –v -v –v 100:说明最详细的日志,包括了测试中选中/未选中的Activity信息
4、参数: -s

伪随机数生成器的seed值。如果用相同的seed值再次运行Monkey,它将生成相同的事件序列

  • Monkey 测试1:adb shell monkey -p com.ifeng.news2 -s 10  100
  • Monkey 测试2:adb shell monkey -p com.ifeng.news2 –s 10 100

两次测试的效果是相同的,因为模拟的用户操作序列(每次操作按照一定的先后顺序所组成的一系列操作,即一个序列)是一样的。

5、参数: –throttle<毫秒>

用于指定用户操作(即事件)间的延时,单位是毫秒

adb shell monkey -p com.ifeng.news2 --throttle 5000 100
6、参数: –ignore-crashes

用于指定当应用程序崩溃时(Force& Close错误),Monkey是否停止运行。如果使用此参数,即使应用程序崩溃,Monkey依然会发送事件,直到事件计数完成。

  • adb shellmonkey -p com.ifeng.news2 –ignore-crashes 1000测试过程中即使程序崩溃,Monkey依然会继续发送事件直到事件数目达到1000为止
  • adb shellmonkey -p com.ifeng.news2 1000测试过程中,如果acg程序崩溃,Monkey将会停止运行
7、参数: –ignore-timeouts

用于指定当应用程序发生ANR(Application No Responding)错误时,Monkey是否停止运行。如果使用此参数,即使应用程序发生ANR错误,Monkey依然会发送事件,直到事件计数完成。

adb shellmonkey -p com.ifeng.news2--ignore-timeouts 1000
8、参数: –ignore-security-exceptions

用于指定当应用程序发生许可错误时(如证书许可,网络许可等),Monkey是否停止运行。如果使用此参数,即使应用程序发生许可错误,Monkey依然会发送事件,直到事件计数完成。

adb shellmonkey -p com.ifeng.news2 --ignore-security-exception 1000
9、参数: –kill-process-after-error

用于指定当应用程序发生错误时,是否停止其运行。如果指定此参数,当应用程序发生错误时,应用程序停止运行并保持在当前状态(注意:应用程序仅是静止在发生错误时的状态,系统并不会结束该应用程序的进程)

adb shellmonkey -p cn.emoney.acg --kill-process-after-error 1000
10、参数: –monitor-native-crashes

用于指定是否监视并报告应用程序发生崩溃的本地代码。

adb shellmonkey -p cn.emoney.acg --monitor-native-crashes 1000
11、参数: –pct-{+事件类别}{+事件类别百分比}

用于指定每种类别事件的数目百分比(在Monkey事件序列中,该类事件数目占总事件数目的百分比)

  • –pct-touch{+百分比}:调整触摸事件的百分比(触摸事件是一个down-up事件,它发生在屏幕上的某单一位置)

    adb shell monkey -p com.ifeng.news2 --pct-touch 10 1000
  • –pct-motion {+百分比}:调整动作事件的百分比(动作事件由屏幕上某处的一个down事件、一系列的伪随件机事和一个up事件组成)

    adb shell monkey -p com.ifeng.news2 --   pct-motion 20 1000
  • –pct-trackball {+百分比}:调整轨迹事件的百分比(轨迹事件由一个或几个随机的移动组成,有时还伴随有点击)

    adb shell monkey -p com.ifeng.news2 --pct-trackball 30 1000
  • –pct-nav {+百分比}:调整“基本”导航事件的百分比(导航事件由来自方向输入设备的up/down/left/right组成)

    adb shell monkey -p com.ifeng.news2 --pct-nav 40 1000
  • –pct-majornav {+百分比}:调整“主要”导航事件的百分比(这些导航事件通常引发图形界面中的动作,如:5-way键盘的中间按键、回退按键、菜单按键)

    adb shell monkey -p com.ifeng.news2 --pct-majornav 50 1000
日志输出输出日志的方法:

C:\Documents and Settings\Administrator>adb shell monkey -p 包名 -v 300  >D:\log.txt