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 · 老墨

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 · 老墨

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 · 老墨

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 · 老墨

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 · 老墨

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 · 老墨

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 · 老墨

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 · 老墨

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 · 老墨

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 · 老墨

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 · 老墨

给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 · 老墨

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

分布式系统,与传统单体架构系统相比,其结构复杂的多,系统间靠网络传输数据,系统存在诸多不确定因素,如硬件故障、网络波动等,都会影响整个系统的稳定性。而分布式事务更是分布式系统的一大难题,本篇将讨论业界分布式事务的几种常见解决方案。 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 · 老墨

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 · 老墨

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 · 老墨

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 · 老墨

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 · 老墨

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 · 老墨

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 · 老墨

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 · 老墨

修改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 · 老墨

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 · 老墨

使用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 · 老墨

使用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 · 老墨

使用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 · 老墨

使用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 · 老墨

使用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 · 老墨

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 · 老墨

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 · 老墨

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 · 老墨

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 · 老墨

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 · 老墨

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 · 老墨

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 · 老墨

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 · 老墨

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 · 老墨

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 · 老墨

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 · 老墨

使用Github的Webhook实现代码库与应用的交互

很早之前,博主的的网站评论模块已经更改为了 gitalk,Gitalk是一个基于GitHub Issue和Preact的现代评论组件。更换评论组件后有一个问题:我不想每次去github上查看别人的评论,如果别人评论了文章,然后我能够在网站后台看到,这样就很方便了。此时,github的webhook功能就可以登场了。 1. 简介 Webhook,翻译过来可以称为网络钩子,用来将Github上的一系列事件信息回传到某一回调地址上,从而完成与外部应用的交互,它是github提供的一种与外部交互的入口。Github上提供了很多交互事件,当某一事件被触发后,如果设置了Webhook的回调地址,Github将会通过HTTP POST请求将事件信息发送到回调地址上,回调处理应用通过接收事件信息然后实现自身的业务需求。 目前,每个组织或代码库上最多只能创建20个webhook。 一个典型的业务场景是:代码保存在github上,如果稳定的master分支上提交了代码,就触发持续集成系统如Jenkins进行代码构建、打包、部署等系列操作。 2. 事件 配置webhook时,可以选择订阅的事件。一般情况下,我们只需要订阅关注的事件,github也支持一个匹配所有支持事件的通配符(*),添加通配符事件时,github将使用通配符事件替换您配置的任何现有事件,并为所有支持的事件发送有效负载。如果将来添加了新的可匹配的事件,那么将会自动订阅。您可以随时通过API或UI更改订阅事件列表。默认情况下,webhooks仅订阅push事件。 每个事件对应于您的组织和/或存储库可能发生的某组操作。例如,如果您订阅了问题事件,则每次打开,关闭,标记等issue时您都会收到详细的有效负载。github现支持的事件列表见 #events[附录一]。 2.1. 有效负载 有效负载,简单理解就是消息所携带的内容和信息。 每种事件类型都具有特定的有效负载格式以及相关的事件信息,事件的有效负载来源于具体的事件类型的有效负载,但原始push事件除外,它的具有更详细的webhook有效负载。 除了为每个事件记录的字段之外,webhook有效负载还包括执行事件的用户(发送者)、组织和事件发生的存储库。GitHub app的webhook有效负载还可能包括安装事件。 一个marketplace_purchase事件类型的有效负载的示例如下: { "action":"purchased", "effective_date":"2017-10-25T00:00:00+00:00", "sender":{ "login":"username", "id":3877742, "avatar_url":"https://avatars2.githubusercontent.com/u/3877742?v=4", "gravatar_id":"", "url":"https://api.github.com/users/username", "html_url":"https://github.com/username", "followers_url":"https://api.github.com/users/username/followers", "following_url":"https://api.github.com/users/username/following{/other_user}", "gists_url":"https://api.github.com/users/username/gists{/gist_id}", "starred_url":"https://api.github.com/users/username/starred{/owner}{/repo}", "subscriptions_url":"https://api.github.com/users/username/subscriptions", "organizations_url":"https://api.github.com/users/username/orgs", "repos_url":"https://api.github.com/users/username/repos", "events_url":"https://api.github.com/users/username/events{/privacy}", "received_events_url":"https://api.github.com/users/username/received_events", "type":"User", "site_admin":true, "email":"username@email.com" }, "marketplace_purchase":{ "account":{ "type":"Organization", "id":18404719, "login":"username", "organization_billing_email":"username@email.com" }, "billing_cycle":"monthly", "unit_count":1, "on_free_trial":false, "free_trial_ends_on":null, "next_billing_date":"2017-11-05T00:00:00+00:00", "plan":{ "id":435, "name":"Basic Plan", "description":"Basic Plan", "monthly_price_in_cents":1000, "yearly_price_in_cents":10000, "price_model":"per-unit", "has_free_trial":true, "unit_name":"seat", "bullets":[ "Is Basic", "Because Basic " ] } } } ...

