Java设计模式(19)-状态模式

(图片来源网络) 生活中,存在很多与"状态"相关的案例,比如:昨天晚上你熬夜看球赛,上午状态很差,迷迷糊糊的,熬到中午睡了一觉,下午又变得精神百倍了。在不同的时间,所处的状态可能不一样,而且还会按照一定条件流转,比如从犯困的状态变成了精神百倍的状态。这种在不同时间、不同条件下状态产生变化的对象,我们称之为"有状态"对象,状态作为其属性会产生变化。 在软件开发过程中,这种状态转换的场景非常多。比如,系统订单随着时间的推移,状态会产生转换,可能从下单后的待支付转换到支付后的待发货,也可能在发货后从待收货变成已收货,等等。 1. 活动状态案例 假设有一个活动需求,管理员可以在系统中添加活动,让用户来参与,同时可以对活动进行管理,比如禁用启用活动、终止活动等。假设活动的状态有:正常、已开始、已结束、已禁用、已终止等,它们之间的流转过程如下图所示: Figure 1. 活动的状态变化 那么,如何实现活动的状态变化呢?传统的方式是将活动的状态定义为枚举类,然后在代码中进行if..else..或者switch的条件判断,通过编码切换到其他的状态,示例代码如下: 活动枚举类 enum ActivityStateEnum { NORMAL, STARTED, FINISHED, DISABLED, TERMINATED (1) } 1 通过枚举来定义活动的不同状态 通过条件判断来实现状态转换 class NormalActivityStateChange { public ActivityStateEnum change(ActivityStateEnum state) { ActivityStateEnum retState = null; switch (state) { case NORMAL: // 省略具体业务逻辑... retState = ActivityStateEnum.STARTED; (1) break; case STARTED: // 省略具体业务逻辑... retState = ActivityStateEnum.FINISHED; (2) break; case DISABLED: // 省略具体业务逻辑... retState = ActivityStateEnum.NORMAL; (3) case TERMINATED: // 省略具体业务逻辑... case FINISHED: // 省略具体业务逻辑... default: // 其他为最终状态,什么都不做 } return retState; } } ...

2021-06-22 · 3 min · 506 words · Hank

Expert One-on-One J2EE Development without EJB中文版PDF下载

你的J2EE项目是否耗费了你太多的时间?它们是否难以调试?它们是否效率不彰?也许你还在使用传统的J2EE方案,然而这种主案太过复杂,而且并非真正面向对象。这里的很多问题都与EJB有关:EJB是一种复杂的技术,但它没有兑现自己曾经的承诺。 1. 站长评价 这是一本很古老的书,同时也是一本介绍Spring框架设计理念、出现缘由的书,也是一本介绍软件设计的书! 在Java开发领域,Spring框架应该说是久负盛名,而且是Java Web开发必备武器。但是,随着Spring的发展壮大, Spring涵盖的东西也越来越多:Spring Boot、Spring Cloud等等以Spring框架(Spring Framework)为基础的Spring生态系统。 所以,对于初学Spring的人而言,可能不知道从何入手。所以,推荐先阅读这本书,本书的作者也是Spring框架的发起者。他在本书中详细介绍了Spring框架的出现原因、Spring框架如何解决一系列企业级问题,如控制反转(IoC)、事务控制、远程调用等等。同时,他还详细阐述了Spring框架架构设计理念和技术抉择,对比当时流行的企业级框架EJB的种种缺陷,一一介绍了Spring框架如何采用轻量级的方式来解决企业级J2EE问题。总之,是一本深入了解Spring框架的经典著作。不论你是初学Spring,还是对Spring已经了如指掌,都可以从本书中学到很多东西。 本书的前5章都在讲述EJB相关的内容,如果不想了解EJB可以直接从第六章开始阅读。 丰富指数:☆☆☆☆ 难度指数:☆☆☆☆ 推荐指数:☆☆☆☆ 迫不及待想要阅读它了?点此立即下载本书的PDF版本(仅供可以学习研究使用,切勿用于商业用途)! 2. 内容简介 在这本实战手册中,你将看到另一种截然不同的方案:没有EJB,却可以创建质量更高的应用程序,所需的时间和成本则更低。你将学会如何充分利用各种实用的技巧和工具,包括时下流行的Spring框架和Hibernate两个开源工具。你将看到如何高效地解决企业级应用的核心问题,例如事务管理、持久化、远程调用和web设计。你将了解这种新的方案给可测试性、性能和可伸缩性带来怎样的影响,并亲身体验轻量级架构如何大幅降低项目开发所需的时间和工作量。 自从servlet、EJB、JSP等J2EE技术发布之初,本书作者Rod Johnson就一直在使用这些技术,他对于这些技术的优劣利弊了如指掌。现在,通过这本书,你将可以面对面地分享他的专家经验。 你将从本书学到…… 如何针对自己的应用程序找到 简单、 易维护的架构;在不使用EJB的情况下有效地管理事务;如何利用AOP和loC解决企业级软件开发中的常见问题;web层设计,以web层在设计良好的J2EE应用中的地位;J2EE应用中的数据访问技术,包括BC、Hibernate和O;如何利用开源产品提升生产率、减少编码量;如何从设计层面上改善性能和可伸缩性。 “传统的J2EE设计思路尤其是EJB日益让架构师和开发者们灰心丧气,我这本书正是为这些人而写的。本书将告诉读者,如何从现在开始用更清晰、更高效的方案去替代EJB,并开始迈向web应用的新时代。” 3. 作者简介 Rod Johnson,Spring框架的创始人,同时也是SpringSource的联合创始人。Spring是面向切面编程(AOP)和控制反转(IoC)的容器框架。Rod的畅销书Expert One-on-One J2EE Design and Development(2002年出版)是迄今为止J2EE领域最具影响力的书之一。 4. 目录 第1章 为什么要“j2ee without ejb” 聚光灯下的ejb j2ee还剩什么? 站在十字路口的j2ee 前行的路 主旋律 轻量级框架和容器 我们还应该使用ejb吗? 小结 第2章 目标 生产率 问题 传统j2ee方案解决生产率问题的办法 提升生产率更好的办法 oo 业务需求的重要性 经验过程的重要性 小结 第3章 各种架构 架构性构件 .业务服务层 向外部暴露业务对象 数据访问层,或eis层 j2ee架构 两种ejb架构 两种非ejb架构 j2ee架构实例 “经典的”j2ee远程ejb架构 本地ejb架构 特制的非ejb架构 “轻量级容器架构”:示例应用系统 确定是否采用应用服务器 小结 ...

2021-06-15 · 3 min · 450 words · Hank

Java设计模式(18)-命令模式

(图片来源于网络) 试想一个去餐厅吃饭的场景,假设餐厅还没有智能化的系统,流程基本上是这样:客人坐下后,服务员会拿出菜单让客人点菜,每点一个菜品就在小本上记录下来,等客人点餐完毕后,服务员将所记录的客人已点菜品清单交给厨房,由厨师来负责烹饪。等待客人用菜完成后,服务员可以拿出记录的点菜清单,然后交给餐厅前台收银员收款结账。这整个过程中,有这么几个角色:客人、服务员、厨师、收银员,现在我们用代码来描述这个场景,怎么来设计呢? 1. 传统的点菜系统设计 传统的方式来设计,我们可能会很容易想到如下的类图关系: Figure 1. 传统方式设计的点菜系统类图 客人直接依赖服务员,使用其点菜服务来点菜,而服务员直接依赖厨师类,通知其完成做菜功能。客人用餐完成后,可以去前台结账。服务员所记录的点菜清单很好的为厨师和收银人员提供了依据:厨师可以借助点菜清单知道自己需要做什么菜,收银人员可以借助点菜清单来计算收款数目。 整个设计初看没有问题,各个功能都实现并且运行的很好。但是,如果某些客人点完菜过后发现,自己点的菜品太多了,可能吃不完而造成粮食的浪费。我们需要系统支持客人取消点菜的功能,一旦客人发现菜品过剩,就可以申请取消,以发扬勤俭节约的美德。 好吧,我们只需要在服务员类上添加"取消点菜"的功能,当服务员收到取消点菜的请求时,他可以在点菜清单上将取消的菜品划掉,然后再交给厨师一个新的点菜清单。此时的类图看起来像这样: Figure 2. 添加了取消点菜功能的类图设计 虽然功能实现了,但是客人、服务员和厨师三者的关系是紧耦合的,随着客人需求的更改,服务员和厨师都需要更改。如果厨师有多个人,每个人负责不同的菜品类别,如炒菜厨师只负责炒菜,而蒸菜、汤品、面食都由不同的厨师负责。那么,客人所点的菜品可能会由多个厨师来完成制作,此时服务员可能会手忙脚乱,因为服务员与厨师紧耦合,客人新增点菜、取消点菜等等请求都需要服务员去与厨师交涉,一旦弄错了对应关系就会造成严重的后果。 其实,服务员与厨师的关系可能是通知和被通知的关系,如果能将这种关系抽象出来,那么服务员和厨师的关系不就解耦了吗?可以将服务员通知厨师看成是给厨师下达一个命令,这个命令有多种支持的操作,如:执行命令、撤销命令、重做命令等等,执行命令时,可以记录日志,以便查阅,就如同服务员手上的"点菜清单",随时都能查看客户点了什么菜、取消了什么菜。 服务员发出命令,我们称之他为请求者,而厨师我们称之为接收者,这种将命令请求者和命令接收者之间添加一层命令抽象的设计模式就是本文要讨论的命令模式。 2. 什么是命令模式 命令模式:将一个请求封装为一个对象,从而可以用不同的请求对客户进行参数化,并且可以对请求排队或记录日志,还支持可以撤销的操作。 Figure 3. 命令模式类图 从图上可以看出,命令模式有几个角色: 请求者(Invoker): 请求的发起者,内部聚合了很多命令对象,可以调用这些命令对象执行相关请求,但是不直接依赖接收者(Receiver); 抽象命令(Abstract Command): 定义了命令通用方法的接口或者抽象类(通常包含execute方法表示执行命令),如执行、撤销、重做等; 具体命令(Concrete Command): 实现或者继承抽象命令,但是自己不直接实现业务逻辑,而是内部聚合多个Receiver,调用其相关方法来完成命令的具体业务实现; 接收者(Receiver): 真正实现了命令的相关业务功能,被具体命令对象依赖; 命令模式常见的场景 遥控器,可以控制家用电器的开关、模式切换、频道切换等,可以使用命令模式来设计,遥控器作为请求者(Invoker),电器作为接收者(Receiver); 前文所述的到餐厅点菜,就可以使用命令模式设计,服务员作为请求者(Invoker),而厨师作为命令接收者(Receiver),抽象命令定义点菜、取消点菜等方法; GUI软件系统界面功能的设计,界面包含多个功能按钮,点击后分别实现不同的功能,此时可以使用命令模式,按钮作为请求者(Invoker),系统后端实现按钮点击后真正功能的类作为接收者(Receiver),而抽象命令可以定义按钮的执行方法,如下图所示: Figure 4. GUI对象将命令委派给命令对象 命令模式的优点 完全符合单一职责原则,每一个具体命令(Concrete Command)负责单独的命令逻辑; 完全符合开闭原则,可以在不修改已有客户端代码的情况下在程序中扩展新的命令; 通过引入中间层(命令层)降低了系统的耦合度,便于命令的扩展; 可以实现命令的撤销和恢复功能;可以实现命令的排队和延迟执行; 可以实现将多个命令组合成复杂的命令。 命令模式的缺点 增加了命令层,系统复杂度增加,带来系统理解上的困难; 具体命令类的数量增加导致系统的类数量增加,系统的开发和维护成本增高。 3. 命令模式代码 接下来看看命名模式的基本代码实现: 抽象命名接口(Abstract Command): interface Command { void execute(); } ...

2021-06-14 · 2 min · 356 words · Hank

Java设计模式(17)-策略模式

策略(Strategy),表示实现某种目标的方案集合。这里的方案,在计算机软件中,就是算法。比如,要实现商场打折的目标,那么可能的方案有多种:商品折扣、商品满减、商品积分等等,这些在软件系统里边都代表了不同的算法实现。再举一个简单的例子:假如从公司回家,你可能有几种方式,如乘坐地铁、乘坐公交,又或者是打网约车、自己开车,等等,这些方案都是为了达到回家这一目标,但是他们都是可以相互替换的,你可以今天坐地铁,明天坐公交,后天懒了不想走路,那么直接打网约车也可以。这样的系统怎么来设计呢? 1. 不使用策略模式 如果我们要实现前边"回家方式"的例子,最普通的方式就是通过条件判断来实现。代码如下: class BackhomeService { public static final int RAILWAY = 1; public static final int BUS = 2; public static final int ONLINE_CAR = 3; public static final int DRIVE = 4; void backHome(int type) { if (type == RAILWAY) { System.out.println("乘坐地铁回家"); } else if (type == BUS) { System.out.println("乘坐公交回家"); } else if (type == ONLINE_CAR) { System.out.println("乘坐网约车回家"); } else if (type == DRIVE) { System.out.println("开车回家"); } } } ...

2021-06-07 · 3 min · 444 words · Hank

Java设计模式(16)-模板方法模式

软件设计过程中,有时候我们发现某一业务场景的基本流程是固定的,但是可能不同业务的某些步骤存在一些差异,但是整体上存在很大的共性。比如,现在全国正大力推动新冠疫苗的接种工作,不论是现在的新冠疫苗,还是以前接种的乙肝、流感等等疫苗,接种的流程其实都差不多,比如可能都需要经过预约、前往、现场审核、接种和最后的观察等步骤。现在我们来设计疫苗接种的类图,怎么设计呢?我们可以找出流程中的共性,设计出几个基本的流程骨架,然后将不同的步骤抽象化,由具体子类去实现,这就是"模板方法模式"。 前边的系列文章,我们已经学习了创建型设计模式和结构型设计模式,从本篇开始开始学习行为型设计模式。模板方法模式属于行为型设计模式的一种,比较简单,我们直接切入正题。 1. 模板方法模式 模板方法模式(Template Method Pattern),也称模板模式,其思想是:定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。 模板方法模式的类图如下: 从图上可以看到,模板方法模式分为2个部分: 抽象模板类(Abstract Template):给出一个算法的轮廓和骨架。它由一个模板方法和若干个基本方法构成。 模板方法:模板方法定义了算法的骨架,内部会顺序调用其他基本方法,实现某一算法流程。通常,模板方法会定义为final的以禁止子类重写; 基本方法:基本方法定义了算法流程中的某一个步骤,它可以定义为抽象方法,具体实现交给子类;也可以定义为普通方法,即提供了默认实现,子类可以选择是否重写。还有一种比较特殊的方法,称为钩子方法,这种方法默认是空方法,子类可以选择是否重写。 具体实现类(Concrete Class):实现抽象模板类定义的抽象方法,并依据情况覆盖抽象模板类提供的基本方法,完成自身特定的业务逻辑处理。 优点 模板方法模式的优点: 可扩展性: 模板方法模式封装了程序不变的部分,将可变的部分延迟到子类实现,可以很容的扩展多个具体子类实现 代码重用: 公共代码抽象到父类中,便于代码的复用 缺点 基于继承体系实现,因此具有继承体系的缺点,如在父类添加抽象方法,所有子类都需要实现 存在多个子类,导致类的数量过多,系统变的庞大 2. 示例 模板方法模式较为简单,我们来看一个demo。 案例:以本文开头的疫苗接种为例,疫苗接种需要经过预约、前往、现场审核、接种和观察等步骤,使用模板方法模式设计程序。 程序代码如下: 1、首先,抽象模板类: 代码清单:抽象模板类 abstract class AbstractVaccinate { protected final String name; public AbstractVaccinate(String name) { this.name = name; } // 疫苗接种 public final void vaccinate() { (1) order(); tripMode(); check(); vaccinating(); if (mustObservate()) (5) observate(); afterVaccinated(); } // 预约 public abstract void order(); (2) // 去往接种目的地方式 public void tripMode() { (3) System.out.println(this.name + " 接种前自行前往接种地..."); } // 审核 public abstract void check(); (2) // 接种 public void vaccinating() { (3) System.out.println(this.name + " 接种中..."); } // 观察 public void observate() { (3) System.out.println(this.name + " 接种后观察30分钟..."); } // 钩子方法,是否必须观察 public boolean mustObservate() { (4) return false; } // 接种完成后 public void afterVaccinated() { (3) System.out.println(this.name + " 接种成功!"); } } ...

2021-05-27 · 2 min · 287 words · Hank

HTTP权威指南中文高清版PDF下载

HTTP是Web的基础,这里所说的Web,不仅只是通常意义上的Browser/Server端的开发,而是Web Service。大多数人对HTTP的了解也就是这种程度了(没有读此书之前的我也是这样)。此书的意义在于,它让你知道,HTTP不仅只有一个简单的GET,HTTP可以做为一种通用的分布式编程的协议。现如今Web上大行其道的所谓Restful Web Service,其基础就是HTTP,而提出Rest这个概念的Roy Fielding,也是HTTP协议(RFC2616)的主要作者之一。 1. 站长评价 Web开发离不开Http,这本《HTTP权威指南》确实是介绍HTTP协议最权威的一本书籍,虽然古老(2012年出版)但是内容丰富,从Http0.9到未来的HTTP-NG,从HTTP基础到HTTP通信安全,可以说涵盖了HTTP协议的方方面面。不论是刚入行的新手,又或是有多年经验的程序大师,反复研读这本巨著肯定会受益匪浅! 丰富指数:☆☆☆☆ 难度指数:☆☆☆ 推荐指数:☆☆☆☆ 迫不及待了?点此立即下载! 2. 内容简介 HTTP(HyperText Transfer Protocol,超文本传输协议)是Web客户端与服务器交互文档和信息时所使用的协议,是每个成功Web事务的幕后推手。众所周知,我们每天访问公司内部网络、搜索绝版书籍、研究统计信息时所使用的浏览器的核心就是HTTP。但HTTP的应用远不仅仅是浏览Web内容。由于HTTP既简单又普及,很多其他网络应用程序也选择了它,尤其是采用SOAP和XML-RPC这样的Web服务。 本书详细解释了HTTP协议,包括它是如何工作的,如何用它来开发基于Web的应用程序。但本书并不只介绍了HTTP,还探讨了HTTP有效工作所依赖的所有其他核心因特网技术。尽管HTTP是本书的中心内容,但本书的本质是理解Web的工作原理,以及如何将这些知识应用到Web编程和管理之中去,主要涵盖HTTP的技术运作方式、产生动机、性能和目标,以及一些相关技术问题。 本书是HTTP协议及相关Web技术方面的著作,主要内容包括: HTTP方法、首部以及状态码 优化代理和缓存的方法 设计Web机器人和爬虫的策略 Cookies、认证以及安全HTTP 国际化及内容协商 重定向及负载平衡策略 本书由具有多年实践经验的专家编写,通过简洁语言和大量翔实的细节图解帮助读者形象地理解Web幕后所发生的事情,详细说明了Web上每条请求的实际运行情况。要想高效地进行Web开发,所有Web程序员、管理员和应用程序开发者都应该熟悉HTTP。很多书籍只介绍了Web的使用方式,而本书则深入说明了Web的工作原理。 3. 作者简介 David Gourley,Endeca的首席技术官(Chief TechnologyOfficer),负责Endeca产品的研究及开发。Endeca开发的因特网及内部网络信息访问解决方案为企业级数据的导航及研究提供了一些新的方式。在到Endeca工作之前,David是Inktomi基础工程组的一员,他在那儿帮助开发了Inktomi的因特网搜索数据库,是Inktomi的Web缓存产品的主要开发者。David在加州大学伯克利分校获得了计算机科学的学士学位,还拥有Web技术方面的几项专利。 BrianTotty,出任了Inktomi公司(这是1996年他参与建立的一家公司)研发部副总裁,在公司中他负责Web缓存、流媒体及因特网搜索技术的研发工作。他曾是SiliconGraphics公司的一名科学家,他在那儿为高性能网络和超级计算机系统设计软件并对其进行优化。在那之前,他是苹果计算机公司高级技术组的一名工程师。Brian在伊利诺伊大学Urbana-Champaign分校获得了计算机科学的博士学位,在MIT获得了计算机科学及电子工程的学士学位,在那里他获得了计算机系统研究的Organick奖。他还为加州大学扩展系统开发并讲授了一些屡获殊荣的因特网技术方面的课程。 MarjorieSayer在Inktomi公司负责编写Web缓存方面的软件。在加州大学伯克利分校获得了数学硕士和博士学位之后,一直致力于数学课程的改革。从1990年开始致力于能量资源管理、并行系统软件、电话和网络方面的写作。 Sailu Reddy目前在Inktomi公司负责嵌入式的性能增强型HTTP代理的开发。Sailu从事复杂软件系统的开发已经有12年了,从1995年开始深入Web架构的研发工作。他是Netscape Web服务器、Web代理产品,以及后面几代产品的核心工程师。他具备HTTP应用程序、数据压缩技术、数据库引擎以及合作管理等方面的技术经验。Sailu在亚里桑那大学获得了信息系统的硕士学位并握有Web技术方面的多项专利。 AnshuAggarwal是Inktomi公司的工程总监。他领导着Inktomi公司Web缓存产品的协议处理工程组,从1997年就开始参与Inktomi的Web技术设计工作。Anshu在科罗拉多大学Boulder分校获得了计算机科学的硕士和博士学位,从事分布式多处理器的内存一致性技术研究。他还拥有电子工程的硕士和学士学位。Anshu撰写了多篇技术论文,还拥有两项专利。 4. 目录 第一部分 HTTP:Web的基础 第1章 HTTP概述 1.1 HTTP——因特网的多媒体信使 1.2 Web客户端和服务器 1.3 资源 1.3.1 媒体类型 1.3.2 URI 1.3.3 URL 1.3.4 URN 1.4 事务 1.4.1 方法 1.4.2 状态码 1.4.3 Web页面中可以包含多个对象 1.5 报文 1.6 连接 1.6.1 TCP/IP 1.6.2 连接、IP地址及端口号 1.6.3 使用Telnet实例 1.7 协议版本 1.8 Web的结构组件 1.8.1 代理 1.8.2 缓存 1.8.3 网关 1.8.4 隧道 1.8.5 Agent代理 1.9 起始部分的结束语 1.10 更多信息 1.10.1 HTTP协议信息 1.10.2 历史透视 1.10.3 其他万维网信息 第2章 URL与资源 2.1 浏览因特网资源 2.2 URL的语法 2.2.1 方案——使用什么协议 2.2.2 主机与端口 2.2.3 用户名和密码 2.2.4 路径 2.2.5 参数 2.2.6 查询字符串 2.2.7 片段 2.3 URL快捷方式 2.3.1 相对URL 2.3.2 自动扩展URL 2.4 各种令人头疼的字符 2.4.1 URL字符集 2.4.2 编码机制 2.4.3 字符限制 2.4.4 另外一点说明 2.5 方案的世界 2.6 未来展望 2.7 更多信息 第3章 HTTP报文 3.1 报文流 3.1.1 报文流入源端服务器 3.1.2 报文向下游流动 3.2 报文的组成部分 3.2.1 报文的语法 3.2.2 起始行 3.2.3 首部 3.2.4 实体的主体部分 3.2.5 版本0.9的报文 3.3 方法 3.3.1 安全方法 3.3.2 GET 3.3.3 HEAD 3.3.4 PUT 3.3.5 POST 3.3.6 TRACE 3.3.7 OPTIONS 3.3.8 DELETE 3.3.9 扩展方法 3.4 状态码 3.4.1 100~199——信息性状态码 3.4.2 200~299——成功状态码 3.4.3 300~399——重定向状态码 3.4.4 400~499——客户端错误状态码 3.4.5 500~599——服务器错误状态码 3.5 首部 3.5.1 通用首部 3.5.2 请求首部 3.5.3 响应首部 3.5.4 实体首部 3.6 更多信息 第4章 连接管理 4.1 TCP连接 4.1.1 TCP的可靠数据管道 4.1.2 TCP流是分段的、由IP分组传送 4.1.3 保持TCP连接的正确运行 4.1.4 用TCP套接字编程 4.2 对TCP性能的考虑 4.2.1 HTTP事务的时延 4.2.2 性能聚焦区域 4.2.3 TCP连接的握手时延 4.2.4 延迟确认 4.2.5 TCP慢启动 4.2.6 Nagle算法与TCP_NODELAY 4.2.7 TIME_WAIT累积与端口耗尽 4.3 HTTP连接的处理 4.3.1 常被误解的Connection首部 4.3.2 串行事务处理时延 4.4 并行连接 4.4.1 并行连接可能会提高页面的加载速度 4.4.2 并行连接不一定更快 4.4.3 并行连接可能让人“感觉”更快一些 4.5 持久连接 4.5.1 持久以及并行连接 4.5.2 HTTP/1.0+ keep-alive连接 4.5.3 Keep-Alive操作 4.5.4 Keep-Alive选项 4.5.5 Keep-Alive连接的限制和规则 4.5.6 Keep-Alive和哑代理 4.5.7 插入Proxy-Connection 4.5.8 HTTP/1.1持久连接 4.5.9 持久连接的限制和规则 4.6 管道化连接 4.7 关闭连接的奥秘 4.7.1 “任意”解除连接 4.7.2 Content-Length及截尾操作 4.7.3 连接关闭容限、重试以及幂等性 4.7.4 正常关闭连接 4.8 更多信息 4.8.1 HTTP连接 4.8.2 HTTP性能问题 4.8.3 TCP/IP 第二部分 HTTP结构 第5章 Web服务器 5.1 各种形状和尺寸的Web服务器 5.1.1 Web服务器的实现 5.1.2 通用软件Web服务器 5.1.3 Web服务器设备 5.1.4 嵌入式Web服务器 5.2 最小的Perl Web服务器 5.3 实际的Web服务器会做些什么 5.4 第一步——接受客户端连接 5.4.1 处理新连接 5.4.2 客户端主机名识别 5.4.3 通过ident确定客户端用户 5.5 第二步——接收请求报文 5.5.1 报文的内部表示法 5.5.2 连接的输入/输出处理结构 5.6 第三步——处理请求 5.7 第四步——对资源的映射及访问 5.7.1 docroot 5.7.2 目录列表 5.7.3 动态内容资源的映射 5.7.4 服务器端包含项 5.7.5 访问控制 5.8 第五步——构建响应 5.8.1 响应实体 5.8.2 MIME类型 5.8.3 重定向 5.9 第六步——发送响应 5.10 第七步——记录日志 5.11 更多信息 第6章 代理 6.1 Web的中间实体 6.1.1 私有和共享代理 6.1.2 代理与网关的对比 6.2 为什么使用代理 6.3 代理会去往何处 6.3.1 代理服务器的部署 6.3.2 代理的层次结构 6.3.3 代理是如何获取流量的 6.4 客户端的代理设置 6.4.1 客户端的代理配置:手工配置 6.4.2 客户端代理配置:PAC文件 6.4.3 客户端代理配置:WPAD 6.5 与代理请求有关的一些棘手问题 6.5.1 代理URI与服务器URI的不同 6.5.2 与虚拟主机一样的问题 6.5.3 拦截代理会收到部分URI 6.5.4 代理既可以处理代理请求,也可以处理服务器请求 6.5.5 转发过程中对URI的修改 6.5.6 URI的客户端自动扩展和主机名解析 6.5.7 没有代理时URI的解析 6.5.8 有显式代理时URI的解析 6.5.9 有拦截代理时URI的解析 6.6 追踪报文 6.6.1 Via首部 6.6.2 TRACE方法 6.7 代理认证 6.8 代理的互操作性 6.8.1 处理代理不支持的首部和方法 6.8.2 OPTIONS:发现对可选特性的支持 6.8.3 Allow首部 6.9 更多信息 第7章 缓存 7.1 冗余的数据传输 7.2 带宽瓶颈 7.3 瞬间拥塞 7.4 距离时延 7.5 命中和未命中的 7.5.1 再验证 7.5.2 命中率 7.5.3 字节命中率 7.5.4 区分命中和未命中的情况 7.6 缓存的拓扑结构 7.6.1 私有缓存 7.6.2 公有代理缓存 7.6.3 代理缓存的层次结构 7.6.4 网状缓存、内容路由以及对等缓存 7.7 缓存的处理步骤 7.7.1 第一步——接收 7.7.2 第二步——解析 7.7.3 第三步——查找 7.7.4 第四步——新鲜度检测 7.7.5 第五步——创建响应 7.7.6 第六步——发送 7.7.7 第七步——日志 7.7.8 缓存处理流程图 7.8 保持副本的新鲜 7.8.1 文档过期 7.8.2 过期日期和使用期 7.8.3 服务器再验证 7.8.4 用条件方法进行再验证 7.8.5 If-Modified-Since:Date再验证 7.8.6 If-None-Match:实体标签再验证 7.8.7 强弱验证器 7.8.8 什么时候应该使用实体标签和最近修改日期 7.9 控制缓存的能力 7.9.1 no-Store与no-Cache响应首部 7.9.2 max-age响应首部 7.9.3 Expires响应首部 7.9.4 must-revalidate响应首部 7.9.5 试探性过期 7.9.6 客户端的新鲜度限制 7.9.7 注意事项 7.10 设置缓存控制 7.10.1 控制Apache的HTTP首部 7.10.2 通过HTTP-EQUIV控制HTML缓存 7.11 详细算法 7.11.1 使用期和新鲜生存期 7.11.2 使用期的计算 7.11.3 完整的使用期计算算法 7.11.4 新鲜生存期计算 7.11.5 完整的服务器——新鲜度算法 7.12 缓存和广告 7.12.1 发布广告者的两难处境 7.12.2 发布者的响应 7.12.3 日志迁移 7.12.4 命中计数和使用限制 7.13 更多信息 第8章 集成点:网关、隧道及中继 8.1 网关 8.2 协议网关 8.2.1 HTTP/*:服务器端Web网关 8.2.2 HTTP/HTTPS:服务器端安全网关 8.2.3 HTTPS/HTTP客户端安全加速器网关 8.3 资源网关 8.3.1 CGI 8.3.2 服务器扩展API 8.4 应用程序接口和Web服务 8.5 隧道 8.5.1 用CONNECT建立HTTP隧道 8.5.2 数据隧道、定时及连接管理 8.5.3 SSL隧道 8.5.4 SSL隧道与HTTP/HTTPS网关的对比 8.5.5 隧道认证 8.5.6 隧道的安全性考虑 8.6 中继 8.7 更多信息 第9章 Web机器人 9.1 爬虫及爬行方式 9.1.1 从哪儿开始:根集 9.1.2 链接的提取以及相对链接的标准化 9.1.3 避免环路的出现 9.1.4 循环与复制 9.1.5 面包屑留下的痕迹 9.1.6 别名与机器人环路 9.1.7 规范化URL 9.1.8 文件系统连接环路 9.1.9 动态虚拟Web空间 9.1.10 避免循环和重复 9.2 机器人的HTTP 9.2.1 识别请求首部 9.2.2 虚拟主机 9.2.3 条件请求 9.2.4 对响应的处理 9.2.5 User-Agent导向 9.3 行为不当的机器人 9.4 拒绝机器人访问 9.4.1 拒绝机器人访问标准 9.4.2 Web站点和robots.txt文件 9.4.3 robots.txt文件的格式 9.4.4 其他有关robots.txt的知识 9.4.5 缓存和robots.txt的过期 9.4.6 拒绝机器人访问的Perl代码 9.4.7 HTML的robot-control元标签 9.5 机器人的规范 9.6 搜索引擎 9.6.1 大格局 9.6.2 现代搜索引擎结构 9.6.3 全文索引 9.6.4 发布查询请求 9.6.5 对结果进行排序,并提供查询结果 9.6.6 欺诈 9.7 更多信息 第10章 HTTP-NG 10.1 HTTP发展中存在的问题 10.2 HTTP-NG的活动 10.3 模块化及功能增强 10.4 分布式对象 10.5 第一层——报文传输 10.6 第二层——远程调用 10.7 第三层——Web应用 10.8 WebMUX 10.9 二进制连接协议 10.10 当前的状态 10.11 更多信息 第三部分 识别、认证与安全 第11章 客户端识别与cookie机制 11.1 个性化接触 11.2 HTTP首部 11.3 客户端IP地址 11.4 用户登录 11.5 胖URL 11.6 cookie 11.6.1 cookie的类型 11.6.2 cookie是如何工作的 11.6.3 cookie罐:客户端的状态 11.6.4 不同站点使用不同的cookie 11.6.5 cookie成分 11.6.6 cookies版本0(Netscape) 11.6.7 cookies版本1(RFC 2965) 11.6.8 cookie与会话跟踪 11.6.9 cookie与缓存 11.6.10 cookie、安全性和隐私 11.7 更多信息 第12章 基本认证机制 12.1 认证 12.1.1 HTTP的质询/响应认证框架 12.1.2 认证协议与首部 12.1.3 安全域 12.2 基本认证 12.2.1 基本认证实例 12.2.2 Base-64用户名/密码编码 12.2.3 代理认证 12.3 基本认证的安全缺陷 12.4 更多信息 第13章 摘要认证 13.1 摘要认证的改进 13.1.1 用摘要保护密码 13.1.2 单向摘要 13.1.3 用随机数防止重放攻击 13.1.4 摘要认证的握手机制 13.2 摘要的计算 13.2.1 摘要算法的输入数据 13.2.2 算法H(d)和KD(s,d) 13.2.3 与安全性相关的数据(A1) 13.2.4 与报文有关的数据(A2) 13.2.5 摘要算法总述 13.2.6 摘要认证会话 13.2.7 预授权 13.2.8 随机数的选择 13.2.9 对称认证 13.3 增强保护质量 13.3.1 报文完整性保护 13.3.2 摘要认证首部 13.4 应该考虑的实际问题 13.4.1 多重质询 13.4.2 差错处理 13.4.3 保护空间 13.4.4 重写URI 13.4.5 缓存 13.5 安全性考虑 13.5.1 首部篡改 13.5.2 重放攻击 13.5.3 多重认证机制 13.5.4 词典攻击 13.5.5 恶意代理攻击和中间人攻击 13.5.6 选择明文攻击 13.5.7 存储密码 13.6 更多信息 第14章 安全HTTP 14.1 保护HTTP 的安全 14.2 数字加密 14.2.1 密码编制的机制与技巧 14.2.2 密码 14.2.3 密码机 14.2.4 使用了密钥的密码 14.2.5 数字密码 14.3 对称密钥加密技术 14.3.1 密钥长度与枚举攻击 14.3.2 建立共享密钥 14.4 公开密钥加密技术 14.4.1 RSA 14.4.2 混合加密系统和会话密钥 14.5 数字签名 14.6 数字证书 14.6.1 证书的主要内容 14.6.2 X.509 v3证书 14.6.3 用证书对服务器进行认证 14.7 HTTPS——细节介绍 14.7.1 HTTPS概述 14.7.2 HTTPS方案 14.7.3 建立安全传输 14.7.4 SSL握手 14.7.5 服务器证书 14.7.6 站点证书的有效性 14.7.7 虚拟主机与证书 14.8 HTTPS客户端实例 14.8.1 OpenSSL 14.8.2 简单的HTTPS客户端 14.8.3 执行OpenSSL客户端 14.9 通过代理以隧道形式传输安全流量 14.10 更多信息 14.10.1 HTTP安全性 14.10.2 SSL与TLS 14.10.3 公开密钥基础设施 14.10.4 数字密码 第四部分 实体、编码和国际化 第15章 实体和编码 15.1 报文是箱子,实体是货物 15.2 Content-Length: 实体的大小 15.2.1 检测截尾 15.2.2 错误的Content-Length 15.2.3 Content-Length与持久连接 15.2.4 内容编码 15.2.5 确定实体主体长度的规则 15.3 实体摘要 15.4 媒体类型和字符集 15.4.1 文本的字符编码 15.4.2 多部分媒体类型 15.4.3 多部分表格提交 15.4.4 多部分范围响应 15.5 内容编码 15.5.1 内容编码过程 15.5.2 内容编码类型 15.5.3 Accept-Encoding首部 15.6 传输编码和分块编码 15.6.1 可靠传输 15.6.2 Transfer-Encoding首部 15.6.3 分块编码 15.6.4 内容编码与传输编码的结合 15.6.5 传输编码的规则 15.7 随时间变化的实例 15.8 验证码和新鲜度 15.8.1 新鲜度 15.8.2 有条件的请求与验证码 15.9 范围请求 15.10 差异编码 15.11 更多信息 第16章 国际化 16.1 HTTP对国际性内容的支持 16.2 字符集与HTTP 16.2.1 字符集是把字符转换为二进制码的编码 16.2.2 字符集和编码如何工作 16.2.3 字符集不对,字符就不对 16.2.4 标准化的MIME charset值 16.2.5 Content-Type首部和Charset首部以及META标志 16.2.6 Accept-Charset首部 16.3 多语言字符编码入门 16.3.1 字符集术语 16.3.2 字符集的命名很糟糕 16.3.3 字符 16.3.4 字形、连笔以及表示形式 16.3.5 编码后的字符集 16.3.6 字符编码方案 16.4 语言标记与HTTP 16.4.1 Content-Language首部 16.4.2 Accept-Language首部 16.4.3 语言标记的类型 16.4.4 子标记 16.4.5 大小写 16.4.6 IANA语言标记注册 16.4.7 第一个子标记——名字空间 16.4.8 第二个子标记——名字空间 16.4.9 其余子标记——名字空间 16.4.10 配置和语言有关的首选项 16.4.11 语言标记参考表 16.5 国际化的URI 16.5.1 全球性的可转抄能力与有意义的字符的较量 16.5.2 URI字符集合 16.5.3 转义和反转义 16.5.4 转义国际化字符 16.5.5 URI中的模态切换 16.6 其他需要考虑的地方 16.6.1 首部和不合规范的数据 16.6.2 日期 16.6.3 域名 16.7 更多信息 16.7.1 附录 16.7.2 互联网的国际化 16.7.3 国际标准 第17章 内容协商与转码 17.1 内容协商技术 17.2 客户端驱动的协商 17.3 服务器驱动的协商 17.3.1 内容协商首部集 17.3.2 内容协商首部中的质量值 17.3.3 随其他首部集而变化 17.3.4 Apache中的内容协商 17.3.5 服务器端扩展 17.4 透明协商 17.4.1 进行缓存与备用候选 17.4.2 Vary首部 17.5 转码 17.5.1 格式转换 17.5.2 信息综合 17.5.3 内容注入 17.5.4 转码与静态预生成的对比 17.6 下一步计划 17.7 更多信息 第五部分 内容发布与分发 第18章 Web主机托管 18.1 主机托管服务 18.2 虚拟主机托管 18.2.1 虚拟服务器请求缺乏主机信息 18.2.2 设法让虚拟主机托管正常工作 18.2.3 HTTP/1.1的Host首部 18.3 使网站更可靠 18.3.1 镜像的服务器集群 18.3.2 内容分发网络 18.3.3 CDN中的反向代理缓存 18.3.4 CDN中的代理缓存 18.4 让网站更快 18.5 更多信息 第19章 发布系统 19.1 FrontPage为支持发布而做的服务器扩展 19.1.1 FrontPage服务器扩展 19.1.2 FrontPage术语表 19.1.3 FrontPage的RPC协议 19.1.4 FrontPage的安全模型 19.2 WebDAV与协作写作 19.2.1 WebDAV的方法 19.2.2 WebDAV与XML 19.2.3 WebDAV首部集 19.2.4 WebDAV的锁定与防止覆写 19.2.5 LOCK方法 19.2.6 UNLOCK方法 19.2.7 属性和元数据 19.2.8 PROPFIND方法 19.2.9 PROPPATCH方法 19.2.10 集合与名字空间管理 19.2.11 MKCOL方法 19.2.12 DELETE方法 19.2.13 COPY与MOVE方法 19.2.14 增强的HTTP/1.1方法 19.2.15 WebDAV中的版本管理 19.2.16 WebDAV的未来发展 19.3 更多信息 第20章 重定向与负载均衡 20.1 为什么要重定向 20.2 重定向到何地 20.3 重定向协议概览 20.4 通用的重定向方法 20.4.1 HTTP重定向 20.4.2 DNS重定向 20.4.3 任播寻址 20.4.4 IP MAC转发 20.4.5 IP地址转发 20.4.6 网元控制协议 20.5 代理的重定向方法 20.5.1 显式浏览器配置 20.5.2 代理自动配置 20.5.3 Web代理自动发现协议 20.6 缓存重定向方法 20.7 因特网缓存协议 20.8 缓存阵列路由协议 20.9 超文本缓存协议 20.9.1 HTCP认证 20.9.2 设置缓存策略 20.10 更多信息 第21章 日志记录与使用情况跟踪 21.1 记录内容 21.2 日志格式 21.2.1 常见日志格式 21.2.2 组合日志格式 21.2.3 网景扩展日志格式 21.2.4 网景扩展2日志格式 21.2.5 Squid代理日志格式 21.3 命中率测量 21.3.1 概述 21.3.2 Meter首部 21.4 关于隐私的考虑 21.5 更多信息 第六部分 附录 附录A URI方案 附录B HTTP状态码 附录C HTTP首部参考 附录D MIME类型 附录E Base-64编码 附录F 摘要认证 附录G 语言标记 附录H MIME字符集注册表 索引 ...

2021-04-12 · 6 min · 1228 words · Hank

IntelliJ IDEA 2020.3.x激活教程

IntelliJ IDEA作为Java界IDE神一般的存在,深受广大java开发者的喜欢。目前官方最新版本为2020.3,本文介绍如何对其进行激活。 实际上,IDEA 2020.3版本的激活方式跟之前的2019版本相比并没有太大区别,基本思路都是一样的,2019版本的IDEA激活方法可以看这几篇文章: IntelliJ IDEA 2019.3.3激活 IntelliJ IDEA 2019.2.x激活 1. 详细激活步骤 具体的激活步骤如下: 1、首先,从官方网站下载IDEA的2020.3版本安装包,下载地址为: https://www.jetbrains.com/idea/download 2、然后,安装IDEA,没什么好说的,一步一步安装就对了 PS: 什么?你居然不会安装?我觉得你还是趁早换个职业吧😅 3、安装完成过后,启动IDEA,先选择试用软件,如下图所示: 选择“Evaluate for free”过后,确定即可。 4、下载IDEA激活包,下载地址在文章最后。下载过后解压,得到lib目录下的BetterIntelliJ-1.16.jar文件,就是激活包,将其拷贝到任意位置(建议英文路径中)。比如我的路径为: D:\install\idea\BetterIntelliJ-1.16.jar 还有一个重要的文件:激活补丁key.txt,里边存储了激活码,后边激活会用到。 5、然后打开软件,点击菜单栏的“help”菜单,点击"Edit Custom VM Options …​"菜单,来自定义VM参数,如下图所示: 在打开的窗口中,增加如下参数: -javaagent:D:\install\idea\BetterIntelliJ-1.16.jar 如下图所示: 6、重启IDEA,然后点击help菜单的Register…​项,填入第4布中激活补丁key.txt文件中的激活码,激活即可,如图所示: 2. 激活成功 ok,您的IDEA已经成功激活到2100年,赶紧操练起来吧! 3. 说明 为了使激活长期有效,注意不要更改激活的jar包文件所在的目录路径和文件名,另外请关闭IDEA的自动更新! IDEA激活包下载地址: 链接: https://pan.baidu.com/s/1LVsi5Dvj2o9L8IuTfekEAQ 提取码:8k1y ...

2021-03-27 · 1 min · 45 words · Hank

Java设计模式(15)-代理模式

虽然马上就是2021年了,但是大家回想起去年(2020年初)的时候,是不是高兴不起来呢?人类还没有彻底战胜新冠病毒之前,我们还是要遵从国家号召:多居家、少聚集、勤洗手、戴口罩!疫情期间,我们足不出户免不了要点外卖。如果没有外卖配送服务,那么你可能得自己去餐厅取餐了。还好,我们可以享受外卖配送服务,只需要点好餐,然后外卖小哥回到餐厅取餐然后直接送到我们手上,这样我们就不需要跟餐厅直接接触了,也减少了感染的风险。其实,外卖小哥就是一个代理,他作为一个中间角色(其实就是中介),帮助用户去餐厅取餐然后送餐给用户,这样用户就不需要直接面对餐厅。 1. 什么是代理 什么是代理?代理就是中介,它提供与服务提供方相同或者相似的功能,客户不再直接面对服务提供方,而是先找代理,再由代理去与服务提供方交互完成功能。现实生活中,有很多代理的例子,比如前边提到的外卖订餐,外卖小哥就是一个代理角色;又如,购买火车票,火车票代售点就是一个代理角色,用户直接可以去代售点购买车票;再如,各种中介所,如婚姻中介所、房产中介等等,都是代理。 在软件中,代理主要指的就是本文要探讨的代理模式,或者模式中的代理类,我们 稍后 再细说。 在网络中,代理服务器(Proxy Server),专门用来代理网络用户去取得网络信息。代理服务器又有正向代理和反向代理的区分。 1.1. 正向代理和反向代理 正向代理和反向代理,其实是按照代理服务所在位置、客户端对代理服务器是否有感知来考虑的。 1、正向代理 正向代理,其服务器位于客户端,客户端需要访问目标服务器,但是不会直接访问,而是先访问代理服务器,然后再由代理服务器转发请求到目标服务器。也就是说,正向代理其代理的是客户端的请求,客户端明确知道代理服务器的地址,然后在连接到代理服务器使其转发请求到目标服务器。其原理如下: Figure 1. 正向代理示意图 如图所示,客户端链接到代理服务器(Proxy),客户端和代理服务器位于同一个网络中。 正向代理的主要用途是,客户端由于某些原因不能直接访问目标服务器,但是代理服务器可以,因此通过代理服务器作为跳板从而达到访问目标服务器的目的。例如,我们需要访问Google网站,但是国内无法访问,我们需要使用一些代理服务或软件(它们可以访问Google),这些服务和软件就是正向代理服务器。各大操作系统都有代理服务设置,例如macOS的: Figure 2. MacOS中网络代理设置 2、反向代理 反向代理,其实是相当于正向代理而言的。在反向代理中,代理服务器与目标服务器位于同一网络中,代理服务器代理并隐藏目标服务器,客户端无需知道代理服务器的存在。原理如下: Figure 3. 反向代理示意图 代理服务器就像服务端的一道防火墙,既可以过滤掉非法的请求,又可以隐藏目标服务其的真实地址等信息,对外提供统一的访问入口,提到服务端的安全性。另外,代理服务器能够实现网络负载均衡,将请求分发到不同的目标服务器上,以缓解请求压力,提高访问速度和性能。 反向代理在网络应用中使用非常普遍,常见的软件如Nginx、Apache等都可以作为反向代理服务器。 1.2. 为什么要使用代理 前边说过了正向代理和反向代理的作用,总结一下,代理有如下的作用: 访问客户端不能直接访问的服务器,比如Google、Facebook等 提高网络访问速度,例如访问某些网站速度很慢,但是代理服务器访问之很快,这样客户端通过访问代理服务器可以提高网络速度,比如常见的网络加速器 实现安全保护,比如反向代理服务器可以设置规则过滤非法请求,也可以隐藏服务器IP地址和端口以保护后端服务器,还可以做权限认证等 实施负载均衡,反向代理服务器可以按照既定策略将不同的请求分发到不同的后端服务器上,缓解其访问压力,如常见的轮询、按IP Hash、按请求URL hash、随机选择、最小访问、按地域选择等负载均衡策略 实现动静分离,反向代理服务器支持直接访问静态资源,不需要后端服务器支持,这样可以加速静态页面的访问,如Nginx对静态资源的访问支持非常好,性能强大 了解了什么是代理,接下来我们看看在计算机软件中,代理模式的概念。 2. 代理模式简介 技术来源于生活,代理模式是一种生活中代理运用的总结。DP对代理模式(Proxy Pattern)的定义如下:给对象提供一个代理以控制对该对象的访问。 当客户端不能直接依赖目标对象,又或是想要增强目标对象的功能,可以为其创建一个代理对象(中介),然后客户端访问代理对象而不直接依赖目标对象,再由中介对象来访问目标对象。这种设计模式就是代理模式,其结构如下图所示: 可以看到,代理模式有几种角色: 抽象主题角色(Subject: 抽象类或接口,用来定义业务功能的公共方法 真实主题角色(RealSubject): 实现了抽象主题(Subject)接口,完成具体业务功能实现。它就是目标对象,客户端需要依赖它提供的功能 代理对象(Proxy): RealSubject的代理实现,保证与其具有相同的功能,因此也会实现Subject接口。代理对象内部会聚合RealSubject,具体功能会调用RealSubject来完成,同时内部也可以增强RealSubject的功能 从结构上看,代理模式与 适配器模式 类似,两者都存在一个中间对象,其都会聚合具体对象并实现一个抽象接口,但是它们的目的并不相同:代理模式更强调对象的访问控制和功能增强,而适配器模式更强调功能的转换。 代理模式的主要优点: 代理模式在客户端与目标对象之间增加了中间对象,可以控制目标对象访问 代理对象可以扩展目标对象的功能 其主要缺点: 代理模式会造成系统设计中类的数量增加 在客户端和目标对象之间增加一个代理对象,中转过程会影响请求速度 增加了系统的复杂度 3. Java中代理实现的三种方式 代理可以分为静态代理和动态代理,动态代理技术有效的减少了上边的缺点。 ...

2020-12-31 · 3 min · 588 words · Hank

Java设计模式(14)-享元模式

享元模式,"享"即共享,"元"即元素,软件中则为对象,享元就是共享对象之意。这种设计模式用来共享对象而不是大量创建对象,以节约系统资源。现实中,很多东西都可以使用享元模式来解决,比如围棋、五子棋,棋子的颜色就黑白两种,只是他们在棋盘的位置不同;又如,展示类网站,多个用户公用一套系统,只是内容和展示形式存在差异;再如,教室的课桌和凳子…​上述示例都有一个共同点:相关的东西存在很大的相似,但是也不完全相同,此时如果要开发软件,那么都可以用享元模式来设计。 1. 从围棋游戏开始 假设要你开发一款围棋游戏,我们知道,围棋棋子有黑色和白色两种,棋子在棋盘上的位置随着下棋的进行而不同,那么你怎么设计类关系? 常规的思路是使用工厂模式,棋手每落下一枚棋子,那么就创建一个棋子实例,并设置它放置的位置: Figure 1. 使用工厂模式 但是,围棋有 19 × 19 = 361 个交叉点,假设每个点上全部都有棋子,那么足足需要创建361个对象!这无疑是对系统资源的浪费。 那么,有好的解决办法吗?答案是使用享元模式来设计。 2. 什么是享元模式 享元模式(flyweight),也叫蝇量模式,它强调对象的细粒度控制和共享,主张运用共享技术有效地支持对象的复用。"享"即共享,"元"即元素,软件中则为对象,说白了就是最大程度的让对象可以共享并复用。享元模式的类结构如图所示: Figure 2. 享元模式类结构 如图所示,可以看出,享元模式有如下几种角色: 抽象享元角色(Flyweight):具体享元对象的超类或接口,可以接受作用于外部状态(见后文); 具体享元角色(ConcreteFlyweight):继承或实现抽象享元角色,为内部状态增加存储空间; 非共享享元角色(UnsharedConcreteFlyweight):同样继承或实现抽象享元角色,但是这些类并不需要共享出来; 享元工厂角色:管理Flyweight对象,确保合理的共享他们。它往往提供获取Flyweight对象的方法,当对象存在时直接返回,否则创建一个。 提示 有的书上也将UnsharedConcreteFlyweight对象单独提出来,将其看做外部状态的抽象,它并不实现Flyweight,而是作为Flyweight接口方法的参数进行传递,以说明它不共享。 享元模式的结构也好理解:抽象一个享元接口,提供通用的方法,然具体享元对象实现该接口,但是并不是所有逇具体享元对象都需要共享,因此按需共享并拆分,最后由享元工厂统一管理他们。上边还提到两个概念:外部状态和内部状态,它们是什么? 2.1. 外部状态和内部状态 在享元模式中,对象状态按是否共享分为两种:共享和不共享,随着环境变化而改变的、不可以共享的状态称为外部状态;相反,不会随着环境变化而改变的状态称为内部状态。 使用享元模式,重点是要分析出哪些对象是外部状态,哪些是内部状态,并将外部状态单独抽象出来,作为一个变化的部分。 在前边围棋的例子中,棋子的颜色只有黑、白两种,它们不会随着下棋的进行而增加或者减少,因此棋子的颜色是内部状态。而棋子在棋盘上放置的位置,随着下棋的进行,棋子的位置都会不同,因此,棋子的位置是外部状态。 又比如,多个用户共用一个网站,网站可以作为Flyweight对象而共享,而网站的代码、模板、数据库都是不会变化(增加或减少)的,它们可作为内部状态来共享。但是,不同的用户账号是不同的,每个用户都有自己额账号,因此,账号可以作为外部状态。此时就可以将账号单独提取出来作为一个变化的实体: Figure 3. 共享网站的设计类图 3. 围棋游戏改进 我们再来看看如何使用享元模式来解决围棋中重复创建对象、浪费资源的问题。 首先,前边已经分析了,围棋棋子的颜色是内部状态,而其在棋盘的位置是外部状态。那么,我们可以设计如下的类图: Figure 4. 围棋使用享元模式设计的类图 内部对象定义为Color类,外部对象抽象为Position类,Piece为抽象的Flyweight对象,WeiqiPiece作为具体的享元对象实现了Piece接口。最后,这些对象通过WeiqiPieceFactory统一管理。示例代码如下: 1、定义外部状态类: // 外部状态: 棋子位置 class Position { private final int x; private final int y; public Position(int x, int y) { this.x = x; this.y = y; } public String position() { (1) return "(" + x + "," + y + ")"; } } ...

2020-12-21 · 2 min · 326 words · Hank

Java设计模式(13)-外观模式

在现实生活中有很多这样的例子,比如,炒股,散户股民大多对股票金融知识缺乏,所以他们想要炒股赚钱往往看运气,但是散户想要保证低风险的投资和拿到稳定回报,怎么办呢?大多数人会选择购买基金,相对于股票,散户们不需要专业知识,只需要将自己交给基金会,由专业人士替他们购买股票,赚到钱后再按资金比例分给他们,这样就大大降低了投资风险。 Figure 1. 股票与基金示意图 基金作为一个中间对象,它解决了股民专业知识匮乏却也能投资股票赚钱的需求。其实,这就是外观模式。 1. 外观模式简介 外观模式(Facade Pattern),也叫门面模式,是一种经常被使用的结构性设计模式。DP对其定义如下:通过为多个复杂的子系统提供一个一致的接口,而使这些子系统更加容易被访问。 还记得 迪米特法则] 吗? 迪米特法则(Law of Demeter,LoD),又叫作最少知识原则(Least Knowledge Principle,LKP),描述了如何设计对象之间的依赖关系,它的定义如下:一个对象对自己所依赖的对象知道的越少越好。更进一步的意思,即:只与你的直接朋友交谈,不跟“陌生人”说话(Talk only to your immediate friends and not to strangers)。 外观模式严格体现了迪米特法则,它降低了系统之间的耦合性和复杂度,提高了系统的可维护性。 外观模式有三种角色: 外观(Facade)角色:整合多个子系统,为其提供统一的接口实以现业务需求; 子系统(SubSystem)角色: 实现某方面的业务功能,供外观角色调用,但是并不知道外观角色的存在; 客户端(Client)角色:调用外观角色访问各个子系统实现不同的业务功能。 其类结构如下图所示: Figure 2. 外观模式类图 我们在软件开发中,经常会有意无意的使用外观模式。比如,MVC模式业务开发中,Service层会屏蔽Dao层的实现细节,对外只暴露一个方法,该方法内部可能会需要使用多个Dao来完成数据库操作。又比如现在流行的微服务架构,API网关就承担着外观角色,客户端直接访问网关,而不会直接去请求各个具体的服务。 Figure 3. API网关示意图 再举一个现在比较专注的例子:智能家居。当你装上了智能家居系统,那么你就可以一键控制家用电器的开关,是不是很舒服?比如,你早上起床,控制器会一键为你打开电动窗帘、播放闹钟、关闭空调等等,再也不用一个一个去操控了。我们看看怎么使用外观模式解决这个问题。 2. 应用案例:智能家居 假设我家安装了一些智能设备:智能空调、智能音响、自动窗帘、智能电灯等,现在我可不想一个个去控制它们。是时候请出超级管家了,它来负责一键操作这些设备。设计类图如下: Figure 4. 智能家居简易设计类图 智能管家作为指挥中心,它具有问好、叫醒起床、上班前告别、下班回家欢迎等功能,当然,假设我们可以通过语音和App操控它。这些功能,智能管家自己不会实现,而是通过调用下边的各个智能设备共同完成。我们看看代码如何实现。 1、智能设备,多个子系统,提供了自身的功能方法。 // 窗帘 class SmartCurtain { public void open() { System.out.println("打开窗帘"); } public void close() { System.out.println("关闭窗帘"); } } // 灯光 class SmartLight { public void open() { System.out.println("打开灯光"); } public void close() { System.out.println("关闭灯光"); } } // 空调 class AirConditioner { public void open() { System.out.println("打开空调"); } public void close() { System.out.println("关闭空调"); } } // 智能音响 class SmartSoundBox { public void play() { System.out.println("播放音乐"); } public void stop() { System.out.println("停止播放音乐"); } } ...

2020-12-08 · 2 min · 271 words · Hank

Java设计模式(12)-组合模式

1. 整体与部分的关系 很多场景下,需要考虑整体和部分之间的关系,在对其进行管理功能设计时,需要考虑到灵活性和扩展性,而继承关系往往不能很好地解决这些问题。一个最典型的例子就是:组织机构。比如,就公司而言,总公司可能下设有分公司,分公司下又有办事处,而办事处、分公司、总公司可能都会存在一些职能部门。很明显,组织机构是一个整体和部分的关系,并且它是一颗树状结构。 Figure 1. 复杂的组织机构树状结构 设计这样的结构,你是否会使用继承关系?如让分公司继承总公司、办事处继承分公司,这样往往是从机构的大小维度来划分的,子类除了具备父类的功能外,还能够独立扩展功能。但是,在组织机构树中,其实每个节点(机构)所承担只能都是差不多的,换言之,总公司、分公司、办事处三者其实并没有父子关系,在管理职能上他们的功能是相同的,比如都能添加、删除、查询子的机构节点。所以采用继承关系来设计其实是不合适的。 还有很多整体-部分关系的例子,比如学校、学院和系的关系,电脑组装商家可以出售整机也可以出售配件,文字处理软件中单个文字和整段甚至整篇文字的处理方式差不多,又如Java AWT、Swing中的容器组件和简单控件的关系。其实,这一类问题,最终解决的都是整体和部分被一致对待的问题,组合模式很好的解决了这个问题。 2. 组合模式简介 在Design Patterns一书中对组合模式的定义如下: 组合模式(Composite Pattern),将对象组合成树形结构以表示"部分-整体"的层次结构,使得用户对单个对象和组合对象的使用具有一致性。 组合模式是一种结构型设计模式,其"对象组合成树形结构"定义代表了对整体部分关系的抽象,将树形结构抽象为三个部分:根节点、树枝节点和叶子节点,其中,根节点、树枝节点可以添加子节点,而叶子节点不能添加子节点。因此,组合模式有两种方式:透明方式和安全方式。 在说明这两种方式之前,先看看组合模式的角色: 抽象构件(Component):对象组合的高度抽象构件,声明了对象操作的公共接口。透明方式和安全方式对节点管理方法的声明存在差异:透明方式会声明管理节点的接口,如添加、删除,但是安全方式不会声明管理接口,而是将其移交到树枝节点声明和实现,具体后边再说; 树枝构件(Composite):树形结构的分支节点,实现抽象构件,同时承担管理子节点职能,如实现添加、删除节点的方法; 树叶构件(Leaf):树形结构的叶子几点,没有子节点,所以不承担管理职能,它实现添加、删除等管理职能方法是没有意义的; 透明方式和安全方式主要区别在于:客户端是否需要区分树枝构件(Composite)和树叶构件(Leaf),透明方式则不区分,而安全方式则需要区分。 2.1. 透明方式 透明方式,不区分树叶构件和树枝构件,两者都实现构建构件的api,因此,树形结构管理职能的API(添加、修改、删除等)声明在抽象构件中,客户端使用时不需要区分树枝和树叶,因为他们具有相同的方法。类图如下: Figure 2. 透明方式类图 这种方式的好处在于,客户端不需要判断树叶构件和树枝构件,因此对客户端是统一的或者说透明的,方便管理;但是前边我们说过,树叶构件是没有子节点的,它不承担管理职能,实现管理职能的方法没有意思,因此我们只能进行方法空实现或者抛出异常,这一点不太友好。 2.2. 安全方式 相对于透明方式,安全方式显得比较保守。它只在抽象构件中声明公共方法,管理职能的方法放到树枝构件中进行声明和实现,这样树叶构件就不会存在管理职能的方法了。类图如下: Figure 3. 安全方式类图 这种方式的优点在于,树叶构件不需要对管理职能方法做无意义的空实现或抛异常,代码更严谨,但是其缺点也很明显:客户端需要明确知道调用的是树叶构件还是树枝构件,相对而言比较麻烦。 3. 应用示例 看一个例子。大学中分为多个学院,每个学院下又存在多个系(专业),我们看看如果运用组合模式来实现。 分析:学校、学院、系属于整体和部分关系,我们将其看做同一个整体并进行抽象,得到一个组织机构接口,让他们分别实现该接口。同时,学校、学院下都包含子节点,因此他们可以看做树枝构件,内部持有一个List来保存子节点数据,而系下不再有子节点,它属于树叶构件。 Figure 4. 学校学院系类图 类图中,Organization为抽象构件,声明了add、remove两个管理职能方法,而业务处理方法只有一个print方法,用来打印自身和其下的所有子节点。 从类图可以看出,我们使用的是组合模式的透明方式。 具体代码如下: 1、抽象构件: interface Organization { void add(Organization org); void remove(Organization org); void print(); } 2、树枝构件: ...

2020-12-07 · 2 min · 240 words · Hank

Java设计模式(11)-装饰模式

我同事大头,喜欢吃面。这天大头去一家面馆吃面,由于大头饭量大,面没吃完觉得不够,有点了两个卤蛋,一会儿再叫了一份青菜,最后时刻又加了一碗豆浆。面我们称为主食,必须要点,其他称为小吃,可以随意组合点单也可不点,假如面的价格是8元,卤蛋2元一个,青菜5元一份,豆浆3元一碗,现在要计算总共价格,如何设计?要求具备良好的扩展性和维护性。 1. 大头吃面第一版 既然面馆提供这么多好吃的东西,最容易的想到的就是在类中增加方法,初版设计类图如下: Figure 1. 大头吃面第一版设计类图 这种方式,将点主食、小吃分别加到管理类中,然后计算总价,实现代码如下: class NoodleRestaurant { private int totalPrice; void orderNoodles(int count) { System.out.println("点了" + count + "份面"); totalPrice += 8 * count; } void addEggs(int count) { System.out.println("点了" + count + "份鸡蛋"); totalPrice += 2 * count; } void addVegetables(int count) { System.out.println("点了" + count + "份青菜"); totalPrice += 5 * count; } void addSoySauce(int count) { System.out.println("点了" + count + "份豆浆"); totalPrice += 3 * count; } public int getTotalPrice() { return totalPrice; } } ...

2020-11-05 · 4 min · 744 words · Hank

Java设计模式(10)-桥接模式

有这样一个需求:手机都有发短信、打电话等功能,而且手机有各种各样的样式,比如翻盖手机、直板手机、滑盖手机、折叠手机等等,同时,手机还有各种各样的品牌,苹果、洛基亚、华为、小米、VIVO等等,我们应该如何来设计类之间的关系,并保持良好的可扩展性呢? 1. 糟糕的继承设计方式 通常,我们会使用简单的继承的方式来设计类,因为继承的方式我们最容易想到,继承设计的类图如下: Figure 1. 继承的方式设计类图 上边的类设计,从手机样式的维度出发,将手机划分为翻盖手机、直板手机,他们继承自"手机"父类,下边的各个品牌在分别按照手机样式分类,继承自样式父类。这样的设计,表面上能够解决问题,但是带来的非常大的缺点: 类非常多,每一种手机样式下,所有的品牌都需要建立这种样式的类,我们将这种现象称为类爆炸; 难以扩展和维护,增加一种手机样式,所有的品牌都需要在这种样式下添加新的类,如果增加一种品牌,那么原有的所有样式下都需要增加新品牌的类,这违反了单一职责原则 [1] 和开闭原则 [1] 前边 面向对象设计模式遵循的原则 一文时我们说过,继承最主要的缺点在于其破坏了类的封装性,父类的修改也会导致子类的变化,子类和父类的依赖关系非常紧密。因此,在考虑使用继承关系时,首先要明确类之间是否是明确的is a的关系,而不是like a,并遵循里式替换原则,另外,合成复用原则也指出:设计类时优先考虑使用合聚合和组用而不是继承。 桥接模式能够很好的解决像这样存在多个维度变化对象(比如这里的手机样式和品牌)的设计问题。 2. 桥接模式介绍 2.1. 桥接模式简介 桥接模式(Bridge Pattern)的定义如下:将抽象部分与它的实现部分分离,使他们都可以独立变化。 桥接模式包括如下的几种角色: 抽象化(Abstraction)角色:抽象类,并包含一个对实现化对象的引用 扩展抽象化(Refined Abstraction)角色:抽象化角色的子类,实现父类中的业务方法,并通过组合关系调用实现化角色中的业务方法 实现化(Implementor)角色:定义实现化角色的接口,供扩展抽象化角色调用 具体实现化(Concrete Implementor)角色:实现化角色接口的具体实现 几种觉色之间的关系如下图所示: Figure 2. 桥接模式的结构类图 抽象化角色、实现化角色将桥接模式结构分为两个部分:抽象部分由抽象化角色作为父类,实现部分由实现化角色为父类。然后抽象化角色依赖了实现化角色,中间的依赖线类似一座桥梁,很形象的说明了桥接模式的命名由来。从上边的类图可以看出,桥接模式是合成复用原则的体现,通过抽象化角色聚合实现化角色,将双方联系起来,这样既便于扩展,又能减少类的数量。 桥接模式的优点: 多个变化维度拆分为抽象和实现两个部分,都可以独立变化,程序易扩展 遵循开闭原则,添加新类时,仅需要修改客户端调用代码,而其他代码不需要变化,对扩展开放对修改关闭 遵循合成复用原则 注意,桥接模式定义中说的抽象与实现分离,并不是说将抽象类和其派生子类分离,这样做并无意义。这只是告诉我们在设计类时,首先需要明确可能变化的维度,将多个变化的维度都单独抽取出来,使得他们都可以独立的扩展。 比如前边的设计样式和手机品牌,这两个维度都会变化,可以将其抽象出来,手机的基本功能如打电话、发短信可以放到品牌中来实现,它就是作为实现部分,而手机样式作为抽象部分实现手机样式的不同功能。具体类图如下; Figure 3. 桥接模式解决手机设计问题 现在,我们编码来实现上述手机问题。 2.2. 桥接模式示例代码 1、首先,抽象手机品牌接口PhoneBrand,它作为实现化角色(Implementor): 实现化角色 // 手机品牌接口 interface PhoneBrand { // 获取品牌名称 String getName(); // 打电话 default String call() { return "用" + this.getName() + "手机打电话"; } } ...

2020-11-04 · 2 min · 323 words · Hank

Java设计模式(9)-适配器模式

模式来源于生活,适配器模式最能直观的体现这一点。适配器,就是将多个原本不能共同工作的产品经过一定的处理,使得他们能够彼此协同工作的一种装置。现实生活中,有很多适配器的例子,如常见的电源适配器,可以将220V的交流电压转换为手机、电脑等电器所需的低电压;又比如,苹果手机的type-C耳机插孔,不能使用3.5mm的耳机,怎么办呢?可以增加一个耳机转换器,它就是一个适配器,能够将3.5mm耳机成功用于Type-C接口的iPhone上;还有各种转换器,如HDMI转VGA、Type-C转USB等等,都是适配器。 1. 适配器模式简介 适配器模式(Adapter pattern)的定义如下:将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。 对于这个定义,需要明确几点: 首先,有一个已经存在的类A,但是不满足我们的需求,我们需要的是类B; 其次,A、B完成的功能相似,或者说,类B的功能类A其实都已经实现了,我们不想再实现一次; 第三,A、B不能共同完成工作,我们需要经过适配将A、B变得可以协同工作。 所以,什么时候可以考虑使用适配器模式? 当两个类所做的事情相同或相似,但是他们不能协同工作,此时可以通过适配器模式,但是有一个前提,就是两个类都不可能修改,如果他们频繁变动,那么首先要考虑的是重构代码来使他们统一。比如,遗留的系统、老的系统组件,他们中已经实现的功能,几乎不会修改,可以使用适配器模式。 适配器模式的主要优点如下: 客户端通过适配器可以透明地调用目标接口,更简单、直接; 复用了现存的类,程序员不需要修改原有代码而重用现有的适配者类; 将目标类和适配者类解耦,解决了目标类和适配者类接口不一致的问题。 适配器模式有三个主要角色: 目标(Target)接口:当前业务系统所期待的类,可以是抽象类或接口 被适配者(Adaptee):即现有系统或组件中已经存在的类,实现了我们所需的功能 适配器(Adapter):一个转换器,将被适配者转换为目标,让客户可以按目标接口透明的使用被适配者的功能 在java语言中,不能多继承,所以适配器模式中,我们一般会将目标申明为接口,然后通过继承或者依赖被适配者的方式来实现适配器。 适配器模式有三种形式:类适配器、对象适配器、接口适配器。 2. 类适配器模式 顾名思义,类适配器适配的是类。要想使用类的功能,java中我们只能用继承的方式,但是这违背了合成复用原则原则。尽量不要使用继承,而是使用合成的方式。 因此,类适配器最典型的缺点是,如果要替换适配器,将会变得很复杂,这种方式应该尽量不用。 这种形式的类结构如下图所示: Figure 1. 类适配器模式 Aapater类继承被适配者Adaptee,并实现目标接口Target,因此客户端可以透明的使用Target接口的api,而Adapter内部则使用Adaptee来实现Target定义的相关功能。 举个例子:用手机充电器给手机充电,假设手机需要使用5V的直流电才可以充电,但是家用电都是220V交流电,我们看看如何使用适配器模式解决这个问题。 1、首先,我们有220v的交流电,这就是被适配者Adaptee: 被适配者 class AlternatingCurrent { int outputVoltage() { (1) return 220; } } 1 被适配者拥有一个输出220V的交流电方法。 2、然后,我们新定义一个输出5V直流电的接口,这就是目标接口: 目标接口 interface DirectCurrent { int outputVoltage(); } 3、最后,定义适配器,进行适配: 适配器 class PhoneCharger extends AlternatingCurrent implements DirectCurrent { @Override public int outputVoltage() { int voltage = super.outputVoltage(); // 电压转换处理 return voltage / 44; (1) } } ...

2020-08-24 · 2 min · 299 words · Hank

Java设计模式(8)-建造者模式

建造者模式(Builder Pattern),旨在解决复杂对象的创建过程,将对象与其创建过程分离,通过相同的创建过程创建多种不同的对象。 生活中有很多复杂对象构建的例子。例如,汽车生产线的组装,都要经过车架、车身、变速箱、发动机、车轮等零部件的组装过程,如果将这些组装过程单独抽取出来,将组装结果——汽车独立出来,那么这个组装过程就可以重用,比如组装不同颜色、不同品牌的各种汽车。再比如,建房子,需要经过打地基、搭框架、砌墙、封顶等一系列复杂过程,如果对这个过程进行抽象,将房子和其建造过程分离,则更灵活,如通过这个建造过程可以建造高层建筑、低层建筑、普通民房等房屋。 1. 什么是建造者模式 建造者模式(Builder Pattern),又叫生成器模式,将复杂对象的创建过程和对象本身进行抽象和分离,使得创建过程可以重用并创建多个不同表现的对象。 优点: 各个具体的建造者相互独立,有利于系统的扩展; 客户端不必知道产品内部组成的细节,便于控制细节风险。 缺点: 产品的创建过程相同才能重用,有一定的局限性; 产品的内部创建过程变化。 2. 建造者模式结构 建造者模式有4个角色: 产品角色(Product):复杂对象,复杂的创建过程中包含多个创建步骤,由具体建造者来实现各个创建步骤的业务逻辑; 抽象建造者(Abstract Builder):抽象产品角色的多个创建步骤,可以是接口和抽象类,通常会定义一个方法来创建复杂产品,一般命名为build; 具体建造者(Concrete Builder):实现抽象建造者中定义的创建步骤逻辑,完成复杂产品的各个部件的具体创建方法; 指挥者(Director):调用抽象建造者对象中的部件构造与组装方法,然后调用build方法完成复杂对象的创建; 他们之间的关系如下图所示: Figure 1. 建造者模式类结构图 Director内部组合了抽象的Builder,用来构建产品;Client依赖Director创建产品,但是需要告诉它使用什么具体的Builder来创建,也就是会依赖具体的构建器. 抽象的Builder有两种形式:抽象类和接口。图中抽象的Builder为抽象类,其内部组合依赖了Product,并在build方法直接返回它;如果抽象Builder为接口,那么内部不会依赖Product,类结构上也会有一些变化,如下图所示: Figure 2. Builder为接口时的类结构图 最显著的区别是,具体构建器需要实现build方法,返回具产品信息。标准设计模式提供的是一种思路,在具体实现的时候有很多形式,但是其核心思想是不变的。 3. 示例 下面我们看一个简单的示例:我们建设生产一部普通汽车,都需要经过组装底盘、组装变速箱、发动机、车架等步骤,如果我们把汽车和它的生产过程分离开,使用建造者模式来实现,该怎么做呢? 1、首先,汽车就是我们需要建造的产品,其定义如下: 产品 class Car { // 发动机 private String motor; // 变速箱 private String gearbox; // 底盘 private String chassis; // 车架 private String frame; } 为了简单,这里省略了getter和setter。这里的产品已经是具体的产品了,实际上,这个产品一般是抽象的产品,下边可能会有多个具体的产品信息。 2、然后,抽象汽车生产过程,定义抽象的建造者: 抽象建造者 abstract class CarBuilder { protected Car car = new Car(); // 构建发动机 abstract void buildMotor(); // 构建变速箱 abstract void buildGearbox(); // 构建底盘 abstract void buildChassis(); // 构建车架 abstract void buildFrame(); public Car build() { return car; } } ...

2020-08-23 · 2 min · 423 words · Hank

Spring扩展原理

Spring的扩展点很多,本文讨论Spring的 BeanPostProcessor、BeanFactoryPostProcessor、ApplicationListener 和 SmartInitializingSingleton 这四个扩展的基础类或接口。 1. Bean后置处理器 bean后置处理器用于bean创建对象初始化前后进行拦截工作,因此它是spring容器扩展必不可少的组件。 1.1. BeanPostProcessor 允许自定义修改新bean实例的工厂钩子,例如,检查标记接口或用代理包装bean。 通常,通过标记接口等填充bean的后处理器将在初始化前实现后处理,而使用代理包装bean的后处理器通常在初始化后实现后处理。 注册表 ApplicationContext 可以在其bean定义中自动检测 BeanPostProcessor bean,并将这些后处理器应用于随后创建的任何bean。一个普通的bean factory允许对后处理器进行编程注册,将它们应用于通过bean工厂创建的所有bean。 顺序 在 ApplicationContext 中自动检测到的 BeanPostProcessor bean将根据 org.springframework.core.PriorityOrdered 和 org.springframework.core.ordered 语义进行排序。相反,以编程方式向 BeanFactory 注册的 BeanPostProcessor bean将按注册顺序应用;通过实现 PriorityOrdered 或 Ordered 接口表示的任何排序语义对于以编程方式注册的后处理器都将被忽略。此外,BeanPostProcessor bean不考虑 @Order 注解。 public interface BeanPostProcessor { /** * 在传入的bean属性设置之后、初始化之前调用。在任何bean初始化回调(如initializengbean的 * afterPropertiesSet或自定义init方法)之前,将此BeanPostProcessor应用于给定的新bean实例。 * bean已经填充了属性值。返回的bean实例可以是原始实例的包装器。 * 默认实现按原样返回给定的bean。 */ @Nullable default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { return bean; } /** * 在传入的bean初始化完成之后调用 * 在任何bean初始化回调(如initializengbean的afterPropertiesSet或自定义init方法)之后, * 将此BeanPostProcessor应用于给定的新bean实例。bean已经填充了属性值。 * 返回的bean实例可以是原始实例的包装器。 * 对于FactoryBean,将为FactoryBean实例和由FactoryBean创建的对象(从Spring 2.0开始)调用此回调。 * 后处理器可以通过FactoryBean检查的相应bean实例来决定是应用于FactoryBean还是创建的对象, * 或者两者都应用。此回调也将在InstantiationAwareBeanPostProcessor.postProcessBeforeInstantiation * 方法触发短路后调用,与所有其他BeanPostProcessor回调不同。 默认实现按原样返回给定的bean。 */ @Nullable default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { return bean; } } ...

2020-08-12 · 4 min · 804 words · Hank

Java设计模式(7)-原型模式

某些情况下,我们需要重复的创建多个对象,但是这些对象仅仅只有某几个属性不一致,大部分的信息都是相同的,如果使用传统的构造函数来创建对象,我们需要不断的实例化对象,并且反复调用属性的set方法来设置值,而这些值大多都是相同的。有没有一种模式,能够快速而高效的创建这些差别不大的对象呢?这就需要使用到原型模式。 1. 什么是原型模式 原型模式(Prototype Pattern),它的基本思想是:创建一个对象实例作为原型,然后不断的复制(或者叫克隆)这个原型对象来创建该对象的新实例,而不是反复的使用构造函数来实例化对象。 原型模式创建对象,调用者无需关心对象创建细节,只需要调用复制方法,即可得到与原型对象属性相同的新实例,方便而且高效。 举一个最常见的例子,猴王孙悟空本领大,拔下猴毛一吹,就可以得到很多个与自己一模一样的猴子猴孙。这里就可以使用到原型模式,来复制孙悟空。另外,再举个生活中的例子,刚毕业找工作的同学们,都需要填写病打印纸质的简历,但是这些简历信息只有你想要投递的公司信息不一样,其他的信息如个人基本信息、教育经历、工作经验等都是相同的,我们就可以使用原型模式复制简历,然后修改公司信息即可,而无需重复创建多个简历,在一遍遍填写。 2. 原型模式结构 原型模式的结构如下图所示: Figure 1. 原型模式结构图 ` 结构分为三个部分: Prototype: 原型抽象接口,提供复制(clone)方法,以便实现类实现该方法来复制自己 ConcretePrototype: 具体原型对象,实现 Prototype 接口的复制方法来复制自己,从而创建新实例。 Client: 负责调用原型对象的复制方法获得原型对象新实例,并按需修改新实例 3. Java中的Cloneable接口 Java语言提供了一个 Cloneable 接口,这是一个标记型接口,用来表示实现了该接口的对象可以进行克隆,其定义如下: public interface Cloneable { } 真正的实现克隆的逻辑其实是在 Object 类上: public class Object { protected native Object clone() throws CloneNotSupportedException; // …… } 因此,Java实现原型模式比较方便,只需要实现 Cloneable 接口即可,克隆时调用对象自己的 clone 方法即可。但是,clone 方法是一个native实现,其实它仅仅实现了浅拷贝,稍后再细说。 4. 基础示例 接下来,我们编码实现前边所举的猴王孙悟空分身的例子,首先看看常规方式是如何实现的。 4.1. 常规的实现方式 先来编写一个 MonkeyKing 类,它有名称、居住地、技能强度、寿命等属性: class MonkeyKing { // 高强度 static int HIGH_SKILL_STRENGTH = 10; // 普通强度 static int NORMAL_SKILL_STRENGTH = 5; // 姓名 private String name; // 地址 private String address; // 能力强度 private int skillStrength; // 寿命 private int lifetime; // 省略getter、setter } ...

2020-07-15 · 4 min · 653 words · Hank

Java设计模式(6)-抽象工厂模式

如果工厂模式中需要生产多种类型的产品,那么工厂方法模式就适合了,需要用到抽象工厂模式。 1. 简介 抽象工厂模式,定义抽象工厂,并将产品生产延迟到具体工厂实现中,而它生产的产品都是同种类型的。比如,电视机工厂生产的都是电视机,电视机是一个抽象产品,而它的具体实现有黑白电视、彩色电视、液晶电视等等,但是都属于同一种产品类型-电视机。 如果现在电视机工厂不仅生产电视,还能生产空调等其他电器了,那么,我们称电视机、空调等不同类型的产品为产品簇,代表不同类型的多种产品。 Figure 1. 产品簇示意图 现在,工厂方法模式不适用了,我们需要使用抽象工厂模式。 抽象工厂模式(Abstract Factory Pattern)的定义如下:提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类,访问类无须指定具体的类就可以得到同组的不同类型的产品。 抽象工厂模式升级了工厂方法模式,将原本仅能创建同种类型的产品升级为可以创建多种类型的产品. 优点 可以使用工厂模式生产多个产品簇,新增同类型产品时只需要新增一个具体工厂实现,不需要修改客户端代码,遵循开闭原则。 缺点 当产品簇中需要新增产品时,会修改原产品簇的所有工厂类,不符合开闭原则。 因此,抽象工厂模式在使用时应该酌情考虑其倾斜性,新增同类产品时,遵循开闭原则;但是,产品簇添加新的产品,所有的工厂类都需要添加生产新产品的方法。 2. 适用场景 抽象工厂模式通常适用于以下场景: 当需要创建的对象是一系列相互关联或相互依赖的产品族时,如电器工厂中的电视机、电风扇、洗衣机、空调等 系统中有多个产品族,但每次只使用其中的某一族产品,比如有人只喜欢穿某一个品牌的衣服和鞋 系统中提供了产品的类库,且所有产品的接口相同,客户端不依赖产品实例的创建细节和内部结构 3. 结构 抽象工厂模式的结构与工厂方法模式一样,同样具有抽象工厂、抽象产品、具体工厂、具体产品四大角色: 抽象工厂: 定义产品创建的接口,具有多个产品创建方法,这些方法负责创建不同的产品; 具体工厂: 实现抽象工厂的产品创建方法,用于具体实现如何创建这些产品簇中不同的产品; 抽象产品: 定义产品的规范,如产品规格、属性、功能等;就有多个抽象产品,组成产品簇; 具体产品: 抽象产品的具体实现,具体产品与具体工厂是多对一的关系,即一个具体工厂可以生产多个具体产品。 Figure 2. 抽象工厂模式结构 可以看到,AbstractFactory 是顶层抽象工厂接口,定义了创建不同产品簇的方法。不同的产品簇抽象出不同的产品接口,如图中的 Product1、Product2。 如果要添加一个产品的具体实现,那么客户端和工厂不需要改动,只需要新增一个具体工厂即可,那么结构变成了这样: Figure 3. 新增具体产品时的类结构变化 图中紫色部分是需要新加的部分,原工厂和客户端不需要做改动,遵循开闭原则。 但是,如果现在要增加新的产品线,即是说产品簇中要新加一个新产品,那么原工厂代码需要添加生产这个新产品的方法,而且客户能也需要为调用这个新方法而做出修改,此时的类结构如下图: Figure 4. 产品簇中添加新产品时的类结构变化 图中紫色部分为新加的产品,而原来的工厂添加新的方法(红色部分)来生产这个新的 Product3 产品,客户端也需要修改代码来调用工厂的新方法。 4. 与工厂方法模式的比较 抽象工厂模式与工厂方法模式在结构上其实是相同的,但是他们创建的产品类型不同: 工厂方法模式仅能创建某一类产品,而抽象工厂模式可以创建多种类型的产品; 工厂方法模式如果能够支持创建多种类型的产品,那么它会演变为抽象工厂模式;同理,如果抽象工厂模式仅创建同一类型的产品,它就变成了工厂方法模式 抽象工厂模式具有工厂方法模式的优点和缺点 5. 示例 接下来看一个实例。现在有一个电器工厂,可以生产电视机、空调,假设电视机有黑白、彩色两种,空调有挂式和柜式空调两种。工厂既能生产电视机,也能生产空调。我们用抽象工厂来设计,创建两个具体的工厂,工厂A来生产黑白电视机和挂式空调,而工厂B来生产彩电和柜式空调。类的结构如下: Figure 5. 电器工厂设计类图...

2020-06-09 · 3 min · 612 words · Hank

Java设计模式(5)-工厂方法模式

工厂方法模式,是工厂模式的一种,它对简单工厂模式进行进一步抽象化。它将工厂和产品进行抽象,抽象出来的工厂负责定义创建产品的规范,而抽象的产品负责定义产品规范,然后通过具体的工厂实现类来创建具体的产品。 1. 简介 工厂方法模式(Factory Method Pattern),它的定义如下:定义一个创建产品对象的工厂接口,它负责定义创建产品的方法,而具体的产品创建工作推迟到具体工厂类当中来完成,具体工厂类来角色如何创建产品。 优点 客户端(产品使用者)和产品的创建过程相分离,客户端无需关心产品的创建过程 系统增加新的产品时,只需要添加具体产品类和创建它的具体工厂实现即可,无需修改原有代码,满足开闭原则 缺点 每个具体的产品都对应了具体的工厂,系统复杂度增加。 2. 结构 简单工厂模式在新加产品的时候会违背开闭原则,而工厂方法模式对其进行了改进,增加了抽象工厂和抽象产品。 工厂方法模式由抽象工厂、具体工厂、抽象产品和具体产品构成,其类图如下: 抽象工厂(AbstractFactory):抽象了创建产品的方法,例如 createProduct(),但是不负责具体实现产品创建过程;这里的抽象工厂一般是接口,也可以是抽象类 具体工厂(ConcreteFactory):抽象工厂的具体实现类,实现了抽象工厂定义的产品创建方法 抽象产品(AbstractProduct):就是图中的Product,抽象产品对工厂生产的产品进行了抽象,定义了产品的规范,如产品的功能、参数、属性等;同样的,抽象产品也可以是抽象类或接口 具体产品(ConcreteProduct):抽象产品的具体实现,由具体工厂进行创建,通常,具体产品与具体工厂成一一对应关系 3. 示例 继续使用 上一篇 生产牛奶的例子。商品售卖各种牛奶,如纯牛奶、酸奶、高钙奶等等,前边使用的是简单工厂模式。现在,我们将其改为工厂方法模式。 类的设计如下: 这里,抽象的产品是 Milk,抽象的工厂是 MilkFactory,具体的工厂是 PureMilkFactory 等,他们负责生产某一种具体的产品,如 PureMilkFactory 负责生产 PureMilk。商店 Store 不用关心具体的产品是什么,它只需要依赖抽象的工厂和产品即可。 部分关键代码如下: 抽象工厂 interface MilkFactory { Milk createMilk(); } 抽象产品 interface Milk { String getName(); } 具体产品 class PureMilk implements Milk { @Override public String getName() { return "伊利纯牛奶"; } } ...

2020-06-06 · 1 min · 181 words · Hank

Java设计模式(4)-简单工厂模式

工厂模式,一般分为三种,包括:简单工厂模式(也叫静态工厂模式)、工厂方法模式、抽象工厂模式。在GOF提及的23中设计模式中,没有简单工厂模式,但对于简单的软件开发中,简单工厂模式使用的还是很普遍的。 1. 简介 传统的设计,客户端创建各种产品,来实现功能。这样,客户端与各种产品进行了耦合,违背了OCP原则。 Figure 1. 传统设计 我们能不能把产品的创建过程抽取出来,形成一个产品创建工厂呢?这样,就可以使创建与使用相分离,客户端就直接依赖工厂,而不是与直接的产品进行耦合了。这就是工厂模式的思路。 简单工厂模式是工厂模式的一种,它的工厂负责创建具体的产品,客户端不需要关注产品的创建过程,而是直接依赖工厂即可。简单工厂模式的类图如下: Figure 2. 简单工厂模式 简单工厂又称为静态工厂,因为它创建产品的方法一般都是静态方法,客户端不需要实例化工厂就可以直接调用它的产品创建方法。 优点 产品的创建过程抽取出来,客户端与具体产品创建过程解耦; 客户端仅负责自身业务,不关注产品创建过程,职责更单一 缺点 工厂和产品耦合,违背了OCP原则,添加新产品需要修改工厂类代码 工厂类的方法是静态的,并不利于扩展 因此,简单工厂模式适用于创建的产品比较固定,种类少、变动不频繁的场景,往往在小型系统适用。 2. 示例 举个例子,现在有一个牛奶店售卖各种牛奶,假设出售的过程包括生产、收款、拿货几个步骤,而且此店售卖的牛奶种类不是很多。如果没有工厂,店铺不但需要处理售卖的逻辑,而且还需要创建各种牛奶的实例。这会导致客户端职责过多,并不利于其管理和维护。 现在的设计这样的: class Store { public Milk sale(String name) { System.out.println("顾客购买牛奶"); System.out.println(" > 生产牛奶"); Milk milk = createMilk(name); System.out.println(" > 收款"); System.out.println(" > 拿货"); return milk; } public Milk createMilk(String name) { if ("pure".equals(name)) { return new PureMilk(); } else if ("yogurt".equals(name)) { return new Yogurt(); } else if ("highCalcium".equals(name)) { return new HighCalciumMilk(); } else { throw new RuntimeException("无法生产牛奶:" + name); } } } ...

2020-06-05 · 1 min · 170 words · Hank

Java设计模式(3)-单例模式

单例模式(Singleton Pattern),Java中最简单的设计模式之一,它定义了如何在整个系统范围内仅创建只有单个实例的类。单例模式是一种创建型模式,系统中的类只有一个实例,该类负责自己创建自身的唯一单个实例,并提供一个静态方法来获取自身实例。 1. 单例模式解决的问题 单例模式(Singleton Pattern)的目的是要保证系统中一个类仅有一个实例,并且该类给外部提供一个访问它实例的方法。单例模式旨在解决系统中的类被频繁创建和销毁而占用较多资源的问题。 单例模式不允许外部创建其实例(构造器私有化),而是自身提供给外部一个静态方法来获取其单实例对象。 优点 单例类减小了资源占用,一个类仅有一个实例,内存开销小。 缺点 单例类没有接口,不能继承,与单一职责原则冲突,需要自己关注自身实例创建逻辑。 2. Java中单例模式的8种写法分析 在Java中,单例模式有8八种写法,但是可用的只有几种,我们来分析一下他们的写法和优缺点。 2.1. 饿汉式-静态常量 这种方式利用了类初始化机制,在类初始化时就创建单例实例。 public class AvailableEagerSingleton1 { private static final AvailableEagerSingleton1 INSTANCE = new AvailableEagerSingleton1(); (1) private AvailableEagerSingleton1() { (2) } public static AvailableEagerSingleton1 getInstance() { (3) return INSTANCE; } } 1 静态常量,类初始化时就创建实例 2 构造器私有化,不允许外部直接创建实例 3 提供静态方法给外部调用,以获取其实例 这种方式的优点就是实现起来简单,而且没有线程安全问题,在初始化静态属性时直接创建实例;缺点是,没有实现懒加载,如果类不会被使用,则会存在资源浪费。 如果确定类会被使用,这种方式也是推荐使用的。 2.2. 饿汉式-静态代码块 另一种懒汉式的变体是,使用静态代码块来代替静态属性创建实例,两者其实没有什么根本区别: public class AvailableEagerSingleton2 { private static AvailableEagerSingleton2 INSTANCE; { INSTANCE = new AvailableEagerSingleton2(); (1) } private AvailableEagerSingleton2() { } public static AvailableEagerSingleton2 getInstance() { return INSTANCE; } } ...

2020-05-29 · 2 min · 408 words · Hank

Java设计模式(2)-软件设计遵循的七大原则

设计模式为面向对象软件设计的前进道路照亮了明灯,而设计模式遵循面向对象软件设计的七大原则,这些原则也是设计模式的基础。 1. 单一职责原则 单一职责原则(Single Responsibility),它的定义是:对类来说的,一个类应该只负责一项职责。即是说,一个类只负责一种职责,让其职责单一化,如果有其他的职责则应该设计另外的类来进行协助。比如:订单处理类只负责处理订单,而不该去处理支付的逻辑。单一职责原则也可以引申到方法、类、模块甚至服务和子系统中。 类的职责单一,并不是说一个类只有一个方法,而是说一个类只承担实现一中业务职责,如:OrderService只负责处理订单相关的逻辑,而不会涉及支付相关的逻辑。 下面来看看具体的例子。假设我们设计一个Animal类,来管理动物的飞行、陆行、游水等动作。 1.1. 基础示例 这个版本的设计如下图所示: Figure 1. 初版设计 初版的代码如下: Animal类 // 一个类负责动物的飞行方法 abstract class Animal { public void fly() { System.out.println(getName() + "能够飞行"); } public void runOnLand() { System.out.println(getName() + "能在陆地上跑"); } public void swimInWater() { System.out.println(getName() + "能在水中游"); } protected abstract String getName(); } 这里设计了一个抽象类,里边有各种方法来打印某种动物(具备名称)的这些行为。 然后,各种动物都可以继承 Animal : Animal下的具体动物 // 小鸟 class Bird extends Animal { @Override public String getName() { return "小鸟"; } } // 大雁 class WildGoose extends Animal { @Override public String getName() { return "大雁"; } } // 狗 class Dog extends Animal { @Override public String getName() { return "狗"; } } // ……其他动物 ...

2020-05-27 · 6 min · 1094 words · Hank

Java设计模式(1)-什么是设计模式?

什么是设计模式?顾名思义,设计模式是一种软件设计所使用的方法,适用于面向对象软件设计。设计模式是一种方法,或者说是一种方法论,它是软件前辈们经历了无数的软件工程设计中的痛点和教训之后,总结出来的经验的结晶。设计模式是一种软件设计方法,不是代码,也不是规范。 1. 设计模式的提出 如果设计出优秀的软件一直是软件从业者探索的目标。优秀的软件,必须具备可复用、可扩展、可维护、高性能等特点,既要保证系统的高内聚、低耦合的特点,又要保证系统随时应对各种变化的灵活性。然很,这往往依赖于软件设计者的个人经验积累,而新手往往对此无从下手。 设计模式的提出,为软件设计指明了方向。 设计模式概念的提出,要追溯到1994年。在1994年,由Erich Gamma、Richard Helm、Ralph Johnson和John Vlissides 四为软件大师合著了一本名为 Design Patterns - Elements of Reusable Object-Oriented Software(中文译名:设计模式 - 可复用面向对象软件的基础)的书,该书首次提到了软件开发中设计模式的概念。 Figure 1. 设计模式-可复用面向对象软件设计的基础 Ps: 公众号后台回复关键字"设计模式"下载此书 该书总结了面向对象设计中最有价值的经验,并总结出了23中最常见和最有价值的设计模式,为软件设计之路开启了一盏明灯,此书的四名作者又被称为GOF(Gang Of Four,四人帮)。这23种设计模式主要基于面向对象设计的两种基本原则: 对接口编程而不是对实现编程 优先使用对象组合而不是继承 2. 设计模式的概念 设计模式来源于城市和建筑模式: 每一个模式描述了一个在我们周围不断重复发生的问题,以及该问题解决方案的核心。这样,你就能一次又一次的使用该方案而不必做重复劳动。 — Christopher Alexander 这个思想同样适用于面向对象的设计模式。一般而言,设计模式包含四大要素: 名称(pattern name): 描述模式解决的问题、方案和效果 问题(problem): 描述何时使用模式、设计的问题以及问题存在的背景 解决方案(solution): 描述了设计的组成部分、他们之间的相互关系和协作的方式 效果(consequence): 描述了使用模式应该达到的效果和应该权衡的问题 3. 设计模式的类型 书中对设计模式进行了详细阐述,并将设计模式分为三大类型: 3.1. 创建型模式(Creational) 对象怎么创建?Java中,最简单的是使用关键 new 来创建一个对象实例,然而,我们需要思考:我们需要这么多对象实例吗?这种方式是我们需要的吗?这种方式是最好的吗?创建型模式抽象了对象实例化过程,它描述了如何去创建对象,而无需关注实例化细节。创建型模式包括: 简单工厂模式(Factory Pattern)、 抽象工厂模式(Abstract Factory Pattern)、 工厂方法模式(Factory Method Pattern) 单例模式(Singleton Pattern) 原型模式(Prototype Pattern) 建造者模式(Builder Pattern) 3.2. 结构型模式(Stuctural) 结构型模式,描述了如何组合类和对象以获得最大的结构,通常这些模式使用继承的方式来进行组合,包括: ...

2020-05-26 · 1 min · 148 words · Hank

给Spring中注册Bean的几种方式

1. 使用@Bean定义单个Bean 基于 @Bean 注解导入单个Bean。这种方式跟xml中 <bean> 标签等价,可以添加外部自定义Bean,但是需要自己创建Bean实例,而且只能导入单个Bean。注解定义如下: @Bean注解定义 @Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Bean { // 自定义bean的名称 @AliasFor("name") String[] value() default {}; // 同value属性,自定义bean的名称 @AliasFor("value") String[] name() default {}; // 设置当前注入的bean是否可用于自动注入,默认是true。 // 如果设置为false,那么即使该bean注入到Spring了,在自动注入时也会找不到bean而抛出NoSuchBeanDefinitionException异常。 // 5.1版本新增 boolean autowireCandidate() default true; (1) // 自定义Bean的初始化方法名称,Spring 在Bean初始化时会调用该方法 String initMethod() default ""; // 自定义Bean的销毁方法名称,Spring在容器关闭时会调用该方法进行自定义Bean销毁工作 String destroyMethod() default AbstractBeanDefinition.INFER_METHOD; } 1 功能与 @Primary 注解相关,都用于自动注入时Bean的选择,而 @Primary 用于指定注入时存在多个Bean实例时优先用哪个,而 autowireCandidate 属性则是设置Bean是否参与自动注入,true 则参与,false 则不参与(即使有Bean实例也可能在自动注入时抛出 NoSuchBeanDefinitionException 异常) ...

2020-05-07 · 4 min · 825 words · Hank

分布式事务的几种解决方案

分布式系统,与传统单体架构系统相比,其结构复杂的多,系统间靠网络传输数据,系统存在诸多不确定因素,如硬件故障、网络波动等,都会影响整个系统的稳定性。而分布式事务更是分布式系统的一大难题,本篇将讨论业界分布式事务的几种常见解决方案。 1. 数据库事务 百度百科对事务的定义: 数据库事务( transaction)是访问并可能操作各种数据项的一个数据库操作序列,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。 解读一下这个定义: 事务产生于一个数据库操作序列:一组读取或者更新数据库等操作时,事务是一种机制,来保证了最终数据的一致性, 事务是一个不可分割的单元:一组操作不能拆分,是一个整体 事务执行:要么全部执行,要么全部不执行,不能部分执行 通常,数据库的事务是通过数据库日志来保证的,所有数据库的操作都记录日志,如果数据库发生宕机,那么通过读取日志可以知道操作时需要回滚还是提交。 众所周知,数据库事务有ACID四大特性,满足ACID特性的事务,一般称之为刚性事务,常见的单体应用(单个数据源)内部都是采用刚性事务。 最常见的事务的例子就是银行转账: 2. 分布式事务 分布式事务,指的是分布式系统间完成的事务机制,百度百科定义如下: 数据库事务( transaction)是访问并可能操作各种数据项的一个数据库操作序列,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。 分布式同数据库事务一样,都用以保证数据的一致性,只是事务的参与者由原来的单数据源的一组操作序列变成了分布式系统的多个子系统。 举个例子: 用户下单流程,如下图所示: 首先,订单系统订单支付完成,然后调用库存系统扣减库存,最后再调用积分系统给账户添加积分,第1、2、3步必须全部执行成功,那么下单操作才算成功,否则,有一步出现错误,那么下单操作就是失败的。与传统事务相比,分布式事务范围在数据库事务之上进行了放大,各个系统除了有本系统事务外,还需要符合整个分布式系统的事务机制,才能保证最终的数据一致性。 但是,在分布式系统中,由于其固有的复杂性,很难保证事务的ACID特性,这种刚性事务在分布式系统中不适用,这就要提到两个概念:CAP定理和BASE理论。 2.1. CAP理论 2000年,名为Eric Brewer的教授在PODC研讨会上提出了CAP:一致性、可用性和分区容错性三者无法在分布式系统中被同时满足,并且最多只能满足其中两个!他还对CAP进行明确的定义: C(Consistency,一致性):所有的节点上的数据时刻保持同步 A(Avallable,可用性):每个请求都能接受到一个响应,无论响应成功或失败 P(Partition tolerance,分区容错性):系统应该能持续提供服务,即使系统内部有消息丢失(分区) CAP理论的提出,揭露了分布式系统的本质。 怎么理解CAP? 一致性,其实就是分布式系统各节点间数据达成一致;可用性,分布式系统始终保持可用,即使出现了数据不一致;分区容错性,分布式系统必须有一定的容错能力,当系统数据丢失或者某些节点不可用(分区)系统还能继续提供服务,系统容错是不可缺少的。 既然CAP三者不能同时满足,而分布式系统中,分区容错性是不可或缺的,因此,只能在CP和AP中间选择。 由于在CAP理论要求各节点数据时刻保持同步,但是这样的强一致性只能在CP中保证,而选择满足AP时无法保证,这就引出BASE理论。 2.2. BASE理论 eBay的架构师Dan Pritchett源于对大规模分布式系统的实践总结,在ACM上发表文章提出BASE理论,BASE理论是对CAP理论的延伸,核心思想是即使无法做到强一致性,但应用可以采用适合的方式达到最终一致性(Eventual Consitency)。 BA: Basically Available(基本可用) S: Soft state(软状态) E: Eventually consistent(最终一致性) 显然,BASE理论满足CAP中的AP,从而舍弃了强一致性C。 满足BASE理论,放弃强一致性,数据最终达到一致的事务我们称之为柔性事务(Flexible Transactions)。 如果分布式系统满足AP,那么虽然数据不可能时刻保持一致,但是可以达到最终一致;如果满足产CP,那么分布式系统舍弃了高可用性,却可以保持数据强一致。分布式系统中,常见的两个服务注册中心Eureka、Consul,前者满足的是AP,而后者满足的是CP。 3. 分布式事务的解决方案 分布式事务解决的核心问题就是分布式系统中各个节点的数据一致性问题,选择强一致还是最终一致,需要根据具体业务需要合理做出选择。目前,常见的几种分布式事务解决方案有2PC(两阶段提交)、3PC(三阶段提交)、TCC(补偿性事务)、本地消息表等。 3.1. 2PC 两阶段提交(2PC),其实是XA协议的标准实现,XA协议由Tuxedo首先提出的,并交给X/Open组织,作为资源管理器(数据库)与事务管理器的接口标准。目前,Oracle、Informix、DB2和Sybase等各大数据库厂家都提供对XA的支持。 2PC事务包括几个角色:事务协调者(coordinator)、事务参与者(participant )、资源管理器(resource manager,RM): 事务协调者:负责收集事务参与者的信息,并决定事务提交/回滚操作 事务参与者:按照事务协调者要求参与事务,可进行事务预提交、提交或者回滚操作 资源管理器:事务参与者管理的数据库 2PC将事务的管理分为两个阶段: 1、第一阶段(prepare):事务协调者要求每个事务参与者预提交(precommit)事务,事务并不会真正提交,并反馈(vote)提交结果 2、第二阶段(commit/rollback):事务协调者收集参与者信息,如果每个参与者都反馈可提交事务,那么协调者下发commit指令给参与者要求提交事务,否则,如果有参与者反馈回滚事务,则协调者下发abort指令要求参与者都回滚事务 通俗的讲:协调者首先发起事务,说参与者们检查一下看看各自操作是否能够正常执行啊,不管可以与否都给我一个反馈。然后,参与者检查完成,都反馈说,老大,我们都能正常执行,那么协调者就说好吧,你们都提交事务吧;如果某一个参与者说,完了,我操作执行失败了,不能提交事务,那么协调者就告诉各参与者:某个哥们儿不能提交事务,数据一致性没法保证了,你们都回滚吧! ...

2020-04-15 · 1 min · 170 words · Hank

Java编程思想(第4版)中文高清版PDF下载

Java编程思想一书赢得了全球程序员的广泛赞誉,即使是最晦涩的概念,在Bruce Eckel的文字亲和力和小而直接的编程示例面前也会化解于无形。从Java的基础语法到最高级特性(深入的面向对象概念、多线程、自动项目构建、单元测试和调试等),本书都能逐步指导你轻松掌握。 1. 站长评价 Java程序员必备参考书、工具书,长期霸占Java编程书籍排行榜榜首的神级著作!Java编程必读书籍,需要反复研读、反复体会编程思想! 丰富指数:☆☆☆☆ 难度指数:☆☆☆ 推荐指数:☆☆☆☆☆ 迫不及待了?点此立即下载! 2. 内容简介 本书赢得了全球程序员的广泛赞誉,即使是最晦涩的概念,在Bruce Eckel的文字亲和力和小而直接的编程示例面前也会化解于无形。从Java的基础语法到最高级特性(深入的面向对象概念、多线程、自动项目构建、单元测试和调试等),本书都能逐步指导你轻松掌握。 从本书获得的各项大奖以及来自世界各地的读者评论中,不难看出这是一本经典之作。本书的作者拥有多年教学经验,对C、C++以及Java语言都有独到、深入的见解,以通俗易懂及小而直接的示例解释了一个个晦涩抽象的概念。本书共22章,包括操作符、控制执行流程、访问权限控制、复用类、多态、接口、通过异常处理错误、字符串、泛型、数组、容器深入研究、Java I/O系统、枚举类型、并发以及图形化用户界面等内容。这些丰富的内容,包含了Java语言基础语法以及高级特性,适合各个层次的Java程序员阅读,同时也是高等院校讲授面向对象程序设计语言以及Java语言的绝佳教材和参考书。 第4版特点: 适合初学者与专业人员的经典的面向对象叙述方式,为更新的Java SE5/6增加了新的示例和章节。 测验框架显示程序输出。 设计模式贯穿于众多示例中:适配器、桥接器、职责链、命令、装饰器、外观、工厂方法、享元、点名、数据传输对象、空对象、代理、单例、状态、策略、模板方法以及访问者。 为数据传输引入了XML,为用户界面引入了SWT和Flash。 重新撰写了有关并发的章节,有助于读者掌握线程的相关知识。 专门为第4版以及Java SE5/6重写了700多个编译文件中的500多个程序。 支持网站包含了所有源代码、带注解的解决方案指南、网络日志以及多媒体学习资料。 覆盖了所有基础知识,同时论述了高级特性。 详细地阐述了面向对象原理。 在线可获得Java讲座CD,其中包含Bruce Eckel的全部多媒体讲座。 3. 作者简介 Bruce Eckel是MindView公司(www.MindView.net)的总裁,该公司向客户提供软件咨询和培训。他是C标准委员会拥有表决权的成员之一,拥有应用物理学学士和计算机工程硕士学位。除本书外,他还是《C编程思想》的作者,并与人合著了《C++编程思想 第2卷》(这两本书的英文影印版及中文版均已由机械工业出版社引进出版)及其他著作。他已经发表了150多篇论文,还经常参加世界各地的研讨会并进行演讲。 4. 目录 读者评论 前言 简介 第1章 对象导论 1.1 抽象过程 1.2 每个对象都有一个接口 1.3 每个对象都提供服务 1.4 被隐藏的具体实现 1.5 复用具体实现 1.6 继承 1.6.1 “是一个”(is-a)与“像是一个”(is-like-a)关系 1.7 伴随多态的可互换对象 1.8 单根继承结构 1.9 容器 1.9.1 参数化类型(范型) 1.10 对象的创建和生命期 1.11 异常处理:处理错误 1.12 并发编程 1.13 Java与Internet 1.13.1 Web是什么 1.13.2 客户端编程 1.13.3 服务器端编程 1.22 总结 第2章 一切都是对象 2.1 用引用操纵对象 2.2 必须由你创建所有对象 2.2.1 存储到什么地方 2.2.2 特例:基本类型 2.2.3 Java中的数组 2.3 永远不需要销毁对象 2.3.1 作用域 2.3.2 对象的作用域 2.4 创建新的数据类型:类 2.4.1 域和方法 2.4.2 基本成员默认值 2.5 方法、参数和返回值 2.5.1 参数列表 2.6 构建一个Java程序 2.6.1 名字可见性 2.6.2 运用其他构件 2.6.3 static 关键字 2.7 你的第一个Java程序 编译和运行 2.8 注释和嵌入式文档 2.8.1 注释文档 2.8.2 语法 2.8.3 嵌入式HTML 2.8.4 一些标签示例 2.8.5 文档示例 2.9 编码风格 2.10 总结 2.11 练习 第3章 操作符 3.1 更简单的打印语句 3.2 使用Java操作符 3.3 优先级 3.4 赋值 3.4.1 方法调用中的别名问题 3.5 算术操作符 3.5.1 一元加、减操作符 3.6 自动递增和递减 3.7 关系操作符 3.7.1 测试对象的等价性 3.8 逻辑操作符 3.8.1 短路 3.9 直接常量 3.9.1 指数记数法 3.10 按位操作符 3.11 移位操作符 3.12 三元操作符 if-else 3.13 字符串操作符 + 和 += 3.14 使用操作符时常犯的错误 3.15 类型转换操作符 3.15.1 截尾和舍入 3.15.2提升 3.16 Java没有“sizeof” 3.17 操作符小结 3.18 总结 第4章 控制执行流程 4.1 true和false 4.2 if-else 4.3 迭代 4.3.1 do-while 4.3.2 for 4.3.3 逗号操作符 4.4 Foreach语法 4.5 return 4.6 break和 continue 4.7 臭名昭著的“goto” 4.8 switch 4.9 总结 第5章 初始化与清理 5.1 用构造器确保初始化 5.2 方法重载 5.2.1 区分重载方法 5.2.2 涉及基本类型的重载 5.2.3 以返回值区分重载方法 5.3 缺省构造器 5.4 this关键字 5.4.1 在构造器中调用构造器 5.4.2 static的含义 5.5 清理:终结处理和垃圾回收 5.5.1 finalize()的用途何在 5.5.2 你必须实施清理 5.5.3 终结条件 5.5.4 垃圾回收器如何工作 5.6 成员初始化 5.6.1 指定初始化 5.7 构造器初始化 5.7.1 初始化顺序 5.7.2. 静态数据的初始化 5.7.3. 显式的静态初始化 5.7.4. 非静态实例初始化 5.8 数组初始化 5.8.1 可变参数列表 5.9 枚举类型 5.10 总结 第6章 访问权限控制 第7章 复用类 第8章 多态 第9章 接口 第10章 内部类 第11章 持有对象 第12章 通过异常处理错误 第13章 字符串 第14章 类型信息 第15章 泛型 第16章 数组 第17章 容器深入研究 第18章 Java I/O系统 第19章 枚举类型 第20章 注解 第21章 并发 第22章 图形化用户界面 附录A 补充材料 可下载的补充材料 Thinking in C:Java的基础 Java编程思想 研讨课 Hands-on Java研讨课CD Thinking in Objects研讨课 Thinking in Enterprise Java Thinking in Patterns(with Java) Thinking in Patterns研讨课 设计咨询与复审 附录B 资源 软件 编辑器与IDE 书籍 分析与设计 Python 我的著作列表 索引 ...

2020-04-14 · 2 min · 350 words · Hank

Spring IoC容器实现原理

1. IOC容器接口设计 Spring IOC容器接口设计如下图所示: Figure 1. Spring IoC容器接口设计 IOC容器两个核心的接口为BeanFacotry 和ApplicationContext,前者提供容器管理Bean的基本功能,后者则扩展 BeanFactory,提供了容器级的强大功能,如国际化支持、资源加载、事件、应用上下文环境等。 2. BeanFactory BeanFactory ,即bean工厂,定义IOC容器的基本规范,提供管理容器Bean的基本功能,如获取bean、判断单例、原型、判断包含bean、获取bean类型和别名等。下边有三个子接口 ListableBeanFactory、HierarchicalBeanFactory、AutowireCapableBeanFactory,他们具备不同的功能。 ListableBeanFactory 扩展 BeanFacoty ,增加了列举容器中bean的功能,BeanFactory 提供查询单个Bean的方法,而 ListableBeanFactory 可以查询多个Bean,如获取 BeanDefinition 的名称、数量、按类型查询bean和bean的名称等。 HierarchicalBeanFactory 在 BeanFactory 的基础上增加了父级 BeanFactory 的功能,即 BeanFacotry 可以有层级关系了。 AutowireCapableBeanFactory 在 BeanFactory 的基础上增加自动注入相关的功能。 ConfigurableBeanFactory 这是一个配置接口,扩展 HierarchicalBeanFactory,用于给 BeanFactory 提供各种配置功能,如配置 parentBeanFactory、beanClassLoader 添加后置处理器等等。 ConfigurableListableBeanFactory 这是一个配置接口,提供了大量方法来扩展 ConfigurableBeanFactory 和 ListableBeanFactory ,大多数可列举的bean工厂都需要实现它,除了 ConfigurableBeanFactory 的配置功能外,它还具备分析和修改bean定义以及预实例化单例bean的的能力。 3. ApplicationContext BeanFactory 提供了管理bean的基础功能,而 ApplicationContext 则在其基础上有用强大的容器特性。ApplicationContext 作为应用上下文,是Spring IOC容器的核心接口,具备 BeanFactory 基础管理bean的功能,还继承了 EnvironmentCapable 、 MessageSource 、 ResourceLoader 、 ApplicationEventPublisher ,具有资源加载、国际化支持、容器事件支持、系统环境支持等能力。 ...

2020-04-08 · 3 min · 627 words · Hank

Docker时间和系统时间不匹配,相差8小时

使用 maven docker 插件部署时,发现docker容器时间与本地时间相差8小时。 因为docker的时区与系统时区不同,docker容器默认的时区采用的UTC, 而中国时区为CTS,与之差8小时(东8区),可以进入容器使用date命令查看时区: root@pro-server-3:~/trainer# date Fri Mar 12 12:56:40 CST 2021 CST: China Standard Time,UTC+8:00 中国沿海时间(北京时间) UTC: Universal Time Coordinated 世界协调时间 所以设置一下Docker容器时区即可,我采用的docker maven插件,所以增加如下命令: 意思就是容器构建时执行 RUN 命令修改系统时间 /etc/timezone,将其设置为上海时间,问题解决。

2020-03-11 · 1 min · 30 words · Hank

Spring Bean自动扫描原理

Spring 基于注解的Bean自动扫描是由后置处理器 ConfigurationClassPostProcessor 来实现的。 1. ConfigurationClassPostProcessor类 ConfigurationClassPostProcessor 是一个 BeanDefinitionRegistryPostProcessor 实现,它的核心功能就是在容器启动时处理Spring的Java代码配置类(被 @Configuration 注解标记)。而且该类实现了 PriorityOrdered 接口,表示它是一个高优先级的后置处理器,会首先被执行。 ConfigurationClassPostProcessor 的定义如下: public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPostProcessor, PriorityOrdered, ResourceLoaderAware, BeanClassLoaderAware, EnvironmentAware { // 省略代码 } 2. 注册bean定义 创建 AnnotationConfigApplicationContext 时,其构造函数如下: AnnotationConfigApplicationContext构造函数 public AnnotationConfigApplicationContext(Class<?>... componentClasses) { this(); register(componentClasses); refresh(); } 首先,会调用 this() 方法调用构造函数: public AnnotationConfigApplicationContext() { this.reader = new AnnotatedBeanDefinitionReader(this); this.scanner = new ClassPathBeanDefinitionScanner(this); } 此时会创建 AnnotatedBeanDefinitionReader 和 ClassPathBeanDefinitionScanner 对象,前者用来读取配置类(@Configuration 注解标注)所定义的Bean定义信息,而后者用来扫描*classpath*下的bean定义信息。 AnnotatedBeanDefinitionReader 内部创建实例时会执行: ...

2020-03-10 · 9 min · 1827 words · Hank

Docker开启监听TCP端口,解决修改docker配置文件无效

Jenkins配置了Docker插件,结果发现不能远程调用远端的docker,原来是tcp端口没有打开。 ubuntu版本号:16.04 root@ubuntu:/etc/docker# cat /proc/version Linux version 4.4.0-62-generic (buildd@lcy01-30) (gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.4) ) #83-Ubuntu SMP Wed Jan 18 14:10:15 UTC 2017 ubuntu docker配置文件在 /etc/default/docker jenkins docker plugin链接远程docker时,需要docker开启tcp端口,编辑docker配置文件添加配置: vi /etc/default/docker 添加内容: DOCKER_OPTS="-H tcp://0.0.0.0:2376 -H unix:///var/run/docker.sock" 注意大小写,否则docker可能启动不起来。 重启docker: service docker restart 发现无效,查看docker进程,并未添加DOCKER_OPTS参数信息: root@ubuntu:/etc/docker# ps -ef | grep docker root 22829 1 0 14:28 ? 00:00:00 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock 编辑文件: vi /lib/systemd/system/docker.service 在[service]节点添加如下内容: EnvironmentFile=/etc/default/docker 然后修改ExecStart为: ExecStart=/usr/bin/dockerd -H fd:// $DOCKER_OPTS --containerd=/run/containerd/containerd.sock ...

2020-02-18 · 1 min · 97 words · Hank

IntelliJ IDEA 2019.3.3激活

IntelliJ IDEA 2019.3.3版本已经可以成功激活了,更新了非常多的性特性并极大的提升了性能。理论上此种激活方式适合2019.3的所有版本,不过笔者仅测试了3.3版本。 1. 激活步骤 具体激活步骤如下: 1、下载最新版本的idea, 目前版本为2019.3.3 2、下载用于激活的代理压缩包,下载地址如下: 下载地址 由于版权限制,请关注公众号并在后台回复6002获取下载地址!如果链接失效,请在公众号后台留言,我会第一时间补上。 3、解压缩下载的代理包,得到jetbrains-agent.jar,把它放到你认为合适的文件夹内 4、启动IDE,如果需要注册,选择:试用(Evaluate for free)进入IDE 5、点击你要注册的IDE菜单:"Configure" 或 "Help" → "Edit Custom VM Options …​" 如果提示是否要创建文件,请点"Yes",在打开的vmoptions编辑窗口末行添加: -javaagent:/absolute/path/to/jetbrains-agent.jar 一定要自己确认好路径(不要使用中文路径),要使用绝对路径,填错会导致IDE打不开! 另外,一个vmoptions内只能有一个-javaagent参数。 示例: mac: -javaagent:/Users/neo/jetbrains-agent.jar linux: -javaagent:/home/neo/jetbrains-agent.jar windows: -javaagent:C:\Users\neo\jetbrains-agent.jar 6、重启你的IDE 7、点击IDE菜单 "Help" → "Register…​" 或 "Configure" → "Manage License…​" 支持两种注册方式:License server 和 Activation code: 1). 选择License server方式,地址填入:http://fls.jetbrains-agent.com (注意与之前2.x的地址不同,通常会自动填上) 或者点击按钮:"Discover Server"来自动填充地址。 2). 选择Activation code方式离线激活,请使用压缩包内的ACTIVATION_CODE.txt中的注册码激活 推荐使用第一种方式。 OK,激活成功! 2. 常见问题 1、如果vmoptions中地址填错了,怎么办? 默认情况下,idea的VMoptions配置文件在用户目录中,我的mac电脑在: Contents> vi /Users/sun/Library/Preferences/IntelliJIdea2019.3/idea.vmoptions 如果还是填错了,可以找到用户目录下的这个文件,直接修改为正确的地址即可! 如果还是不行,参考这篇文章补救: https://intellij-support.jetbrains.com/hc/en-us/articles/206544519 注意!...

2020-02-14 · 1 min · 78 words · Hank

Spring IoC容器启动过程

1. 容器启动入口 一般而言,都使用 ApplicationContext 容器实现,这里使用了基于注解的 AnnotationConfigApplicationContext 实现。 要跟踪Spring的源码,可能需要结合不同的功能测试代码,以便跟踪源码时查看Spring具体的功能实现。比如:要了解Spring @ComponentScan 注解的原理,则首先需要编写一个配置类(@Configuration),然后定义组件扫描。 本文的跟踪代码的过程可能涉及多个部分的测试代码,但是容器启动的代码都类似,详细的代码见 Github。 容器启动测试代码[1] AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(ScopeConfig.class); (1) System.out.println("IOC容器创建完成..."); User user = applicationContext.getBean(User.class); User aUser = applicationContext.getBean(User.class); System.out.println(user == aUser); 1 构造一个 AnnotationConfigApplicationContext 对象。 AnnotationConfigApplicationContext 的构造函数如下: AnnotationConfigApplicationContext构造函数 public AnnotationConfigApplicationContext(Class<?>... componentClasses) { this(); register(componentClasses); refresh(); } 1.1. ApplicationContext 类实现关系 AnnotationConfigApplicationContext 扩展了 GenericApplicationContext,增加了基于java配置类的支持,而 GenericApplicationContext 内部持有一个 DefaultListableBeanFactory 实例,而且仅允许刷新一次容器: GenericApplicationContext构造函数 public GenericApplicationContext() { this.beanFactory = new DefaultListableBeanFactory(); } ...

2020-02-06 · 7 min · 1345 words · Hank

修改docker容器端口映射

在docker run创建并运行容器的时候,可以通过 -p 指定端口映射规则。但是,我们经常会遇到刚开始忘记设置端口映射或者设置错了需要修改。当docker start运行容器后并没有提供一个-p选项或设置,让你修改指定端口映射规则。那么这种情况我们该怎么处理呢?今天Docker君教你如何修改运行中的docker容器的端口映射? 方法一:删除原有容器,重新建新容器 这个解决方案最为简单,把原来的容器删掉,重新建一个。当然这次不要忘记加上端口映射。 优缺点: 优点是简单快捷,在测试环境使用较多。缺点是如果是数据库镜像,那重新建一个又要重新配置一次,就比较麻烦了。 方法二:修改容器配置文件,重启docker服务 容器的配置文件路径: /var/lib/docker/containers/[hash_of_the_container]/hostconfig.json 其中的 hashofthecontainer 是docker镜像的hash值,可以通过 docker ps 或者 docker inspect containername 查看。(CONTAINER ID就可以看出来) 文件中其中有一项是PortBindings,其中8080/tcp对应的是容器内部的8080端口,HostPort对应的是映射到宿主机的端口9190。8361/tcp对应的是容器内部的8361端口,HostPort对应的是映射到宿主机的端口9191。按需修改端口,然后重启docker服务,再启动容器服务就可以了。 systemctl restart docker 优缺点: 这个方法的优点是没有副作用,操作简单。缺点是需要重启整个docker服务,如果在同一个宿主机上运行着多个容器服务的话,就会影响其他容器服务。 方法三:利用docker commit新构镜像 docker commit:把一个容器的文件改动和配置信息commit到一个新的镜像。这个在测试的时候会非常有用,把容器所有的文件改动和配置信息导入成一个新的docker镜像,然后用这个新的镜像重起一个容器,这对之前的容器不会有任何影响。 1、停止docker容器 docker stop container01 2、commit该docker容器 docker commit container01 new_image:tag 3、用前一步新生成的镜像重新起一个容器 docker run --name container02 -p 80:80 new_image:tag 优缺点: 这种方式的优点是不会影响统一宿主机上的其他容器,缺点是管理起来显得比较乱,没有第二种方法那么直观。

2020-01-20 · 1 min · 52 words · Hank

Docker入门系列八——使用Dockerfile构建镜像

Dockfile就是一个文本文件,里边包含了一行行的指令,用来描述如何创建自定义镜像。 1. 使用 使用 docker build 命令来基于Dockerfile文件和上下文构建镜像,构建上下文指的是特定路径(PATH或URL)的文件集合,PATH用来指定本地文件系统目录,而URL用来指定Git仓库的地址,它们包含的所有文件(子目录或子模块)都会被递归处理。在大多数情况下,最好以空目录作为上下文,并将Dockerfile保存在该目录中。仅添加构建Dockerfile所需的文件。 docker build命令语法: docker build [OPTIONS] PATH | URL | - 例如: $ docker build . Sending build context to Docker daemon 6.51 MB ... 上边的命令会被docker daemon程序来处理而不是docker CLI,构建进程将发送指定路径下的Dockerfile文件到docker daemon。 Dockerfiie通过命令来关联构建上下文的文件,如 COPY、ADD 等命令。另外,可以通过 .dockerignore文件来排除文件或目录,以提高构建速度和性能。 一般而言,Dockerfile文件应该位于构建上下文的根目录中,但是,也允许使用 -f 选项来指定它的位置: $ docker build -f /path/to/a/Dockerfile . 也可以在镜像构建完成后指定tag: $ docker build -t shykes/myapp . 多次使用 -t 选项可以指定多个tag。 Docker daemon运行Dockfile之前会先进行校验,校验失败会输出错误信息: $ docker build -t test/myapp . Sending build context to Docker daemon 2.048 kB Error response from daemon: Unknown instruction: RUNCMD ...

2019-11-15 · 4 min · 711 words · Hank

使用Springboot开发websocket程序(四)——使用RabbitMQ作为STOMP消息代理

上一篇,我们在介绍了Spring中如何使用websocket的子协议stomp,并使用简单的基于内存的stomp消息代理来编写了一个web聊天室实例。基于内存的stomp消息代理,虽然能够满足基本需求,但还是存在一些不足,比如由于stomp代理在应用内部,多个外部websocket应用需要消息互通,那么就难以满足了。在本篇,我们来学习如何使用RabbitMQ作为stomp代理。 1. 为何要使用外部消息代理 简单消息代理,能够满足单websocket应用的需要,但是如果有多个websocket应用,他们之间需要进行消息共享,那么就需要做大量的工作才能实现了。其实,MQ一个最重要的作用就在于能个在各个系统间解耦。引入外部MQ作为stomp消息代理,很好的解决了多系统消息共享的问题,只要其支持stomp协议。RabbitMQ本身提供了对STOMP的支持,加上后结构变化如下: 前边的是单应用时的结构,后边为怎么了RabbitMQ过后,多个应用程序结构。 2. RabbitMQ对STOMP的支持 RabbitMQ对stomp协议的支持是通过插件的方式,默认stomp插件是关闭的,我们需要先启用之。 2.1. 启用插件 进入rabbitmq所在服务器,然后控制台输入如下命令来启用stomp插件: ``rabbitmq-plugins enable rabbitmq_stomp`` 然后可以查看插件是否启用成功: ``rabbitmq-plugins list`` 2.2. 插件配置 默认情况下,STOMP将会监听61613端口,默认的用户名和密码都为guest。通过配置文件来配置: ubuntu下rabbitmq的配置文件在/etc/rabbitmq/rabbitmq.conf,找到stomp开头的选项,就可以进行配置了 比如配置STOMP监听端口: ``stomp.listeners.tcp.1 = 12345`` RabbitMQ中STOMP适配器连接时如果用户名和密码使用默认的guest/guest,则可以忽略,如果需要修改,则配置如下: stomp.default_user = guest stomp.default_pass = guest 2.3. Destinations STOMP规范并没有规定消息代理来支持什么样的目的地(destination),只是根据消息头的destination的值来判断消息发送的目的地,一般由消息代理自定义支持,RabbitMQ中定义了几种destination类型: #exchange[/exchange]: 发送到任意的routing key和订阅任意的binding key #queue[/queue]: 发送和订阅队列,该队列由STOMP管理 #amqqueue[/amq/queue]: 发送和订阅外部创建的队列 #topic[/topic]: 发送和订阅到topic #temptopic[/temp-queue/]: 创建临时的队列(使用reply-to请求头) 现在,我们结合代码来看看Spring中对RabbitMQ的这几类destination是如何支持的。 3. Spring中使用RabbitMQ消息代理 我们通过一个demo来看看如何在Spring中使用RabbitMQ支持的这几个destination,整体界面如下; 下边的示例仅贴上部分关键代码,完整的代码可以参看文末的源码。 首先,我们创建一个名为03-websocket-stomp-rabbitmq的springboot工程,引入如下依赖: <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> <dependency> <groupId>io.projectreactor</groupId> <artifactId>reactor-net</artifactId> <version>2.0.5.RELEASE</version> </dependency> <dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> <version>4.0.33.Final</version> </dependency> <dependencies> ...

2019-11-12 · 3 min · 509 words · Hank

使用Springboot开发websocket程序(三)——基于子协议STOMP的web聊天室

我们使用Spring Boot对原生WebSocket的支持来编写了一个简单的聊天室程序。它仅支持群发,如果要做到点对点消息,我们还需要编写代码自己实现。本篇,我们将使用websocket的子协议STOMP来更新我们的聊天室程序,使其可以支持单聊。 1. STOMP 1.1. 为什么要支持子协议 WebSocket是一种简单的通信协议,而并非消息协议。它是TCP之上的非常薄的一层,所做的事情仅仅是将字节流转换为消息流(文本或二进制)。至于消息是什么含义,怎么路由和解析,都交由应用程序自身来决定。HTTP协议是一种应用级协议,它明确告诉我们请求的地址、格式、编码等等信息,但是websocket与之不同,websocket并不提供详细的信息来告诉我们如何路由和处理消息。因此,对于开发大型应用程序而言,websocke级别太低了,要实现非常复杂的功能,我们需要进行大量的编码工作。这就好比,大多JAVA WEB开发都会选择使用Spring框架,而不是基于Servlet API来实现。 基于这个原因,WebSocket RFC定义了 子协议的使用规范。在握手阶段,客户端和服务端使用Sec-WebSocket-Protocol请求头来通知彼此使用子协议,即更高级的、应用级的协议。当然,也可以不使用子协议,但是客户端和服务端仍然需要定义消息的格式。使用更规范的通用消息协议,更能让应用程序开发和维护变得简单。STOMP就是这样的一个消息协议,Spring框架提供了对其的支持。 1.2. 什么是STOMP STOMP即Simple (or Streaming) Text Orientated Messaging Protocol,中文称简单(流)文本定向消息协议,它是一种简单的互操作协议**,旨在通过消息代理在客户端之间传递异步消息,STOMP协议具有简单行和互操作性,因此在多种语言和多种平台上得到广泛地应用。。 STOMP是由于需要通过脚本语言(例如Ruby,Python和Perl)连接到企业消息代理而产生的,旨在对消息进行简单的消息处理,例如可靠地发送单个消息并断开连接或在给定目的地上消耗所有消息等等。它是其他开放消息协议(例如AMQP)和JMS代理(例如OpenWire)中使用的实现特定wire protocol的替代。STOMP仅仅实现部分常用的消息API,而不是实现完整的消息API,因此更加轻量和简单。 STOMP除了支持文本消息外,也支持二进制消息,默认使用UTF-8的编码格式。 STOMP有几个比较重要的概念: frame:帧,即STOMP客户端和服务端发送的数据 COMMAND: frame的命令,由STOMP规范定义,用来表示消息的特定用途,比如连接服务端、发送消息等,每个frame都有命令 destination:消息发送的目的地址,通过消息头来表示,如destination:/topic/chat STOMP基于frame,frame的模型源于HTTP,一个frame由命令、一组可选的标头和可选的主体组成,客户端和服务端发送frame来进行通信。frame的格式如下: COMMAND header1:value1 header2:value2 Body^@ frame分为两个部分:消息头和消息体。消息头第一行为命令,然后跟key:value格式的头信息;然后是消息体,消息体跟消息头之间用一个空行分隔。最后,消息体后跟八位空字节(上文用^@表示)。另外,frame的命令和消息头信息都区分大小写。 STOMP客户端支持的命令包括:SEND、SUBSCRIBE、UNSUBSCRIBE、BEGIN、COMMIT、ABORT、ACK、NACK、DISCONNECT、CONNECT、STOMP 服务端支持的命令包括:CONNECTED、MESSAGE、RECEIPT、ERROR 只有几种消息可以包含消息体:SEND、MESSAGE、ERROR 举个例子,客户端连接到服务端,则会想服务端发送如下的frame: CONNECT accept-version:1.0,1.1,2.0 host:stomp.github.org ^@ 服务端接收连接请求则会发送下边的frame给客户端,否则发送ERROR frame: CONNECTED version:1.2 ^@ 1.3. 使用STOMP的优点 使用STOMP作为子协议,与使用原生WebSocket相比,Spring框架和Spring Security可以提供更丰富的编程模型。具体的优点有以下几点: 无需自定义消息协议和消息格式 可以使用STOMP客户端,包括Spring框架中的Java客户端 消息代理(例如RabbitMQ,ActiveMQ和其他代理)可以用于管理订阅和广播消息 可以在任意数量的@Controller中组织应用程序逻辑,并根据STOMP消息头将消息路由给它们,而对于给定的连接,可以使用单个WebSocketHandler处理原始WebSocket消息 可以使用Spring Security基于STOMP 目标(destinations)和消息类型来对消息进行安全处理 详细内容请看STOMP规范: https://stomp.github.io/stomp-specification-1.2.html 2. Spring Boot中使用STOMP 使用STOMP有如此多的好处,我们看看Spring中如何使用。 在Spring boot中使用STOMP,大概需要以下几步: ...

2019-11-02 · 5 min · 914 words · Hank

使用Springboot开发websocket程序(二)——基于原生websocket的web聊天室

上一篇介绍了什么是websocket,说到websocket是一种由HTML5定义的浏览器和服务器保持长连接的通信协议,可以进行实时数据交换。在本篇,我们将使用Spring boot,基于原生websocket开发一个web聊天室,并一步步介绍如何在spring boot中开发websocket程序。 一个WebSocket程序包括客户端和服务端。WebSocket客户端除了支持Html5的浏览器外,还包括各大语言提供的WebSocket实现,比如Java中Spring框架的实现,从而在没有浏览器时也能进行websocket通信。HTML5中WebSocket API请看 这里。服务端中,Java定义Java WebSocket API标准 JSR-356[JSR-356],Java主流容器都已经支持websocket,主要包括Tomcat 7.0.47 ,Jetty 9.1 +,GlassFish 4.1 +,WebLogic 12.1.3+和Undertow 1.0(以及WildFly 8.0+)等。 1. 整体功能 现在我们来写一个简单的web的聊天室程序,并一步步学习Spring中是如何封装WebSocket的,这里工程还是使用Spring Boot。 整体功能界面如下: 功能很简单:用户填写上自己的用户名,然后点击链接按钮进入聊天室,然后就可以发送消息给其他人,聊天室中的用户可以看到他人的连入信息和发送的消息。 我们看看在spring boot中如何编写以上程序。 2. 服务端 1、新建一个springboot-websocket的maven工程,引入如下依赖: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> 引入该启动器后,Spring boot会自动引入一下几个启动器或jar包: spring-boot-starter:spring boot启动器 spring-boot-starter-web:spring boot web工程启动器 spring-messaging:包含对消息传递应用程序的基础支持,下一篇用到的STOMP协议的支持也在这个模块。它是Spring Integration项目中的关键抽象,包含Message,MessageChannel,MessageHandler和其他可以作为此类消息传递体系结构基础支撑 spring-websocket: Spring对websocket的支持,它与Java WebSocket API标准( JSR-356[JSR-356])兼容,并且还提供了很多扩展 由此可见,对websocket的支持在spring-websocket和spring-messaing两个模块。 2、编写启动类: @SpringBootApplication public class SpringWebsocketApplication { public static void main(String[] args) { SpringApplication.run(SpringWebsocketApplication.class, args); } } 上边的两步没什么说的,常规的spring boot工程开发。 3、编写websocket握手拦截器: ...

2019-11-01 · 3 min · 577 words · Hank

使用Springboot开发websocket程序(一)——什么是websocket

在互联网飞速发展的当代,浏览器和服务器之间的实时通信已经越来越重要,传统的HTTP协议难以解决实时通信的需求。因此,由HTML5定义的websocket协议应运而生。这里我将用三篇文章来介绍websocket,并使用Springboot开发一个web聊天室的程序,其中会使用原生websocket协议开发,也会单独来介绍使用STOMP协议来开发。 在本篇,我们先看看什么是websocket。 1. 诞生背景 由于HTTP是无状态的协议,采用请求响应模型,服务端是不能主动推送信息给客户端的,只能由客户端发起请求,然后再由服务端进行响应。如果服务端数据有变化,必须通过客户端来获取,服务端是不能主动推送的。要解决实时交换数据的需求,一般的做法是通过轮询(还有http long pull和streaming两种方案,参见这篇 博文)来获得服务端最新信息,即:客户端(浏览器)每隔一段时间(比如1秒)向服务端发起请求,以获取最新的数据。 我们知道,HTTP是一种无状态协议,客户端请求然后服务端响应,则会断开TCP连接。因此,使用轮询的弊端是,客户端不断与服务端建立连接、断开连接,效率非常低下,且非常消耗服务器资源,并且也不是真正意义上的实时。 因此,我们需要一种技术,能够让服务端主动推送数据给客户端,而且消耗资源很少,效率更高。 2. 什么是websocket WebSocket是一种在 单个TCP连接上进行全双工通信的协议,2008诞生,在2011年被IETF定为标准 RFC6455,并由 RFC7936补充规范。WebSocket API也被W3C定为标准,目前各大主流浏览器都已经支持WebSocket。 WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据,这些数据通常包括两种:文本和二进制。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。 3. 客户端与服务器的握手 WebSocket基于Http协议来完成握手(Handshake)阶段,在该阶段,客户端需要将服务端请求协议升级,由原来的HTTP协议升级为WebSocket协议,如果服务端能够支持WebSocket,则会告诉客户端升级成功。之后,双方就建立了一条快速通道,可以互相进行数据交互,后续通信就采用WebSocket协议来完成。 Figure 1. websocket与http的区别(图片来源于网络 我们来简单看一下websocket与服务端的握手过程。WebSocket请求头跟HTTP请求头类似,只是添加了一些特殊的头标签,一个握手请求的头信息如下: GET /chat HTTP/1.1 Host: server.example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw== Sec-WebSocket-Protocol: chat, superchat Sec-WebSocket-Version: 13 Origin: http://example.com 可以看到,websocket在握手阶段,采用的仍然是HTTP协议,但是请求添加了一些websocket特有的信息: Upgrade: websocket:就是在请求服务端升级通信协议为WebSocket Sec-WebSocket-Key:浏览器随机生成的一个base64的加密字符串,服务器用它来进行校验请求是来自websocket客户端 Sec-WebSocket-Protocol: 告诉服务器不直接使用websocket,而是使用websocket协议的子协议,例如后文将要介绍的STOMP就是websocket协议的子协议来通信 Sec-WebSocket-Version: 使用websocket协议的版本号 请求发送到服务器,如果服务器也支持websocket则升级成功,并响应如下头信息: HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk= Sec-WebSocket-Protocol: chat 这里,websocket握手完成服务器则会响应101状态码,至此,HTTP协议服务就已经结束,后续通信就采用websocket了。 4. websocket的应用场景 浏览器需要与服务端进行实时数据交换时,都可以使用websocket协议。最常见的如视频直播网站,都有聊天室和弹幕功能,巨大的数据量,如果使用HTTP,交过可想而知。另外,网页游戏开发、也包括一些电商类网站的秒杀、团购等等场景,都会用到websocket。 5. websocket备选方案 目前,各大浏览器都已经支持websocket,如果仍要兼容老版本的浏览器,或者某些代理服务对websocket协议有一些限制,又或者由于连接保持打开时间过长而被浏览器强制断开,导致出现问题,该怎么办呢? ...

2019-11-01 · 1 min · 98 words · Hank

使用Redis实现分布式锁

在单体Java应用中,由于代码运行于同一个JVM,使用实现资源加锁是比较容易的,例如使用synchronized或ReentrantLock加锁来控制并发访问;但是在分布式系统中,多个分布式系统之间也需要控制并发访问,由于处于不同的JVM,此时就不能简单使用java的锁机制来进行控制。这种跨进程或者跨服务器的加锁,需要额外使用全局的获取锁的服务,就是本文探讨的分布式锁。 1. 为什么需要分布式锁 分布式锁解决的问题:保证分布式系统的共享资源在某一时刻只被一个客户端访问,保证数据的准确性。 举个例子: 如图所示,订单服务下单前需要保证库存足够,库存服务首先会检查库存充足,然后在将订单的库存数量锁定,如果此时管理系统对库存数量进行了修改,那么由于跨系统的并发操作可能操作库存数据的不正确。此时,对库存的操作就需要考虑分布式锁,将库存锁定,暂时不能更改。 这样一来,对库存的更改和扣减操作使用同一把锁来锁定,每次只有一个客户端能够操作成功,要么订单服务先扣减服务,要么管理系统先修改库存,反正两个不能同时进行。 2. 分布式锁的现有方案 分布式锁的整体思路是:在分布式系统中,有一个全局的东西(中间件或服务),各个服务需要加锁时都向它获取锁,然后给锁一个标记(例如锁的名称),如果标记相同则认为是同一把锁,这样就可以控制各个系统的资源共享。 目前,分布式锁的方案大致有以下几种: 基于Zookeeper的临时节点 基于Redis的SET命令 这里仅仅讨论Redis的分布式锁实现。 3. Redis实现分布式锁的原理 基于Redis来实现分布式锁,其原理很简单:在Redis中设置一个Key,表示加锁,如果其他系统来加锁时发现这个Key已经存在,表示已经加了锁,则它获取锁失败,然后它再不断重试加锁,直到加锁成功,然后才能执行后续业务逻辑。释放锁也很简单,直接将这个KEY删除即可。 锁一旦被创建,就必须能够释放,否则会引起死锁(其他系统永远获取不到锁),一般会使用Redis的过期机制,让KEY一段时间后自动过期以避免死锁。 加锁时,过程如下: 首先,使用SET命令来为某一个KEY(可以作为锁名称)设置一个唯一的值,仅当KEY不存在时才能加锁成功,如果KEY存在则设置失败,表明锁已经存在; 其次,为该KEY设置一个过期时间,来避免死锁问题; 释放锁时,先获取锁是否存在,如果存在则调用DEL命令删除该KEY。 无论是加锁,还是释放锁,都需要保证命令的原子性执行(要么都成功,要么都失败,试想一下,如果加锁时SET命令成功,然后在调用EXPIRE命令设置过期时间,未完成时Redis宕机了,会造成死锁)。例如,加锁时,SET命令和设置过期时间需要为一个原子命令,Redis已经提供了原子命令,如下: // NX是指如果key不存在就成功,key存在返回false,PX指定过期时间,单位毫秒 SET anyLock unique_value NX PX 30000 释放锁时,获取锁和删除KEY为一个原子操作,Redis没有提供获取KEY然后DEL的原子命令,这里需要用到LUA脚本以保证原子性: // 执行LUA脚本保证原子性,先获取锁,然后调用DEL删除 if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end 需要注意的是,加锁时设置的KEY值value必须是唯一的,这是因为在释放锁时需要获取到该值以便验证释放锁的客户端和加锁的客户端是同一客户端,如果value值不唯一则可能客户端A加了锁,但是由客户端B给释放了,引起业务混乱而没有达到加锁的目的。 4. 三种部署方式下的锁问题 Redis有三种部署方式,每种方式下的分布式锁都存在一些问题: 1、单机部署 这种方式下,很明显的缺点就是单点问题,Redis故障了,那么分布式锁就不能使用了。 2、Master-Slave + Sentinel模式 主从+哨兵模式,主节点挂了,哨兵会重新选择一个从节点作为主节点,数据会复制到从节点上,但是复制过程需要一定的时间,如果主节点挂了,它上边的锁可能还没有复制到从节点上,就会造成锁丢失。 3、Cluster模式 集群部署模式,同理,在某一个节点上的锁可能还没有复制到其他节点上,同样会造成锁丢失。 使用Redis的分布式锁,其优点是性能很高,支持高并发分布式锁场景,缺点则是如果加锁失败,需要不断循环重试加锁,消耗资源,另外,Redis集群下可能造成锁丢失的极端情况,对于这种情况,Redis的作者也考虑到了,他提出了RedLock算法,具体可以看 这里。 5. 使用Redisson的分布式锁 一般而言,不推荐自己实现Redis分布式锁,因为需要考虑诸如锁重入等多种情况,Java的Redisson框架已经为我们提供了分布式锁的支持。 Redisson是一个Java版的Redis Client,提供了大量的基于Redis的分布式特性支持,例如 分布式锁、分布式任务调度、分布式远程服务、分布式集合等等,Redisson官网: https://redisson.org/ 要使用Redisson的分布式锁非常简单,基本的代码如下: RLock lock = redisson.getLock("anyLock"); lock.lock(); // do something …… lock.unlock(); ...

2019-10-17 · 2 min · 392 words · Hank

IntelliJ IDEA 2019.2.x激活

IntelliJ IDEA 2019.2.x版本已经可以成功激活了,理论上此种激活方式适合2019的所有版本,不过笔者仅测试了2.3版本。具体激活步骤如下: 1、下载最新版本的idea, 目前版本为2019.2.3 2、下载用于激活的代理压缩包,下载地址如下: 链接 https://pan.baidu.com/s/1bsG4E9P744VlgG_kwgZ40w 密码 z98a 3、解压缩下载的代理包,得到jetbrains-agent.jar,把它放到你认为合适的文件夹内 4、启动IDE,如果需要注册,选择:试用(Evaluate for free)进入IDE 5、点击你要注册的IDE菜单:"Configure" 或 "Help" → "Edit Custom VM Options …​" 如果提示是否要创建文件,请点"Yes",在打开的vmoptions编辑窗口末行添加: -javaagent:/absolute/path/to/jetbrains-agent.jar 一定要自己确认好路径(不要使用中文路径),要使用绝对路径,填错会导致IDE打不开! 一个vmoptions内只能有一个-javaagent参数。 示例: mac: -javaagent:/Users/neo/jetbrains-agent.jar linux: -javaagent:/home/neo/jetbrains-agent.jar windows: -javaagent:C:\Users\neo\jetbrains-agent.jar 如果还是填错了,参考这篇文章编辑vmoptions补救: https://intellij-support.jetbrains.com/hc/en-us/articles/206544519 注意 这是最重要的一步,不要直接在IDEA目录下修改配置文件,而是在IDEA启动后通过菜单来修改,否则可能不能成功激活** 6、重启你的IDE 7、点击IDE菜单 "Help" → "Register…​" 或 "Configure" → "Manage License…​" 支持两种注册方式:License server 和 Activation code: 1). 选择License server方式,地址填入:http://jetbrains-license-server (可能会自动填上) 或者点击按钮:"Discover Server"来自动填充地址。 2). 选择Activation code方式离线激活,请使用压缩包内的ACTIVATION_CODE.txt中的注册码激活 OK,激活成功! 代理包连接失效请留言,我会第一时间补上! 此项目仅供交流学习用,请勿用于商业用途,若资金允许,请购买正版!

2019-10-12 · 1 min · 65 words · Hank

Docker入门系列七——端口映射与容器互联

1. 端口映射 1.1. 外部访问容器内的应用 如果外部要访问容器,需要将容器的端口开放出来,使用 -p 或 -P (大写)参数来映射容器端口和宿主机端口的关系: -p:明确指定端口映射,每个端口只能绑定一个容器,格式包括:IP:HOST_PORT:CONTAINER_PORT|IP:CONTAINER_PORT|HOST_PORT:COINTAINER_PORT 举例: docker run -p 8080:80 nginx 该命令表示启动容器是将宿主机的8080端口映射为容器的80端口,外部访问宿主机的8080端口就是在访问容器的80端口。 -P(大写):随机映射一个宿主机的可用端口到容器 举例: docker run -P nginx 通过docker ps命令可以看到端口的映射结果: root@ubuntu:~# docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 9e4cb770c5cc nginx "nginx -g 'daemon of…" 17 seconds ago Up 16 seconds 0.0.0.0:32768->80/tcp peaceful_robinson 可以看到,宿主机的32768端口映射到容器的80端口 1.2. 映射多个端口 多次使用 -p 参数,可以将容器的多个端口与宿主机的多个端口进行映射 例如: docker run -p 8080:80 -p 8081:80 nginx 1.3. 映射指定地址的指定端口 使用 IP:HOST_PORT:CONTAINER_PORT 格式来指定IP地址下的端口 例如: ...

2019-09-15 · 2 min · 234 words · Hank

SpringBoot自定义日志配置

当系统出现问题时,先从表现分析问题远远不够,要快速、准确地分析并定位问题原因,日志就是一个非常重要且必不可少的手段。Java而言,日志框架有很多,常用的有Common-logging、Java自带的Logging、Log4J、Logback等。另外,还有一个Slf4J的框架,主要目的是将这些框架进行整合。本篇主要介绍的是SpringBoot中的日志支持,如何进行日志自定义。 1. 日志框架选择 在SpringBoot内部,使用common-logging框架来记录日志,也支持自定义配置,支持的框架有:Java util logging(JDK自带)、Log4J和Logback等。 如果使用了Springboot的starter,则springboot会使用Logback作为默认的日志框架,从maven的依赖关系可以看到,spring-boot-starter下依赖的spring-boot-starter-logging启动器使用了logback: <dependencies> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>jcl-over-slf4j</artifactId> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>jul-to-slf4j</artifactId> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>log4j-over-slf4j</artifactId> </dependency> </dependencies> 如上所示,Spring boot内部使用Slf4J框架,作为日志抽象层,以便整合并支持各大主流的框架,具体日志实现还是使用logback。 2. 日志输出内容 一个标准Spring boot日志如下图所示: 其实,Spring boot工程默认的日志级别为INFO,仅输出INFO级别的日志信息,由于我这里开启了debug模式(配置debug=true),所以能打印出DEBUG级别的日志级别。 一般而言,日志有几个重要的输出项: 时间:记录日志时的时间,精确到毫秒,便于排序,缩小问题范围的重要指标,例如问题产生在某天10:20,那么就需要查找这段时间的所有日志以帮助分析问题 线程:输入日志的执行线程名称 日志级别:日志的输出级别,不同的日志框架级别定义不同,而且日志级别有层级之分,一般而言,日志级别从低到高都有调试(DEBUG)、信息(INFO)、警告(WARNING)、错误(ERROR)等,不同的框架可能名称不同,日志级别越高,输出的信息越少,反之则越多 进程ID:记录输出日志的进程ID 类名:输出日志的类名称,通常是简写的,例如:c.b.WebappViewStarter 日志内容:具体的日志内容 如果输出终端支持ANSI字符集,那么日志会彩色显示,可以通过配置spring.output.ansi.enabled的值来设置彩色显示的条件,包括ALWAYS、DETECT、NEVER。日志的颜色可以通过在日志输出格式时定义%clr来配置,支持的颜色有:blue、cyan、faint、green、magenta、red、yellow,例如: ``%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){yellow}`` 输出的日志级别默认为INFO,可以通过在配置文件修改来自定义日志级别: logging.level.root=WARN logging.level.org.springframework.web=DEBUG logging.level.org.hibernate=ERROR root配置整个日志输出的级别,没有特别配置均按照此级别输出,而下边两项则是配置具体某一个框架的日志级别。 3. 输出日志到文件 默认情况下,SpringBoot仅将日志输出到控制台,但我们更希望将日志输出到单独的文件,此时我们需要在配置文件中添加logging.file或者logging.path配置项,他们的作用如下: logging.file:配置一个具体的日志文件的路径,可以是绝对或者相对路径,建议定义为绝对路径,避免不需要的路径问题,例如logging.file=/data/logs/springboiot-demo.log, logging.path:配置一个存储日志的目录,会在该目录下创建spring.log文件,并写入日志内容,如:logging.path=/var/log 需要注意的是,二者不能同时使用,如若同时使用,则只有logging.file配置生效。默认情况下,日志文件的大小达到10MB时会重新创建新文件来写入日志,原日志文件会被顺序重新命名,例如:springboot-demo.log、springboot-demo.log1,默认输出的日志级别为:ERROR、WARN、INFO。 4. 自定义日志 Springboot虽然可以成功将日志输出到自定义的文件中,但是通常,我们更希望按照日期来输出日志文件,例如每天一个日志文件,保留一定天数的日志文件。Springboot也支持此类特殊需求的自定义日志,只需要提供一个自定义日志配置文件即可,该配置文件可以通过logging.config配置项来定义,例如:logging.config=classpath:logging-config.xml,但是,一般不需要我们配置,只需要使用springboot能加载的日志配置文件名即可,springboot默认可以加载下表的这些配置文件: 日志框架 自定义日志配置文件 Logback logback-spring.xml, logback-spring.groovy, logback.xml 或 logback.groovy Log4j2 log4j2-spring.xml or log4j2.xml JDK (Java Util Logging) logging.properties 例如,我们要使用默认的logback日志框架,并且日志要按天输出,我们只需在resources目录下添加一个logback-spring.xml文件,加入如下内容: ...

2019-09-06 · 1 min · 182 words · Hank

Mysql binlog格式

在前一篇,我们开启了Mysql的binlog,当Mysql在进行数据更改操作时会自动记录binlog日志,以便进行主从服务,或者恢复丢失的数据。Binlog的日志内容可以设置不同的格式,以满足不同的需求。 1. Binlog格式 Binlog有三种格式(format),包括STATEMENT、ROW和MIXED,根据Mysql版本的不同,有着不同的默认值,它们的设置方式和默认值见下表: 属性 值 命令行格式 --binlog-format=format 系统变量 binlog_format 范围 GLOBAL, SESSION 动态 是 类型 列举 默认值(>= 5.7.7) ROW 默认值(⇐ 5.7.6) STATEMENT 有效值 ROWS TATEMENT MIXED 1.1. STATEMENT 基于SQL语句的日志记录,每一条修改数据的sql语句都会被记录在binlog中,但是不会记录数据前后的变化。 优点 只需要记录执行语句的细节和上下文环境,避免了记录每一行的变化,在一些修改记录较多的情况下相比ROW level能大大减少binlog日志量,节约IO,提高性能;还可以用于实时的还原;同时主从版本可以不一样,从服务器版本可以比主服务器版本高 缺点 为了保证sql语句能在slave上正确执行,必须记录上下文信息,以保证所有语句能在slave得到和在master端执行时候相同的结果;另外,主从复制时,存在部分函数(如sleep)及存储过程(procedure、function、及trigger)在slave上会出现与master结果不一致的情况,而相比Row level记录每一行的变化细节,绝不会发生这种不一致的情况 1.2. ROW 基于行的日志记录,不记录SQL语句,仅记录被修改的行的数据信息。 优点 能非常清晰的记录下每行数据的修改细节,不需要记录上下文相关信息,因此不会发生某些特定情况下的procedure、function、及trigger的调用触发无法被正确复制的问题,任何情况都可以被复制,且能加快从库重放日志的效率,保证从库数据的一致性 缺点 由于所有的执行的语句在日志中都将以每行记录的修改细节来记录,因此,可能会产生大量的日志内容,干扰内容也较多;比如一条update语句,如修改多条记录,则binlog中每一条修改都会有记录,这样造成binlog日志量会很大,特别是当执行alter table之类的语句的时候,由于表结构修改,每条记录都发生改变,那么该表每一条记录都会记录到日志中,实际等于重建了表。 row模式生成的sql编码需要解码,不能用常规的办法去生成,需要加上相应的参数(--base64-output=decode-rows -v)才能显示出sql语句; 新版本(5.7.7以后)binlog默认为ROW,且5.6新增了一个参数:binlog_row_image;把binlog_row_image设置为minimal以后,binlog记录的就只是影响的列,大大减少了日志内容 1.3. MIXED STATEMENT和ROW格式的混合,默认使用STATEMENT来记录binlog,当其无法实现主从复制的操作(例如函数操作)时,会切换到ROW格式来记录binlog。 2. 对比 接下来,我们使用mysql5.7.22版本来对比一下三种格式下的日志输出情况。 ...

2019-08-28 · 5 min · 893 words · Hank

Mysql开启Binlog并做数据备份

某些时候,一不小心删除了数据库的数据甚至某些表、库,顿时慌得一P,是不是想死的心都有了?尤其是在小公司或者小项目中,没有遵循严格的规章制度,每个开发人员都有测试数据库甚至生产库的权限,一些没有经验的程序员一顿胡乱操作,导致数据库的数据丢失的情况时有发生。此时,我们就需要用到Mysql的binlog了。 1. Binlog简介 Binlog,即binary log、二进制日志,记录了描述数据库更改的“事件”(EVENT),例如表创建操作或对表数据的更改(DELETE、UPDATE、INSERT),通俗的说,binlog主要用来记录对mysql数据更新或潜在发生更新的SQL语句(例如DELETE语句没有删除任何行也会记录),并以”事务”的形式保存在磁盘中Binlog;另外,它还包含有关每个语句获取更新数据的时间长度的信息。 二进制日志有两个重要目的: 数据同步:mysql master上的更改数据,通过binlog发送给slave节点,然后达到主从数据一致的目的 数据恢复:binlog为数据恢复提供了有效的手段,因为binlog记录了数据操作的内容、时间等关键信息,可以根据binlog来恢复数据 Binlog仅记录了DML和DDL,并不包括查询(SELECT和SHOW)语句。Binlog会稍微降低性能,但是对于其数据同步和数据恢复等优势而言,牺牲些许性能是值得的。 2. 开启binlog 可以在启动mysql是添加--log-bin[=base_name] 参数来启用binlog,base_name为日志文件的基础名称(base_name为空则默认使用pid-file选项的名称,该选项默认是主机名),通常建议自己设置base_name而不是使用默认的名称,或者通过修改mysql配置文件来开启。 我们看看如何修改mysql配置文件来开启binlog,环境使用的是ubuntu: 1、查看是否开启binlog 登录mysql,进入mysql命令行,输入如下命令查看: mysql> show variables like 'log_bin%' 结果如下: mysql> show variables like 'log_bin%'; +---------------------------------+--------------------------------+ | Variable_name | Value | +---------------------------------+--------------------------------+ | log_bin | ON | | log_bin_basename | /var/log/mysql/mysql-bin | | log_bin_index | /var/log/mysql/mysql-bin.index | | log_bin_trust_function_creators | OFF | | log_bin_use_v1_row_events | OFF | +---------------------------------+--------------------------------+ 5 rows in set (0.00 sec) 如果log_bin为OFF则未开启,ON则为已开启。另外有两个重要的信息: log_bin_basename:binlog存储的文件基础名称,完整的日志文件后缀会递增为形如0000x的格式,例如mysql-bin.00001 log_bin_index: binlog索引文件的名称,该文件存储了所有的binlog日志文件的位置 ...

2019-08-28 · 3 min · 631 words · Hank

Docker入门系列六——数据管理

容器的数据管理有两种方式: 数据卷(Data Volumes):容器内数据直接映射到本地主机环境 数据卷容器(Data Volume Containers):使用特定容器维护数据卷 1. 数据卷 数据卷是一个可供容器使用的特殊目录,将主机的目录映射进容器,类似于Linux中的mount行为。 特点: 可在容器间共享和重用,容器间传输数据变得高效 对数据卷内数据的修改会立即生效,容器和本地操作都可以 数据卷的更新不会影响镜像,应用和数据解耦 卷会一直存在,直到没有容器使用,就可以安全地卸载它 1.1. 数据卷管理 volume命令: root@ubuntu:~# docker volume --help Usage: docker volume COMMAND Manage volumes Commands: create Create a volume inspect Display detailed information on one or more volumes ls List volumes prune Remove all unused local volumes rm Remove one or more volumes 1.1.1. 创建数据卷 docker volume create -d local test 数据卷创建后存放在/var/lib/docker/volumes下。e.g. root@ubuntu:~# docker volume create -d local test test root@ubuntu:~# cd /var/lib/docker root@ubuntu:/var/lib/docker# ls builder buildkit containers image network overlay2 plugins runtimes swarm tmp trust volumes root@ubuntu:/var/lib/docker# cd volumes/ root@ubuntu:/var/lib/docker/volumes# ls metadata.db test root@ubuntu:/var/lib/docker/volumes# cd test/ root@ubuntu:/var/lib/docker/volumes/test# ls _data root@ubuntu:/var/lib/docker/volumes/test# ...

2019-08-20 · 4 min · 719 words · Hank

Docker入门系列五——仓库

docker 仓库的概念与java的maven仓库非常类似,它们都用来存储数据,maven仓库存储的是 jar 包,docker 仓库存储的是镜像。许多第三方私有仓库工具如 nexus 既可以构建 maven 仓库,也支持 docker 仓库。 1. 镜像仓库和注册表 1.1. Registry 镜像注册表,用来存储镜像数据的地方,官方的Docker hub就是一个公共的Registry,另外,还可以通过官方的registry镜像搭建私有的镜像注册表。通常所说的镜像仓库是泛指Registry,但并不完全准确,一个Registry可以包含多个Repository。 例如,拉取镜像:docker pull registry.hub.docker.com/ubuntu:18.04,这里的registry.hub.docker.com就是官方提供的镜像注册表,可以省略不写。 1.2. Repository 镜像库,包含多个镜像,存储于Registry中。在仓库搜索镜像时,按名称搜索在registry中查找repository。例如,我们所说的nginx镜像,一般就是指的nginx的Repository,它包含多个nginx镜像,它们通过tag来区分。 镜像的类别: 根镜像:Docker官方提供的基础镜像,单名字,如centos、ubuntu等 用户的镜像:由docker用户创建并维护的镜像,带有用户名前缀,表明是某用户下的仓库,例如:ansible/centos7-ansible,belonk/mynginx等 第三方镜像市场:阿里云、腾讯云、网易云等等 2. 搭建本地私有仓库 执行如下命令: docker run -d -p 5000:5000 registry:2 它会下载并启动一个registry容器,registry对应的是仓库镜像,版本为2。 上传镜像到本地私有仓库: 标记镜像: docker tag ubuntu IP:端口/镜像 上传镜像: docker push IP:端口/镜像 3. 基本操作 登录Docker hub Docker Hub是官方的公共镜像仓库,注册账号,然后在命令行进行登录: docker login 输入账号和密码登录即可,登录信息保存在 ~/.docker/config.json 下载镜像 docker [image] pull 搜索镜像 docker search IMAGE 上传镜像 docker [image] push [REGISTRY_HOST[:REGISTRY_PORT] / ]NAME[:TAG] ...

2019-08-12 · 1 min · 72 words · Hank

Docker入门系列四——容器

前一篇我们介绍了Docker的核心概念–镜像,知道了镜像是只读的创建容器的指令模板,由不同的层组成,也提到了镜像运行后就成为了容器,容器启动后回家镜像上增加一个可写的容器层,容器和镜像最主要的区别就在于容器层,容器层可读写,新写入或者修改的数据都存储在容器层上。在本篇,我们再来详细了解一下Docker的容器。 1. 简介 容器是镜像的可运行实例。您可以使用Docker API或CLI创建,启动,停止,移动或删除容器。您可以将容器连接到一个或多个网络,附加存储,甚至可以根据其当前状态创建新镜像。默认情况下,容器与其他容器及其主机相对隔离。您可以控制容器的网络,存储或其他基础子系统与其他容器或主机的隔离程度。容器由其镜像以及在创建或启动时为其提供的配置选项定义。删除容器后,其未持久存储的状态数据都将消失。 简单而言,容器可以看作是一个或一组独立运行的应用,以及这些应用必须的运行环境的一个整体。镜像是只读的,而容器则是在镜像上层添加了一个可写的容器层,镜像运行的实例就是容器。容器都有唯一的CONTAINER ID和名称(NAME),还包括该容器对应的镜像(IMAGE)、状态(STATUS)等属性,另外,容器可以被创建、启动、停止和删除。 例如,ubuntu是一个镜像,运行后,就得到了容器,多次运行则得到多个容器。 2. 容器操作 现在我们看看操作容器的一些基本命令。 2.1. 查看容器 查看容器列表 1、命令 docker container ls [OPTIONS] 改命令等同于: docker ps [OPTIONS] 选项: -a, --all:显示所有的容器(不加此选项则仅显示运行中的) -f, --filter:按照条件过滤查询 --format string: 格式化输出信息 -n, --last int: 仅显示最后的n个容器(所有状态) -l, --latest: 仅显示最近创建的容器(所有状态) --no-trunc: 不截断输出信息 -q, --quiet: 仅输出容器id -s, --size: 设置显示的总数 2、举例 查看本机运行的容器: root@ubuntu:~# docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 1dd6cb8df75d ubuntu "/bin/bash" 24 hours ago Up 24 hours dbdata2 d428c1240bc8 ubuntu "/bin/bash" 25 hours ago Up 25 hours db2 2442cb173404 ubuntu "/bin/bash" 25 hours ago Up 25 hours db1 db3ac83e2531 ubuntu "/bin/bash" 26 hours ago Up 25 hours dbdata ...

2019-07-29 · 7 min · 1411 words · Hank

Docker入门系列三——镜像

上一篇介绍了如何在Windows、macOS、Ubuntu上安装docker,安装完成后,我们将开始正式学习docker。Docker的三大核心概念分为是:镜像、容器和仓库,在本篇,我们将介绍第一个核心概念----镜像。 1. 简介 Docker镜像(Image),一个镜像是一个只读的用于创建docker容器(container)的指令模板,包括了程序运行所需的全部依赖包(包括程序、库、资源、配置等)。通常,镜像基于另一个镜像,并进行自定义。镜像是只读的、可运行的,运行后的镜像即为容器(Container)。 1.1. 镜像的几个概念 通过docker images查询本地的镜像列表时,可以看到镜像有REPOSITORY、ID、TAG等字段,镜像包含几个概念,首先需要弄清楚它们间的关系: Registry 镜像注册表,用来存储镜像数据的地方,官方的Docker hub就是一个公共的Registry,另外,还可以通过官方的registry镜像搭建私有的镜像注册表。通常所说的镜像仓库是泛指Registry,但并不完全准确,一个Registry可以包含多个Repository。 例如,拉取镜像:docker pull registry.hub.docker.com/ubuntu:18.04,这里的registry.hub.docker.com就是官方提供的镜像注册表,可以省略不写。 Repository 镜像库,包含多个镜像,存储于Registry中。在仓库搜索镜像时,按名称搜索在registry中查找repository。例如,我们所说的nginx镜像,一般就是指的nginx的Repository,它包含多个nginx镜像,它们通过tag来区分。 Tag 镜像的标签,一般用来作为版本区分,默认不写Tag为latest,一个Image有多个Tag。 Image 具体的镜像,有唯一的GUID和多个Tag。 简单而言,一个Registry包含多个Repository,每个Repository包含多个Image,每个Image有多个Tag,但是ID是唯一的。 1.2. 镜像层 镜像由不同的层(layer)组成。一般而言,Dockerfile每一个指令都会创建一个层,每层只是与之前图层的一组差异,它们都有自己的GUID,并且堆叠在彼此之上。当创建新容器时,将会在基础层的顶部添加新的可写层,称为“容器层”,对正在运行的容器所做的所有更改(例如,写入新文件、修改现有文件和删除文件)都将写入此可写容器层。 Figure 1. 镜像层示意 以镜像ubuntu15.04为例,运行一个容器,下图展示了其基本结构: Figure 2. 基于ubuntu15.04镜像的容器结构 上图展示了一个容器层(container layer)和镜像的多个镜像层(image layer)。其实,容器和镜像最主要的区别就在于容器层,容器层可读写,新写入或者修改的数据都存储在容器层上。基于同一镜像的不同容器都有自己的不同容器层,但是对于底层的各个镜像层,各个容器是共享的。当容器被删除后,只需删除该容器的容器层即可。可以看到,Docker通过层的设计极大地复用了资源,这也是docker轻量和快速的主要原因。 Figure 3. 基于ubuntu15.04镜像创建的多个容器结构 2. 镜像基本操作 接下来,我们看看镜像的一些基本操作。 2.1. 拉取镜像 拉取镜像意思是从镜像的Registry中将镜像下载到本地,类似于Git的pull拉取代码,命令如下: docker image pull [OPTIONS] NAME[:TAG|@DIGEST] 选项包括: -a, --all-tags: 拉取镜像的所有标签 --disable-content-trust: 是否跳过镜像验证,默认为true 例如:docker pull registry.hub.docker.com/ubuntu:18.04 2.2. 镜像查询 1、查询列表 查询本地的所有镜像: docker image ls 等同于docker images,例如: root@ubuntu:~# docker images REPOSITORY TAG IMAGE ID CREATED SIZE local/mynginx latest 4d24e58d851d 26 hours ago 108MB python 1.0 22a7b6b93718 5 days ago 131MB nginx latest f68d6e55e065 2 weeks ago 109MB python 2.7-slim ca96bab3e2aa 5 weeks ago 120MB hello-world latest fce289e99eb9 6 months ago 1.84kB ubuntu 14.10 a8a2ba3ce1a3 4 years ago 194MB ...

2019-07-23 · 6 min · 1111 words · Hank

Docker入门系列二——安装

在上一篇 Docker简介中,我们介绍了Docker的概念、优势以及Docker结构,也提到了Docker是基于Linux系统的cgroup技术实现的,既然如此,那么Docker是否支持非Linux系统上使用呢?答案是肯定的。Docker官方提供了Docker Machine工具,用于在非Linux系统上构建虚拟机并在其上安装Docker;Docker官方还提供了Docker Toolbox工具包,用来在旧版本的操作系统上安装Docker;另外,Docker还专门针对Windows和Mac OS,提供了更简单易用的Docker Desktop,用来一键安装Docker及其相关组件。\n 在本篇,我们将来看看如何在Linux、Mac OS和Windows上安装Docker。\n 本篇安装的docker版本在最新的18.09版本,后续的docker入门系列文章都是基于该版本。 1. Ubuntu上安装Docker 1.1. 系统要求 Docker要求安装与64位的Ubuntu,版本要求如下: Cosmic 18.10 Bionic 18.04 (LTS) Xenial 16.04 (LTS) Docker支持的架构包括:x86_64 (或amd64), armhf, arm64, s390x (IBM Z), and ppc64le (IBM Power)。 测试Linux服务器为Ubuntu,版本为16.04,Ubuntu发布代码为xenial,其他版本的Linux就不做实践了: root@ubuntu:~# cat /etc/lsb-release DISTRIB_ID=Ubuntu DISTRIB_RELEASE=16.04 DISTRIB_CODENAME=xenial DISTRIB_DESCRIPTION="Ubuntu 16.04.2 LTS" 1.2. 准备工作 1、卸载旧版本 如果原来安装过旧版本的docker,需要先卸载之: $ sudo apt-get remove docker docker-engine docker.io containerd runc 旧版本的docker在Ubuntu的镜像仓库中名为docker.io,docker-io包是Debian/Ubuntu用于官方docker发布的名称,不叫docker的原因在于,避免与Ubuntu的docker system-tray二进制文件发生名称冲突。在新版本的docker中,docker已经区分为docker-ce和docker-ee版本,在安装新版本的docker时,需要先卸载旧版本docker。 2、添加仓库地址 安装前,需要先将docker官方安装地址加入到Ubuntu apt地址库中。 (1)更新package索引 apt-get update ...

2019-07-12 · 2 min · 412 words · Hank

Docker入门系列一——简介

1. 简介 容器化技术,是对Linux容器的一种封装,将程序打包为镜像文件,运行该文件创建容器,最终达到使用容器进行快速、简单、高效的开发、部署、运行软件程序的目的。相对于虚拟化技术 [1],容器化技术有着更高的资源利用率、更高的性能、更低的开发和维护成本。 容器化技术有以下特点: 灵活性:绝大部分应用程序都可以使用容器化技术进行开发和运维,并且可及时部署和更新应用 轻量级:共享宿主机内核资源,仅将程序和其依赖打包到镜像文件,运行于容器,占用资源少、性能高 可移植:一次构建,到处运行;本地一次构建,然后可部署到各个平台或云端docker环境中,强移植性 可扩展:可以自定义镜像文件,扩展功能满足不同需要 可堆叠:您可以垂直和即时堆叠服务 Docker就是一种容器化技术。 Docker是一个开发、运输和运行应用程序的开放平台,基于Linux内核的 cgroup [2] 技术实现,根据开源Apache 2.0许可证授权。使用Docker,您可以将应用程序与基础架构分离,以便快速交付软件,并且像管理应用程序一样管理基础架构。通过Docker快速发布、测试和部署代码,可以显着减少编码和部署程序的工作量,大大提高工作效率。 Figure 1. Docker场景示意图 1.1. 容器和虚拟机 容器运行在宿主机并且和其他容器共享宿主机内核资源,运行于独立的进程,不消耗更多内存及CPU资源,这使得容器很轻量级。 虚拟机也是运行在宿主机上,但是虚拟机有自己完全独立的操作系统,通过虚拟管理程序与宿主机交换资源,各个虚拟机共用宿主机的资源很少,导致虚拟机占用资源高、资源利用率低。 Figure 2. docker容器化技术结构 Figure 3. 虚拟化技术结构 如图2所示,Docker下层是其基础服务层,上层是docker容器,多个容器之间相互隔离,docker共享宿主机(Host OS)的系统资源。 而虚拟化技术结构如图3所示,底层是hypervisor [3] 层,用来协调多个虚拟机(VM)和宿主机(物理机)的资源共享,上层的多个虚拟机都有单独的操作系统,他们之间不进行资源共享。 不难看出,虚拟机提供给运行其上的程序过量的资源,资源利用不充分,性能低;而容器技术,很好的解决了这一问题。 1.2. Docker可以做什么 1、快速,一致地交付您的应用程序 Docker允许开发人员使用提供应用程序和服务的本地容器在标准化环境中工作,从而简化了开发生命周期。容器非常适合持续集成和持续交付(CI / CD)工作流程。 2、响应式部署和扩展 Docker基于容器的平台允许高度可移植的工作负载。Docker容器可以在开发人员的本地笔记本电脑,数据中心的物理或虚拟机,云提供商或混合环境中运行。 Docker的可移植性和轻量级特性还使得可以轻松地动态管理工作负载,按照业务需求即时扩展或拆除应用程序和服务。 3、在同一硬件上运行更多工作负载 Docker轻巧而快速。它为基于管理程序的虚拟机提供了一种可行且经济高效的替代方案,因此您可以使用更多的计算容量来实现业务目标。Docker非常适合高密度环境以及需要用更少资源完成更多工作的中小型应用部署,譬如现非常流行的微服务架构。 2. Docker核心概念 Docker有三大核心概念,分别是镜像、容器和仓库。 Docker镜像 Docker镜像(Image),一个镜像是一个只读的用于创建docker容器(container)的指令模板。通常,镜像基于另一个镜像,并进行自定义。一般而言,通过定义Dockerfile文件来创建镜像。 Docker容器 Docker容器(Container),容器是镜像的运行实例,Docker的容器间是相互隔离的,可以将容器看做是简化版的Linux系统和运行在其中的应用打包而成的沙箱。您可以使用Docker API或CLI创建、启动、停止、移动或删除容器,也可以将容器连接到一个或多个网络,附加数据卷,甚至可以根据其当前状态创建新镜像。 Docker仓库 Docker仓库指的是镜像文件存储的地方。Docker还有一个镜像注册表(Registry)的概念,它是存放镜像仓库的地方,有许多的仓库存放于其中。仓库有公共仓库和私有仓库之分,最大的公共仓库是Docker hub,用户也可以搭建自有的私有镜像仓库。用户创建了镜像过后,通过push命令将其推送到镜像仓库中,使用时再从镜像仓库pull到本地运行,这设计类似于Git的代码仓库。 3. Docker结构 Figure 4. docker的整体结构 Docker整体结构如图4所示。一般而言,我们所说的docker,均指docker引擎(Docker Engine)。 3.1. Docker引擎 Docker Engine是一个客户端 - 服务器架构的应用程序,我们常说的docker指的就是docker engine,它包含以下主要组件: 服务器,是一种长时间运行的程序,称为守护进程( dockerd命令) REST API,程序与守护进程通信的接口 CLI,命令行界面,即docker客户端(docker命令),负责接收用户指令并通过REST API请求守护程序,完成与守护进程交互 ...

2019-07-03 · 1 min · 203 words · Hank