2019-07-02 · 2 min · 408 words · 老墨

Spring AMQP消息转换

上一篇,我们介绍了如果使用Spring AMQP注解来实现消息发送和监听,示例都是使用的默认的消息转换器,即SimpleMessageConverter,它只能处理byte[]、String、java序列化对象(实现了Serializable接口的对象)。 通常,不推荐使用Java序列化,因为它存在与Java对象强耦合、依赖java语言等缺点,Spring AMQP也提供了其他的消息转换方式,在本篇,我们将重点来看看如果将消息序列化为JSON格式。 1. MessageConverter Spring AMQP消息转换定义了顶层接口MessageConverter,它的定义如下: public interface MessageConverter { // 将对象转换为Message对象,支持自定义消息属性 Message toMessage(Object object, MessageProperties messageProperties) throws MessageConversionException; // 将Message转换为对象 Object fromMessage(Message message) throws MessageConversionException; } 它定义了两个方法:将对象转换为Message,将Message转换为对象。 同时,在AmqpTemplate中定义了便捷的消息转换和发送的方法: void convertAndSend(Object message) throws AmqpException; void convertAndSend(String routingKey, Object message) throws AmqpException; void convertAndSend(String exchange, String routingKey, Object message) throws AmqpException; void convertAndSend(Object message, MessagePostProcessor messagePostProcessor) throws AmqpException; void convertAndSend(String routingKey, Object message, MessagePostProcessor messagePostProcessor) throws AmqpException; void convertAndSend(String exchange, String routingKey, Object message, MessagePostProcessor messagePostProcessor) throws AmqpException; ...

2019-06-17 · 3 min · 488 words · 老墨

Spring AMQP注解的使用

上一篇 "Spring AMQP简介和使用",我们介绍了Spring AMQP的一些基本要素和概念,也通过一些示例代码介绍了消息的发送和接收,但是都是使用的原始编码方式来实现,并不依赖Spring环境。其实,Spring AMQP也支持使用注解的方式来进行异步接收消息,极大的简化了编码。 1. hello world 要使用注解,首先需要在Spring应用环境中,我们看一个最简单的demo: 1、重新新建一个Spring boot工程,添加如下依赖: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.amqp</groupId> <artifactId>spring-amqp</artifactId> <version>1.7.6.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.amqp</groupId> <artifactId>spring-rabbit</artifactId> <version>1.7.6.RELEASE</version> </dependency> 2、新建一个Spring配置类RabbitConfiguration,用来申明Bean: @Configuration public class RabbitConfiguration { public static final String ANONYMOUS_QUEUE_NAME = "spring.amqp.anonymous.queue"; @Bean public ConnectionFactory connectionFactory() { CachingConnectionFactory cachingConnectionFactory = new CachingConnectionFactory("192.168.0.27", 5672); cachingConnectionFactory.setUsername("admin"); cachingConnectionFactory.setPassword("123456"); return cachingConnectionFactory; } @Bean public AmqpAdmin amqpAdmin() { return new RabbitAdmin(connectionFactory()); } @Bean public RabbitTemplate rabbitTemplate() { return new RabbitTemplate(connectionFactory()); } @Bean public Queue anonymousQueue() { // 匿名队列 return new AnonymousQueue(() -> ANONYMOUS_QUEUE_NAME); } } ...

2019-06-13 · 5 min · 990 words · 老墨

Spring AMQP简介和使用

1. 前言 很早之前写过几篇关于RabbitMQ的一些基础文章,在本篇中,我们将来学习Spring AMQP。 Spring AMQP 是 Spring 对 AMQP( http://www.amqp.org) 协议的封装和扩展,它提供了模板来对消息的发送和接收进行高级抽象,还提供了基于消息驱动的POJO的支持(类似JMS,Java消息服务)。 在开始之前,首先需要了解一些 RabbitMQ 和 AMQP 的一些基础概念,并在你的机器上安装 RabbitMQ, 本文使用的spring-amqp的版本为1.7.6。 2. Spring AMQP抽象 Spring AMQP 是 Spring 对 AMQP 协议的封装和扩展,提供了消息发送和接收的模板。Spring AMQP项目将核心Spring概念应用于基于 AMQP 的消息传递解决方案的开发,以便更容易和简单的管理AMQO资源。 Spring AMQP由spring-amqp和spring-rabbit两个模块组成。spring-amqp模块位于org.springframework.amqp.core包,它的目标是提供不依赖于任何特定AMQP代理实现或客户端库的通用抽象;而spring-rabbit是spring-amqp通用抽象的具体实现,目前仅提供了rabbitmq的实现。 Spring AMQP包括一些基本的抽象定义(上边说过,他们位于org.springframework.amqp.coreb包中,而非AMQP协议本身定义): 2.1. Message 在0-9-1版本的 AMQP 规范中没有定义 Message 类或接口,当执行诸如 basicPublish() 操作时,内容作为字节数组参数传递,而其他属性作为单独的参数传递。Spring AMQP将 Message 类定义为更通用的AMQP域模型表示的一部分。Message该类的目的是将主体和属性封装在单个实例中,以便API可以更简单。以下示例显示了Message类定义: public class Message { private final MessageProperties messageProperties; private final byte[] body; public Message(byte[] body, MessageProperties messageProperties) { this.body = body; this.messageProperties = messageProperties; } public byte[] getBody() { return this.body; } public MessageProperties getMessageProperties() { return this.messageProperties; } } ...

2019-06-05 · 8 min · 1703 words · 老墨

Spring boot集成quartz并实现任务调度

最近在实现任务调度服务,使用 quartz调度框架,由于使用的spring boot版本为1.5.10,该版本并没有提供quartz的starter,在集成的时候还是遇到了很多问题。本文并不会详细介绍quartz框架,请自行查阅相关资料。如果对Spring boot不太熟悉,可以看前边几篇关于spring boot的文章。 1. quartz简介 1.1. 核心概念 scheduler:任务调度器,相当于指挥中心,负责管理和调度job,由SchedulerFactory创建和关闭,创建后需要通过starter()方法来启动调度。 trigger:触发器,定义job的触发时机,通过TriggerBuilder来创建,有SimpleTrigger和CronTrigger,前者可以定义在某一个时刻触发或者周期性触发,后者使用cron表达式来定义触发时间。 Job:具体的调度业务逻辑实现,也就是任务,具体任务需要实现org.quartz.Job接口。 JobDetail:org.quartz.JobDetail用来描述Job实例的属性,并且可以通过JobDataMap传递给Job实例数据,通过JobBuilder来创建。 1.2. quartz配置 quartz框架默认会通过quartz.properties文件来加载配置信息,有几个重要的配置项: org.quartz.scheduler.*:调度器相关配置 org.quartz.threadPool.*:线程池相关配置 org.quartz.jobStore.*:quartz数据存储相关配置,在quartz中,有三种存储类型: RAMJobStore:基于内存存储job JobStoreCMT:基于数据库的数据存储,并且受运行的java容器的事务控制 JobStoreTX:基于数据库的数据存储,不受事务控制 org.quartz.dataSource.*:配置数据库存储quartz数据时的数据源信息 org.quartz.plugin.*:quartz插件相关配置 quartz简单介绍这么多,更多信息请看 这里。 2. Spring boot与quartz集成 前边说过,我使用的spring boot版本为1.5.10,没有quartz的starter,所以需要自己编码实现。同时,我还需要对quartz做持久化处理,需要在mysql库中导入quartz的建表脚本。集成步骤如下: 1、引入依赖 引入quartz的依赖包,quartz-jobs根据实际需要决定是否引入。 <dependency> <groupId>org.quartz-scheduler</groupId> <artifactId>quartz</artifactId> <version>2.2.3</version> </dependency> <dependency> <groupId>org.quartz-scheduler</groupId> <artifactId>quartz-jobs</artifactId> <version>2.2.3</version> </dependency> 2、创建表 在下载的quartz包中有建表脚本,我的为quartz-2.2.3,建表脚本在/docs/dbTables下边,导入tables_mysql_innodb.sql即可。 3、配置文件 在resources目下新建一个quartz.properties配置文件,我的配置如下: #============================================================================ # Configure Main Scheduler Properties #============================================================================ org.quartz.scheduler.instanceName:event-scheduler org.quartz.scheduler.instanceId:AUTO org.quartz.scheduler.skipUpdateCheck:true #============================================================================ # Configure ThreadPool #============================================================================ org.quartz.threadPool.class:org.quartz.simpl.SimpleThreadPool org.quartz.threadPool.threadCount:5 org.quartz.threadPool.threadPriority:5 #============================================================================ # Configure JobStore #============================================================================ org.quartz.jobStore.misfireThreshold:60000 org.quartz.jobStore.class:org.quartz.impl.jdbcjobstore.JobStoreTX org.quartz.jobStore.driverDelegateClass:org.quartz.impl.jdbcjobstore.StdJDBCDelegate org.quartz.jobStore.useProperties:false org.quartz.jobStore.tablePrefix:QRTZ_ org.quartz.jobStore.isClustered:false #============================================================================ # Configure Datasources #============================================================================ org.quartz.dataSource.ds.maxConnections:7 #============================================================================ # Configure Plugins #============================================================================ org.quartz.plugin.triggHistory.class: org.quartz.plugins.history.LoggingTriggerHistoryPlugin ...

2019-01-22 · 4 min · 647 words · 老墨

Spring boot工程Feign请求时Get请求无法传递body的问题

前一篇,我们解决了Spring的RestTemplate在发送Get请求不能传递body,相比RestTemplate,我们更多的还是采用更方便的申明式服务调用框架Feign。本篇不会介绍Feign,而是解决Feign发送Get请求时仍然不能传递body的问题。 1. 准备 在这里,我使用的Spring boot的版本为1.5.10.RELEASE,Spring Cloud版本为Edgware.SR3。 准备三个服务:注册中心、服务生产者、服务消费,这里我们继续使用在 Spring Cloud服务注册中心Eureka定义的三个服务:服务注册中心01-eureka-server、服务提供者01-service-demo,同时新建一个服务消费者01-feign-consumer,使用feign来请求服务而不是ribbon,记得在启动类加上@EnableFeignClients启用Feign。 1、在服务提供者01-service-demo添加一个接口,代码如下: @GetMapping("/hello/user") public String sayHello(@RequestBody User user) { return "hello, " + user.getName() + ", give you a gift " + user.getGift(); } 这样,Get请求需要接收body数据。 2、在服务消费者01-feign-consumer编写如下代码: (1)在SayHelloClient中添加如下声明式服务请求: @FeignClient("hello-service") public interface SayHelloClient { @GetMapping(value = "/hello/user") String sayHello(@RequestBody User user); } (2)在SayHelloController中调用SayHelloClient: @RestController public class SayHelloController { private static Logger log = LoggerFactory.getLogger(SayHelloController.class); @Autowired private ApiProductClient apiProductClient; @GetMapping("/user") public String userHello() { User user = new User(); user.setName("lily"); user.setGift("birthday card"); return sayHelloClient.sayHello(user); } } ...

2019-01-22 · 1 min · 125 words · 老墨

使用Spring的RestTemplate发送GET请求,并支持传递Request body参数

最近在使用Spring Boot实现微服务,都是使用RESTful风格的Api接口,服务间使用RestTemplate来进行HTTP通信,遇到这样一个需求:开发一个查询请求Api,参数使用JSON格式的字符串来提交。 1. 请求格式 希望的请求格式如下: GET /pointCard/ HTTP/1.1 Host: localhost:8100 Content-Type: application/json;charset=UTF-8 Content-Length: 114 {"iColumns":7,"iDisplayLength":10,"iDisplayStart":0,"iSortingCols":0,"sColumns":"","sEcho":1,"subjectId":"11227"} 在RESTful下,这样的设计是合理的,GET请求表示从服务器获取资源,但需要将查询参数以JSON格式来提交。但是,这违背了传统的GET请求的规范,我们都知道,GET请求只能将请求参数拼接URI后边,而不能单独传递request body参数,除非你改用POST。 2. 代码实现 我们先来编一个上述请求的API,然后进行测试。 1、编写一个API: @GetMapping(value = "/") public Response getById(@RequestBody @Valid PointCardQuery query) throws Exception { Assert.notNull(query,"查询条件不能为空!"); …… return Response.success(pointCardPurePager, "积分卡获取成功!"); } 上边的代码片段处于一个Restcontroller,要求使用GET方法,并且使用了@RequestBody注解来获取request body参数。 2、我们使用RestTemplate来测试一下: @Test public void testGetWithBody() { RestTemplate restTemplate = new RestTemplate(); String p = "{\"iColumns\":7,\"iDisplayLength\":10,\"iDisplayStart\":0,\"iSortingCols\":0,\"sColumns\":\"\",\"sEcho\":1,\"subjectId\":\"11227\"}"; String url = "http://localhost:8100/pointCard/"; HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON)); HttpEntity<String> httpEntity = new HttpEntity<>(p, headers); ResponseEntity<String> responseEntity = restTemplate.exchange(url, HttpMethod.GET, httpEntity, String.class); String body = responseEntity.getBody(); System.out.println(body); System.out.println(responseEntity.getStatusCode()); System.out.println(responseEntity.getStatusCodeValue()); System.out.println(responseEntity); } ...

2018-10-31 · 2 min · 341 words · 老墨

Spring Boot参数验证(下)——Bean Validation在Web中的应用

在 Spring Boot参数验证(上)--Bean Validation及其Hibernate实现 一篇中,我们介绍了验证标准Bean Validation和其Hibernate实现,在本篇,我们看看它们是如何应用在Spring Boot Web项目中。 1. Spring Validator 其实,Spring很早就有了自己的Bean验证机制,其核心为Validator接口,表示校验器: public interface Validator { // 检测Validator是否支持校验提供的Class boolean supports(Class<?> clazz); // 校验逻辑,校验的结果信息通过errors获取 void validate(@Nullable Object target, Errors errors); } Errors接口,用以表示校验失败的错误信息: public interface Errors { // 获取被校验的根对象 String getObjectName(); // 校验结果是否有错 boolean hasErrors(); // 获取校验错误数量 int getErrorCount(); // 获取所有错误信息,包括全局错误和字段错误 List<ObjectError> getAllErrors(); // 获取所有字段错误 List<FieldError> getFieldErrors(); …… } 当Bean Validation被标准化过后,从Spring3.X开始,已经完全支持JSR 303(1.0)规范,通过Spring的LocalValidatorFactoryBean实现,它对Spring的Validator接口和javax.validation.Validator接口进行了适配。 1.1. 全局Validator 全局Validator通过上述的LocalValidatorFactoryBean类来提供,只要使用@EnableWebMvc即可(Xml配置开启<mvc:annotation-driven>),也可以进行自定义: @Configuration @EnableWebMvc public class WebConfig extends WebMvcConfigurerAdapter { @Override public Validator getValidator(); { // return "global" validator } } ...

2018-10-12 · 3 min · 624 words · 老墨

Spring boot全局异常处理和自定义异常页面

Spring boot提供了默认的异常处理机制,但是难以满足业务需要,一般需要编码来实现自己的业务处理机制。在本篇,将介绍如何自定义异常页面,和进行全局的异常处理。 1. 全局异常处理 如果系统业务处理发生异常,我们希望能够将异常信息以友好的形式返回给调用方,而不是仅在后台记录日志,尤其是在开发RESTFul的API时,需要将业务异常信息进行明确定义并返回给API调用方,这显得尤为重要。 现在,我们来定义一个全局的业务异常类BusinessException,如果业务处理失败抛出该类或者其子类,然后编写一个全局异常处理器,将异常转换为有好的信息并返回。 1、定义异常类 public class BusinessException extends Exception { public BusinessException() { super(); } public BusinessException(String msg) { super(msg); } public BusinessException(Throwable cause) { super(cause); } public BusinessException(String message, Throwable cause) { super(message, cause); } public BusinessException(MsgDefinition msgDefinition) { super(msgDefinition.msgOf()); this.msgDef = msgDefinition; } public MsgDefinition msgDef() { return msgDef == null ? MsgDefinition.UNKOWN_ERROR : msgDef; } } 其中的MsgDefinition为具体的错误信息,包含错误码code和错误提示信息msg,子类继承该类进行扩展: public class MsgDefinition { public static final MsgDefinition SUCCESS = new MsgDefinition("0000", "请求成功"); public static final MsgDefinition EMPTY_ARGUMENTS = new MsgDefinition("4001", "请求参数为空"); public static final MsgDefinition ILLEGAL_ARGUMENTS = new MsgDefinition("4002", "请求采参数非法"); public static final MsgDefinition FILE_SIZE_OVER_LIMIT = new MsgDefinition("4301", "文件大小超过限制"); public static final MsgDefinition FILE_NUMBER_OVER_LIMIT = new MsgDefinition("4302", "文件数量超过限制"); public static final MsgDefinition FILE_FORMAT_UNSUPPORTED = new MsgDefinition("4310", "文件格式不支持"); public static final MsgDefinition UNKOWN_ERROR = new MsgDefinition("9999", "系统未知异常"); private String code; private String msg; public MsgDefinition(String code, String msg) { this.code = code; this.msg = msg; } public String codeOf() { return this.code; } public String msgOf() { return this.msg; } @Override public String toString() { return JsonUtil.toJson(this); } } ...

2018-10-10 · 2 min · 383 words · 老墨

Spring Boot参数验证(上)——Bean Validation及其Hibernate实现

通常,web项目都需要对请求的参数进行校验,一般是前端JavaScript需要校验,然后后台还需要再进行校验,而校验逻辑大多数是重复的。如果在后台通过硬编码的方式,对Controller参数进行逐个校验,将会非常耗时和低效的,尤其是在使用Spring Boot和Spring Cloud的微服务体系中,大多数服务均会对外提供RESTFul的api,如果有统一的验证机制和框架,将大大提高生产力。幸运的是,Java已经提供了标准的验证API,称为Bean Validation。 1. 简介 1.1. Bean Validation Bean Validation,是JCP(Java Community Process)定义的标准化的JavaBean校验API,基于注解,并且具有良好的易用性和扩展性,1.0版本定义为 /attachment/20181009/28f52319fd84452ba22b490ff911fd04.pdf[JSR 303],而现在发布了2.0版本,定义为 JSR 380,了解详细信息可以看 这里。 Bean Validation并不局限于应用程序的某一层或者哪种编程模型, 它可以被用在任何一层, 也可以是像Swing这样的富客户端程序中. 目标:简化Bean校验,将以往重复的校验逻辑进行抽象和标准化,形成统一API规范; 版本变化:JSR 303在2009发布了1.0Final版,而最新的是在2017年发布的Bean Validation 2.0,被定义为 JSR 380。 需要注意的是,Bean Validation只是一个规范和标准,并没有提供实现,而接下来介绍的hibernate validator就是它的一种实现。 1.2. Hibernate Validator 是JSR 380的一种标准实现,同时还对其进行了扩展,如增加了部分验证约束。目前,最新的稳定版本为 6.0.13.Final。 2. 入门示例 接下来,我们使用Spring Boot来编写一个简单的示例工程。如果对Spring Boot不熟悉的可以看这几篇文章: "Spring Boot之基础入门" "SpringBoot-工程结构、配置文件以及打包" "Spring Boot JPA使用详解" 1、添加maven依赖 前边说过,JSR 303定义了JavaBean的验证标准,而Hibernate Validator是它的一种实现,所以,这两个的jar包我们都需要添加到工程。 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> 其实,spring-boot-starter-web启动器已经依赖了hibernate-validator,而后者很明显会依赖validation-api。 ...

2018-10-10 · 8 min · 1686 words · 老墨

Spring Cloud服务注册中心Eureka高可用

在 上一篇,我们通过编码,分别编写了服务注册中心、服务提供者、服务消费者,但是服务注册中心仍然是单节点,如果服务注册中心不可用,则会导致整个体系的所有服务都不可用。所以,有必要保证服务注册中心的高可用,本章将来讨论整个问题。 1. Eureka集群 对于Eureka而言,其本身是支持多节点集群的。其原理大致如下:服务注册中心的多个实例彼此互相注册,形成注册中心的集群,同时对外提供服务,即使其中一个挂掉,其他实例仍然可以对外提供服务,从而保证了高可用。 Figure 1. Eureka双节点结构示意图 以双节点为例,如上图所示,两个服务注册中心(Eureka Server)彼此相互注册形成集群,服务提供者(Service Provider)会自动向两者都进行服务注册,即是说,两个服务注册中心有用相同的服务实例列表,服务消费者(Service Consumer)会采用负载均衡算法从服务注册中心集群中选择出一个可用的服务注册中心,来获取服务注册实例,并完成请求。 上一篇已经说过,默认情况下,每一个Eureka服务端同样也是一个Eureka客户端,因此至少需要配置一个其他的Eureka服务端URL来让自己完成注册。如果不提供这个服务端URL,服务端本身可以正常运行和提供服务,但是控制台会打印许多无法成功注册的日志信息。在上一篇在编码单节点服务注册中心时,我们做了如下配置,让其不作为客户端让其他服务注册中心注册: eureka.client.register-with-eureka=false eureka.client.fetch-registry=false 这两项配置默认均为true,即是说eureka默认是开启集群功能的。 2. 编码实战 接下来,我们参照上一篇的服务注册中心、服务提供者、服务消费者代码,来组建一个双节点的服务注册中心,并通过服务消费者获取服务实例,完成远程请求。 2.1. 服务注册中心 1、新建一个名为02-eureka-server-nodes的Spring Boot工程,作为服务注册中心 2、引入如下依赖和插件: <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka-server</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> 引入spring-boot-maven-plugin的目的是为了打可执行jar包,由于要组建双节点集群,需要启动两次,所以不能直接在idea里边运行工程了,需要打包成可执行jar,通过命令行来启动。 3、启动类添加@EnableEurekaServer注解,表明该工程为Eureka服务端 4、配置多环境,新建三个配置文件:application.properties、application-node1.properties、application-node2.properties,分别进行如下配置: application.properties: spring.application.name=eureka-server # 开发用,关闭服务端自我保护,防止关闭的实例不能被剔除 eureka.server.enable-self-preservation=false application-node1.properties: server.port=8080 eureka.instance.hostname=eureka-node1 # 服务注册中心访问地址 eureka.client.service-url.defaultZone=http://eureka-node2:8081/eureka application-node2.properties: server.port=8081 eureka.instance.hostname=eureka-node2 # 服务注册中心访问地址 eureka.client.service-url.defaultZone=http://eureka-node1:8080/eureka 需要注意的是,eureka-node1需要向eureka-node2来注册服务,而eureka-node2需要向eureka-node1来注册服务,彼此交叉注册,注意配置地址的不同。 5、由于在本机测试,并且这里通过hostname来访问服务,所以需要让hostname可以访问,修改hosts文件进行本机映射: 127.0.0.1 eureka-node1 127.0.0.1 eureka-node2 各操作系统修改hosts文件的方式不同,请自行查阅相关资料。 6、到这里编码工作就已完成,现在,使用maven命令进行打包: ``mvn clean package`` ...

2018-08-16 · 1 min · 202 words · 老墨

Spring Cloud服务注册中心Eureka

上一篇,我们学习了Spring Cloud配置集成化组件Config,在本篇,我们来学习Spring Cloud微服务架构的一个核心组件----Eureka。 1. 服务治理 服务治理,即将体系中各个微服务统一纳入服务注册中心,由注册中心维护各个微服务的名称、地址、端口、状态等信息,并提供服务自动化注册和发现机制。 为什么需要服务治理? 对于传统应用而言,应用数量少,各应用间的API调用我们可以通过在配置文件中写入IP地址和端口的方式来进行维护。那么对于现在的微服务架构而言,应用的服务数量上百甚至上千,如果还使用传统的配置方式来维护各个服务信息及其其调用管理,那么无疑是困难重重,不仅容易出错,而且成本直线上升。 <p style="text-align: center;">没有服务治理时,如何管理各个服务? <p style="text-align: left;">在微服务体系中,服务实例的网络位置都是动态分配的,而且实例的地址可能会经常性的改变,有了服务治理,不再需要人为的维护各个服务信息,一切交给服务注册中心来自动管理和维护,大大提高了效率。 几个核心概念 服务注册表:维护了服务实例的网络地址、名称等信息的数据库 服务提供者:提供RESTApi以供其他服务调用的被调用方 服务消费者:调用服务端提供的RESTApi接口,发起服务请求的请求方 服务注册:将服务注册表中提供服务信息进行注册服务的行为 服务发现:从服务注册表中查询服务实例信息的行为 接下来,我们看看服务发现的两种模式。 服务发现模式 服务发现主要有两种模式:服务端发现模式和客户端发现模式。 1、客户端发现模式 即由客户端来获取服务实例列表和网络位置,并且进行负载均衡。客户端查询服务注册表(一个维护了服务实例和其网络位置等信息的数据库),获取服务实例列表,并且采用负载均衡算法,从列表中选择一个服务实例作为请求目标。 Figure 1. 客户端发现模式示意图 Eureka采用的就是客户端发现模式。 2、服务端发现模式 即由服务端来发现服务实例并进行负载均衡:客户端通过负载均衡器向某个服务提出请求,负载均衡器查询服务注册表,并将请求转发到可用的服务实例。 Figure 2. 服务端发现模式示意图 AWS Elastic Load Balancer(ELB)采用的是服务端发现模式。 本篇讲解的Spring Cloud Eureka采用的是客户端发现模式,接下来我们看看eureka的基本概念。 2. Eureka简介 Eureka,字面意思即"发现"之意,是Netflix下开源的服务治理(注册、发现等)中间件。Spring Cloud Eureka在其基础上进行了二次封装,添加了自动化配置等功能,使其成为Spring Cloud微服务体系中核心的服务治理方案之一。 Eureka包括三个角色,如下图所示: Figure 3. Eureka基础架构(图片来源网络) Eureka Server:服务注册中心,维护了服务注册表,提供服务注册、服务发现、刷新和取消注册等功能 Service Provider:服务提供者,对外提供服务,一般为RESTApi接口 Service Consumer:服务消费者,调用服务提供者接口,实现具体业务逻辑 由上图可见,Service Provider将注册中心注册服务,Service Consumer从注册中心查询服务注册表,获取服务实例信息,并通过负载均衡算法获取到一个服务提供者实例,并发起调用请求。 Eureka Server并没有后端存储机制,注册表中的所有服务实例必须发送心跳监测以保证正确连接(内存中实现),服务实例(客户端)会将服务端注册表拉去到本地并缓存在内存中,而不是每次都向服务端发送请求来获取服务实例注册信息。 前边介绍了Eureka和服务治理相关的理论,接下来,我们来编程实现服务注册中心。 3. 单节点服务注册中心 3.1. 编写服务注册中心 1、新建名为01-eureka-server的Spring Boot工程,作为服务注册中心 2、引入如下依赖 <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka-server</artifactId> </dependency> ...

2018-08-15 · 2 min · 247 words · 老墨