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

玩转设计模式系列(1)

hzqiuxm阅读(54)评论(0)

OOD设计原则

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

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

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

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

单一职责原则 SEP

核心思想

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

实际应用举例

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

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

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

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

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

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

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

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

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

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

实践原则建议

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

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

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

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

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

里氏替换原则 LSP

核心思想

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

实际应用举例

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

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

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

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

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

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

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

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

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

原则建议

请严格遵守四条原则。

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

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

难点

带来好处:

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

可能坏处:

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

依赖倒装原则

核心思想

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

原则建议

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

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

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

难点

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

接口隔离原则 ISP

核心思想

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

实际应用举例

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

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

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

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

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

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

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

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

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

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

原则建议

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

难点

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

最少知识原则 LSP

核心思想

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

实际应用举例

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

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

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

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

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

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

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

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

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

对上述代码改造如下:

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

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

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

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

原则建议

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

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

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

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

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

开放关闭原则

核心思想

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

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

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

实际应用举例

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

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

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

修改后的类图如下:

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

原则建议

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

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

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

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

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

小结

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

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

Springboot教程系列之

hzqiuxm阅读(165)评论(0)

Springboot装配指南

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

模式注解

模式注解概念

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

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

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

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

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

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

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

模式注解的装配

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

  • 方式,通过xml配置文件

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

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

自定义模式注解

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

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

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

模块装配

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

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

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

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

模块装配实现方式

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

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

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

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

自定义Enable模块装配

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

条件装配

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

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

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

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

自动化装配

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

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

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

自动化配置实现

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

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

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

小结

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

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

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

Elasticsearch简明教程(1)

hzqiuxm阅读(235)评论(0)

ElasticSearch简介与安装入门

ElasticSearch简介

概述

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

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

作用

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

特性

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

ElasticSearch 安装

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

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

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

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

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

常用插件安装

Head

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

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

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

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

head插件展示示意图:

IK分词器

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

版本v5.5.1之前的

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

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

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

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

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

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

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

版本v5.5.1之后的

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

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

注意事项

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

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

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

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

pinyin分词器

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

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

mavel

1: 在ES里面安装Marvel插件

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

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

简单官网示例测试步骤

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

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


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

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

Docker简明教程(11)

hzqiuxm阅读(226)评论(0)

Docker容器链接与编排

容器链接

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

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

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

基本使用

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

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

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

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

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

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

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

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

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

3:重启docker服务

使用容器连接来通信

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

容器编排

编排简介

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

Docker-compose简介

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

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

Docker-compose安装

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

Docker-compose基本示例

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

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

Docker-compose yml文件的配置

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

Docker-compose 的networks配置

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

networks:
frontend:
backend:

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

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

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

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

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

Docker简明教程(10)

hzqiuxm阅读(240)评论(0)

Docker与常见的数据库结合使用

MYSQL使用

1:下载镜像

docker pull mysql:5.7 

2:指定宿主机数据卷启动

docker run --name mysql -p 12345:3306 -v /home/dev/mysqldata:/var/lib/mysql --privileged=true -e MYSQL_ROOT_PASSWORD=123321qq -d mysql:5.7 

这里指定了使用自己的mysql数据文件

3:使用自定义配置文件启动,在mysqlconf下放着my.cnf文件:

docker run --name mysql -p 12345:3306 -v /home/dev/mysqldata:/var/lib/mysql -v /home/dev/mysqlconf:/etc/mysql/conf.d --privileged=true -e MYSQL_ROOT_PASSWORD=123321qq -d mysql:5.7

4:使用自定义日志目录

docker run --name mysql -p 12345:3306 -v /home/dev/mysqldata:/var/lib/mysql -v /home/dev/mysqllogs:/var/log/mysql --privileged=true -e MYSQL_ROOT_PASSWORD=123321qq -d mysql:5.7 

如果日志开启不成功,可能是因为mysql用户没有对日志文件夹的操作权限,进入到容器里面,设置一下,示例如下:

chown -R mysql:mysql /var/log/mysql

5:数据表备份,在宿主机上执行

docker exec 容器id sh -c 'exec mysqldump --all-databases -uroot -p"123321qq"' > /home/all-databases.sql 

REDIS使用

1.下载镜像

docker pull redis 

2.启动容器,默认暴露6379端口

docker run --name myredis -d redis

3.如果想使用自己的配置文件启动redis,一种方法是在其基础上写一个dockerfile,例如:

FROM redis
COPY redis.conf /usr/local/etc/redis/redis.conf
CMD [ "redis-server", "/usr/local/etc/redis/redis.conf" ]

另外一种方式是在启动命令中修改配置,如:

docker run -v /myredis/conf/redis.conf:/usr/local/etc/redis/redis.conf --name myredis redis redis-server /usr/local/etc/redis/redis.conf

4.数据默认存储在VOLUME /data目录下,使用-v来指定挂载,如:

docker run --name myreis -d -p 6379:6379 -v /redisdocker/data:/data -v /redisconf/redis.conf:/usr/local/etc/redis/redis.conf --privileged=true redis redis-server /usr/local/etc/redis/redis.conf

注意:自己写的conf文件里面,不要配置bind的ip,也不要daemonize的配置,直接注释掉

5.aof的持久化方式

如果需要开启aof的持久化方式默认是rdb的,可以在配置文件里面设置,也可通过命令行指定:

docker run --name some-redis -d redis redis-server --appendonly yes 

6.如果应用需要连接redis:

docker run --name some-app --link some-redis:redis -d application-that-uses-redis

ElasticSearch使用

1.下载镜像

docker pull elasticsearch:6.4.1

2.启动过后,在里面安装上ik和head,然后构建自己的镜像:

具体安装过程请查看 [ELK简明指南系统—Elasticsearch及常用插件安装]一文

docker run -d -v /es-6.4.1/data:/usr/share/elasticsearch/data -v /es-6.4.1/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml -v /es-6.4.1/config/logging.yml:/usr/share/elasticsearch/config/logging.yml -v /es-6.4.1/config/scripts:/usr/share/elasticsearch/config/scripts --privileged=true -p 9200:9200 -p 9300:9300 elasticsearch:6.4.1
docker commit -m="add head and ik" -a="qiuxm" 容器id qiuxmes:6.4.1

3.然后就可以使用自己的镜像了

注意在使用docker里面的es的时候,要把client.transport.sniff设置为false

Docker简明教程(9)

hzqiuxm阅读(284)评论(0)

Docker优雅使用指南

敏捷思想地图

敏捷思想和团队

Docker 的优点在于通过简化CI(持续集成)、CD(持续交付)的构建流程,让开发者把更多的精力用在开发上。

每家公司都有自己的开发技术栈,我们需要结合实际情况对其进行持续改进,优化自己的构建流程。
那在引入 Docker 技术的时候,遵循什么样的业界标准呢?大家会发现其实目前并没有什么最佳实践标准,常常以最佳实践为口号,引入多种工具链,导致在使用 Docker 的过程中没有侧重点。涉及到 Docker 选型,又在工具学习上花费大量时间,而不是选用合适的工具以组建可持续发布产品的开发团队。基于这样的场景,我们可以把“简单易用”的原则作为评判标准。

首先指定好整体构建蓝图,让团队的每一个成员都清晰知道流程,然后需要解决的是让团队成员尽快掌握 Docker 命令行的使用。

基础镜像

包括了操作系统命令行和类库的最小集合,一旦启用,所有应用都需要以它为基础创建应用镜像。Ubuntu 作为官方使用的默认版本,是目前最易用的版本,但系统没有经过优化,可以考虑使用第三方优化过的版本,比如 phusion-baseimage。
那我们为什么要使用优化过的基础镜像?拿baseimage来说,它提供了:

  • 一个正确的 init进程

    Baseimage-docker附带一个init进程/sbin/my_init,可以正确地处理孤立的子进程,并正确响应SIGTERM。这样你的容器就不会充满僵尸进程,并且docker stop可以正常工作。

  • 修复了与Docker的APT不兼容问题:请参阅Docker问题#1024。

  • syslog优化:它运行syslog守护程序,以便重要的系统消息不会丢失。
  • cron守护进程:它运行一个cron守护进程,以便cronjobs工作。
  • SSH服务器:允许您轻松登录容器以检查或管理事物。

对于选择 RHEL、CentOS 分支的 Base Image,提供安全框架 SELinux 的使用、块级存储文件系统 devicemapper 等技术,这些特性是不能和 Ubuntu 分支通用的。另外需要注意的是,使用的操作系统分支不同,其裁剪系统的方法也完全不同,所以大家在选择操作系统时一定要慎重。

配置管理工具

一般学习Docker时,主要用于基于 Dockerfile 来创建 Image 镜像的,考虑到在实际情况中,我们需要配置和维护Docker宿主机,对容器和镜像进行管理,对构建的进行配置。所以需要结合开发团队的现状,选择一款团队熟悉的工具作为通用工具。

配置工具有很多种选择:Chef、Ansible、Salt Stack、Puppet。其中 Ansible 作为后起之秀,具备简单、强大、无代理的特点。在配置管理的使用中体验非常简单易用,推荐大家参考使用。

Host主机系统

主机系统是Docker 后台进程的运行环境。从开发角度来看,它就是一台普通的单机 OS 系统,我们仅部署Docker 后台进程以及集群工具,所以希望 Host 主机系统的开销越小越好。这里推荐给大家的 Host 主机系统是 CoreOS,它是目前开销最小的主机系统。另外,还有红帽的开源 Atomic 主机系统,有基于Fedora、CentOS、RHEL多个版本的分支选择,也是不错的候选对象。

另外一种情况是选择最小安装操作系统,自己定制Host 主机系统。如果你的团队有这个实力,可以考虑自己定制这样的系统。

优雅实践指南

持续集成的构建系统

开发团队把代码提交到 Git 应用仓库的那一刻,我相信所有的开发者都希望有一个系统能帮助他们把这个应用程序部署到应用服务器上,以节省不必要的人工成本。但是,复杂的应用部署场景,让这个想法实现起来并不简单。

首先,我们需要有一个支持 Docker 的构建系统,这里推荐 Jenkins。它的主要特点是项目开源、方便定制、使用简单,完全拥抱docker。Jenkins 支持安装各种第三方插件,从而方便快捷的集成第三方的应用。

通过Jenkins 系统的 Job 触发机制,我们可以方便的创建各种类型的集成 Job 用例,但缺乏统一标准的 Job 用例使用方法,会导致项目 Job 用例使用的混乱,难于管理维护,这也让开发团队无法充分利用好集成系统的优势,当然这也不是我们期望的结果。所以,敏捷实践方法提出了一个可以持续交付的概念 DeploymentPipeline(管道部署)。通过Docker 技术,我们可以很方便的理解并实施这个方法。

Jenkins 的管道部署把部署的流程形象化成为一个长长的管道,每间隔一小段会有一个节点,也就是 Job,完成这个 Job 工作后才可以进入下一个环节。形式如下:

大家看到上图中的每一块面板在引入 Docker 技术之后,就可以使用 Docker 把任务模块化,然后做成有针对性的 Image 用来跑需要的任务。每一个任务 Image 的创建工作又可以在开发者自己的环境中完成,类似的场景可以参考下图:所以,使用 Docker 之后,任务的模块化很自然地被定义出来。通过管道图,可以查看每一步的执行时间。开发者也可以针对任务的需要,为每一个任务定义严格的性能标准,已作为之后测试工作的参考基础。

所以,使用 Docker 之后,任务的模块化很自然地被定义出来。通过管道图,可以查看每一步的执行时间。开发者也可以针对任务的需要,为每一个任务定义严格的性能标准,已作为之后测试工作的参考基础。

最佳的发布环境

应用经过测试,接下来我们需要把它发布到测试环境和生产环境。这个阶段中如何更合理地使用Docker 也是一个难点,开发团队需要考虑如何打造一个可伸缩扩展的分发环境。其实,这个环境就是基于 Docker 的私有云,更进一步我们可能期望的是提供 API 接口的 PaaS 云服务。为了构建此 PaaS 服务,这里推荐Google Kubernetes。

Kubernetes是一个容器集群管理工具,它提出两个概念:
1 Cluster control plane(AKA master),集群控制面板,内部包括多个组件来支持容器集群需要的功能扩展。

2 The Kubernetes Node,计算节点,通过自维护的策略来保证主机上服务的可用性,当集群控制面板发布指令后,也是异步通过 etcd 来存储和发布指令,没有集群控制链路层面的依赖。
通过官方架构设计文档的介绍,可以详细的了解每个组件的设计思想。这是目前业界唯一在生产环境部署经验的基础上推出的开源容器方案,目前是 CNCF 推荐的容器管理系统的行业参考标准。

需要的监控网站

这里介绍使用一个开源项目来监控容器的运行——Weave Scope,操作简单易用,初学者极力推荐。
首先,在宿主主机执行以下命令来安装和启动 Weave Scope:

sudo curl -L git.io/scope -o /usr/local/bin/scope  //如果下载不下来,可以自己手动去下载再传到宿主机上
sudo chmod a+x /usr/local/bin/scope
scope launch

安装启动后,控制台会输出浏览器服务器 IP 地址和端口信息4040,访问这个URL地址就可以实时监控了:

点击对应的容器即可查看容器信息,包括 CPU 占用率,内存占用,端口映射表,开机时间,IP 地址,进程列表,环境变量等等。并且,通过这个监控网站上的界面操作,可以对容器做一些简单操作:停止,重启,attach,exec 等,提升一定的效率。
当然这是一个很简单的 Docker 容器监控方案,虽然使用者可以通过这个监控网站直接操作容器,所以无需登录宿主主机来进行相关操作,完美实现资源隔离。但是权限并没有做到独立,所以选择的时候要根据你的实际情况进行使用。
此外结合Kubernetes的使用,还可以监控整个集群,总之是一个不错的web监控方案。

镜像加速

方式一:
- 注册 DaoCloud https://account.daocloud.io/signup
- 使用加速功能

curl -sSL https://get.daocloud.io/daotools/set_mirror.sh | sh -s http://f1361db2.m.daocloud.io

方式二:修改/etc/sysconfig/docker
vim /etc/sysconfig/docker
--registry-mirror={加速地址} 比如: --registry-mirror=http://f1361db2.m.daocloud.io https://yzcs6yua.mirror.aliyuncs.com
方式三:修改/etc/default/docker
echo "DOCKER_OPTS=\"\$DOCKER_OPTS --registry-mirror=http://f2d6cb40.m.daocloud.io\"" | sudo tee -a /etc/default/docker
配置好后要重启服务,并查看下是否带上了镜像参数,相关命令如下

service docker restart  //重启服务
ps aux | grep docker  // 查看启动参数

小结

Docker优雅实践方案,其实是一套灵活简单的敏捷解决方案,充分体现了DevOps思想。它克服了之前集群工具复杂、难用的困境,使用统一的 Docker 应用容器概念部署软件应用。
通过引入 Docker 技术,开发团队在面对复杂的生产环境中,可以结合自己团队的实际情况,定制出适合自己基础架构的开发测试运维一体化解决方案。

参考文档 :
https://www.jianshu.com/p/1a82f357c364
https://blog.csdn.net/qian1122221/article/details/82260644
http://guide.daocloud.io/dcs/daocloud-services-9152664.html

Docker简明教程(8)

hzqiuxm阅读(295)评论(0)

Docker的私有仓库

私有仓库搭建与入门

创建步骤

  • 下载官方的仓库镜像 docker pull registry
  • 启动Docker Registry容器,默认情况下,会将私有仓库存放于容器内的/var/lib/registry(版本不同可能会有变动,之前是在/tmp/registry)目录下,这样如果容器被删除,则存放于容器中的镜像也会丢失。
    所以一般情况下会指定本地一个目录挂载到容器内的/var/lib/registry下,如下:
docker run -d --name=my_registry -p 7779:5000 -v /ccuse/myregistry/:/var/lib/registry/ --privileged=true registry
  • 查看Docker Registry进程 docker ps

基本操作

  • 查看Registry仓库中现有的镜像
curl -XGET http://192.168.3.112:7779/v2/_catalog

命令的结果将返回一个镜像的清单

  • 将Docker镜像推到Registry中
//给本地镜像打Tag
docker tag mytomcat9 192.168.3.112:7779/mytomcat9test
//推送镜像到Registry中
docker push 192.168.3.112:7779/mytomcat9test
(会出现错误,因为client与Registry交互默认将采用https访问,但我们在安装Registry时并未配置指定相关的key和crt文件,https将无法访问)
//centos在/etc/sysconfig/docker中做配置
ADD_REGISTRY='--add-registry 192.168.3.112:7779’
INSECURE_REGISTRY=‘--insecure-registry 192.168.3.112:7779’
//ubuntu在/etc/docker/default.json中配置:
{ "insecure-registries":["192.168.3.112:7779"] }
//然后重启docker服务:
service docker restart

WEB管理服务搭建

根据上面官方提供的镜像搭建的私服,只有API的操作方式,对于我们管理自己的镜像不直观也不方便,熟悉nexus私服的都知道,管理一些jar依赖时候可以方便的通过web页面的方式进行。还好我们可以通过hyper/docker-registry-web这个镜像来搭建一个web服务来进行私服镜像的管理。
访问docker的官方,可以搜索到,主要的安装步骤如下(具体的说明请参考官方文档说明https://hub.docker.com/r/hyper/docker-registry-web/):

  • 拉取镜像到本地 docker pull hyper/docker-registry-web
  • 启动:
方式一:
//启动私服镜像服务
docker run -d -p 7779:5000 --name registry-srv registry
//启动web服务
docker run -it -p 7780:8080 --name registry-web --link registry-srv -e REGISTRY_URL=http://registry-srv:5000/v2 -e REGISTRY_NAME=localhost:5000 hyper/docker-registry-web 

方式二:
//带身份验证访问
docker run -it -p 7780:8080 --name registry-web --link registry-srv \
           -e REGISTRY_URL=https://registry-srv:5000/v2 \
           -e REGISTRY_TRUST_ANY_SSL=true \
           -e REGISTRY_BASIC_AUTH="YWRtaW46Y2hhbmdlbWU=" \
           -e REGISTRY_NAME=localhost:5000 hyper/docker-registry-web

方式三:           
//使用配置启动
1 新建配置文件
registry:
  # Docker registry url
  url: http://registry-srv:5000/v2
  # Docker registry fqdn
  name: localhost:5000
  # To allow image delete, should be false
  readonly: false
  auth:
    # Disable authentication
    enabled: false
2 启动
docker run -d -p 7779:5000 --name registry-srv registry
docker run -it -p 7780:8080 --name registry-web --link registry-srv -v $(pwd)/config.yml:/conf/config.yml:ro hyper/docker-registry-web

访问web服务查看仓库中镜像:

为web服务增加删除功能

我们可以看到上面私服中只有查看镜像的操作,没有删除的操作,如果要删除的话,要创建一个给registry用的config.yml,在里面设置可以delete,例如:

``` registry的配置文件
version: 0.1
log:
level: info
formatter: text
fields:
service: registry-srv
environment: production
storage:
cache:
layerinfo: inmemory
filesystem:
rootdirectory: /var/lib/registry
delete:
# 要在ui 上能够删除镜像,enable 的值必须是true
enabled: true
http:
addr: :5000

<pre class="line-numbers prism-highlight" data-start="1"><code class="language-null"><br />还要给web服务也提供一个配置文件,是其能够在页面上显示delete操作的按钮
``` web的配置文件
registry:
# Docker registry url
url: http://registry-srv:5000/v2
# Docker registry fqdn
name: localhost:5000
# To allow image delete, should be false
readonly: false
delete:
enabled:true
auth:
# Disable authentication
enabled: false

它们的启动命令如下:

docker run -d -p 7779:5000 --name registry-srv  -v /ccuse/myregistry/:/var/lib/registry  -v $(pwd)/config-svr.yml:/conf/config.yml  registry
docker run -it -p 7780:8080 --name registry-web --link registry-srv -v $(pwd)/config.yml:/conf/config.yml:ro hyper/docker-registry-web

再次进入私服的web页面查看容器镜像,发现页面上多了delete的操作按钮,我们可以通过这里对镜像进行删除了。当然你也可以进入到容器的保存目录,手动去删除。

Docker简明教程(7)

hzqiuxm阅读(273)评论(0)

Dockerfile入门与实践技巧

在上一节中演示了一个构建静态网站的例子,里面涉及到了Dockerfile的编写,刚接触的同学可能不太理解,所以专门新增一节介绍下Dockerfile的基础知识和一些编写原则与技巧。
然后以构建一个自己的java+tomcat的镜像为例子

理论部分

Dockerfile是用来构建Docker镜像的构建文件,是由一系列命令和参数构成的脚本。
熟悉linux下Shell脚本同学学起来应该是毫不费力,没有经验的掌握好常用的指令,多看看官网现成的例子也应该很快就上手的。

基本语法讲解

1:每条指令都必须为大写字母,且后面要跟随至少一个参数
2:指令按照从上到下,顺序执行
3:#表示注释
4:每条指令都会创建一个新的镜像层,并对镜像进行提交
5:Docker执行Dockerfile的大致流程
(1)docker从基础镜像运行一个容器
(2)执行一条指令,对容器作出修改
(3)执行类似docker commit的操作,提交一个新的镜像层
(4)docker再基于刚提交的镜像运行一个新容器
(5)执行dockerfile中的下一条指令,直到所有指令都执行完成
6:Docker会将构建镜像的过程缓存起来,如果不需要缓存,可以在docker build的时候指定 --no-cache

指令功能介绍

FROM

指定一个已经存在的镜像,也是构建的基础镜像,Dockerfile的第一条必须是FROM

MAINTAINER

设置作者,联系邮件

RUN

指定要运行的命令,建议使用数组的格式,也是exec的格式,如:
RUN[“apt-get”,”install”,”-y”,”nginx”]

EXPOSE

向容器外部公开容器内的端口

WORKDIR

指定在创建容器的时候,在容器内部设置一个工作目录,entroypoint和CMD指定的
程序会在这个目录执行
可以在docker run中使用-w来覆盖工作目录

USER

指定该镜像以什么样的用户去执行,可以单独指定用户,也可以指定用户和组,格
式:USER uid:gid,可以在docker run中通过-u来覆盖,如果都不指定,默认是root

CMD

指定一个容器启动时要运行的命令,如果指定了多条CMD,只有最后一条会执行
例如:CMD[“/bin/bash”,”-l”]
如果在docker run 后面跟上要执行的命令,会覆盖Dockerfile里面的cmd指定的命令

ENTROYPOINT

也用来指定一个容器启动时要运行命令
1:但是它不会被docker run后面的命令覆盖,而是把docker run指定的任何参数当作参数传
递给entroypoint
2:可以和CMD一起用,比如:

ENTROYPOINT[“/usr/sbin/nginx”]
CMD[“-h”]

这样如果docker run的时候不覆盖CMD,那么就是按照
/usr/sbin/nginx –h来运行
如果运行的时候:docker run –it 容器id –g“daemon off;”
那么实际运行就是:
/usr/sbin/nginx –g “daemon off;”
3:如果非要覆盖entrypoint,可以在docker run的时候,设置 --entroypoint 标志

ENV

用来在构建镜像过程中设置环境变量,例如:
ENV MY_PATH /usr/my
这个环境变量可以在后续的任何RUN指令中使用,这就如同在命令前面指定了环境变
量前缀一样;也可以在其它指令中直接使用这些环境变量,比如:
WORKDIR $MY_PATH
可以在docker run 命令中使用–e来指定环境变量,这些变量只在运行时有效
小技巧:
可以在不需要构建缓存的前面,添加一个ENV语句,这样,要后面更新的时候,就修
改一下这个ENV的值

ADD

用来将构建环境下的文件或目录复制到镜像中。
1:只能操作构建环境相对的文件或目录,文件源也可以使用URL格式
2:如果将一个归档文件指定为源文件,docker会自动解压
3:如果目的位置不存在的话,Docker会自动创建全路径
注意:ADD会使得构建缓存无效,ADD后续指令都不能使用之前的构建缓存了

COPY

类似于ADD,COPY只做构建上下文中复制文件,而不会去做文件提取和解压的工作
如果源文件是目录,那么这整个目录会被复制到容器中

VOLUME

用来向镜像创建的容器添加卷,一个卷是可以存在于一个或者多个容器内的特定目
录,这个目录可以绕过联合文件系统,并提供如下共享数据或者对数据进行持久化:
1:卷可以在容器间共享和重用
2:对卷的修改是立即生效的
3:对卷的修改不影响镜像
卷可以让我们把数据、数据库或其它内容添加到镜像中,而不是将这些内容提交到
镜像中,并且允许我们在多个容器间共享这些内容。
注意:如果删除了最后一个使用卷的容器,内部卷就不见了。
可在docker run的时候,使用-v来把宿主机的目录映射到容器,这样数据就能一直保存了

ONBUILD

指定当镜像做为其它镜像的基础镜像时,该镜像触发执行的功能。
ONBUILD在子镜像build的时候,在FROM之后就先执行,并且只能被执行一次不会被孙镜像继承

Dockerfile编写原则

  • 容器应该是短暂的
  • 避免安装不必要的包
  • 每个容器应该只有一个关注点
  • 最小化层的数量
  • 对多行参数进行排序
  • 缓存中间构建的镜像

实践部分

编写一个Dockerfile文件

FROM ubuntu
MAINTAINER hzqiuxm
#把java与tomcat添加到容器中
ADD jdk-8u111-linux-x64.tar.gz /root/src/soft/
ADD apache-tomcat-9.0.0.M26.tar.gz /root/src/soft/
#配置java与tomcat环境变量
ENV JAVA_HOME /root/src/soft/jdk1.8.0_111
ENV CLASSPATH $JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
ENV CATALINA_HOME /root/src/soft/apache-tomcat-9.0.0.M26
ENV CATALINA_BASE /root/src/soft/apache-tomcat-9.0.0.M26
ENV PATH $PATH:$JAVA_HOME/bin:$CATALINA_HOME/lib:$CATALINA_HOME/bin
#容器运行时监听的端口
EXPOSE 8080
#启动时运行tomcat
# CMD ["/root/src/soft/apache-tomcat-9.0.0.M26/bin/catalina.sh","run"]
# ENTRYPOINT ["/root/src/soft/apache-tomcat-9.0.0.M26/bin/startup.sh" ]
CMD /root/src/soft/apache-tomcat-9.0.0.M26/bin/startup.sh && tail -F /root/src/soft/apache-tomcat-9.0.0.M26/bin/logs/catalina.out

使用Dockerfile来制作镜像
在/root/src/soft/目录下准备以下文件:1 刚创建的Dockerfile文件 2 jdk压缩包 3 tomcat压缩包,然后执行以下命令

docker build -t mytomcat9 .

构建镜像过程日志如下:

[root@localhost docker]# docker build -t mytomcat9 .
Sending build context to Docker daemon 190.8 MB
Step 1/11 : FROM ubuntu
 ---> 16508e5c265d
Step 2/11 : MAINTAINER hzqiuxm
 ---> Using cache
 ---> f5d67d9ca44a
Step 3/11 : ADD jdk-8u111-linux-x64.tar.gz /root/src/soft/
 ---> 34a20e7958ba
Removing intermediate container 8b7d6c0735ca
Step 4/11 : ADD apache-tomcat-9.0.0.M26.tar.gz /root/src/soft/
 ---> b4ce4316740b
Removing intermediate container b208617a6a63
Step 5/11 : ENV JAVA_HOME /root/src/soft/jdk1.8.0_111
 ---> Running in 1ae8272ac0ef
 ---> 52cc51e6fdcd
Removing intermediate container 1ae8272ac0ef
Step 6/11 : ENV CLASSPATH $JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
 ---> Running in 91ace6986fed
 ---> a4f53b5778a4
Removing intermediate container 91ace6986fed
Step 7/11 : ENV CATALINA_HOME /root/src/soft/apache-tomcat-9.0.0.M26
 ---> Running in caf3441b162c
 ---> d1595d6ae6e7
Removing intermediate container caf3441b162c
Step 8/11 : ENV CATALINA_BASE /root/src/soft/apache-tomcat-9.0.0.M26
 ---> Running in 6e359bd47512
 ---> 23b7222d20fc
Removing intermediate container 6e359bd47512
Step 9/11 : ENV PATH $PATH:$JAVA_HOME/bin:$CATALINA_HOME/lib:$CATALINA_HOME/bin
 ---> Running in 438a22f75079
 ---> 9c0df759c3ed
Removing intermediate container 438a22f75079
Step 10/11 : EXPOSE 8080
 ---> Running in 994985ed6196
 ---> 2bfbb442a498
Removing intermediate container 994985ed6196
Step 11/11 : CMD /root/src/soft/apache-tomcat-9.0.0.M26/bin/startup.sh && tail -F /root/src/soft/apache-tomcat-9.0.0.M26/bin/logs/catalina.out
 ---> Running in 18b045c67d53
 ---> 41bd1cf525b6
Removing intermediate container 18b045c67d53
Successfully built 41bd1cf525b6

启动容器进行测试访问

docker run -i -t -d -p 9080:8080 --name myt9 -v /ccdockermake/tomcat9/test:/root/src/soft/apache-tomcat-9.0.0.M26/webapps/test -v /tomcat9logs/:/root/src/soft/apache-tomcat-9.0.0.M26/logs --privileged=true mytomcat9

  • 可以进入运行的容器进行相关的操作
docker exec -it 容器id /bin/bash
  • 访问容器的tomcat首页
//我是在虚拟机中运行的,所以ip地址是虚拟机的地址
http://192.168.52.128:9080/

Springboot教程系列之

hzqiuxm阅读(1077)评论(0)

Springboot中的MonogoDB多文档事务

前言

NOSQL事务之殇

说到关系数据库的事务,开发同学们一定耳熟能详,原子性、隔离性、一致性、持久性是不是如数家珍?熟悉Spring框架的还深谙在Spring中如何优雅的控制事务。但是我们知道Spring本身没有实现事务,其事务控制也是以来底层关系型数据库的。

对使用MongoDB的同学来说,当然不能使用了。因为在MongoDB里单文档的操作是具备原子性的,多文档就不支持了,所在设计MongoDB数据库的时候,就会用到嵌入和引用来规避事务控制,如果向银行转账之类的操作,无法完全规避的情况下,也是通过自己创建一个容器文档,来做仿真事务的。但是在MongoDB4.0中,支持了我们梦寐以求的多文档事务。

官方消息解读

来看下官方文档中的介绍:

Best way to work with data: adding multi-document ACID transactions, data type conversions, native visualizations with MongoDB Charts, the MongoDB Compass aggregation pipeline builder, and the MongoDB Stitch serverless platform.

Intelligently place data where you need it to be: with 40% faster shard migrations, non-blocking secondary replica reads, SHA-2 authentication, and the new MongoDB Mobile database.

Freedom to run anywhere: bringing global clusters and enterprise security with HIPAA compliance to the MongoDB Atlas database service, along with the free community monitoring service, and Kubernetes integration.

我们可以看到4.0中优化了不少特性,但今天只要关注第一条的开头一句就够了: adding multi-document ACID transactions(增加了多文档ACID事务),接下来我们可以喜大普奔了。

MongoDB新版本提供了面向复制集的多文档事务特性。其能满足在多个操作,文档,集合,数据库之间的事务性,事务的特性:一个事务中的若干个操作要么全部完成,要么全部回滚,操作的原子性A,数据更新的一致性C。事务提交时,所有数据更改都会被永久保存D。事务提交前其数据更改不会被外部获取到。还有个重磅消息:多文档事务在4.0版本仅支持复制(副本)集,对分片集群的事务性支持计划在4.2版本中实现。

不过有个前提,事务的ACID操作必须是针对一个已经存在的集合进行,详细的方法和介绍大家可以去读读官方的文档。我接下来要介绍的是我们如何在Springboot中实现MongoDB的多文档事务操作。

阅读下面的内容需要你具备以下知识:对Springboot有一定的使用经验,了解MongoDB的基本概念掌握基本使用。

环境准备

MongoDB环境准备

  • 版本检查:版本必须是4.0+的,如果不是新安装而是老版本升级的,要检查下自己数据库featureCompatibilityVersion参数是不是4.0
db.adminCommand( { getParameter: 1, featureCompatibilityVersion: 1 } )

如果不是的话设置下:db.adminCommand( { setFeatureCompatibilityVersion: "4.0" } )

  • 副本集群搭建:因为多文档事务前提就是需要多个副本集,所以我们至少准备2个副本集,我这里准备的是在内网环境中的一台机器上采用不同的端口实现多副本集,生产环境下一般都会分开。

主副本:192.168.3.112:27117,辅助副本:192.168.3.112:27217

其中一个的配置文件如下:

port = 27117  端口,另一个是27217
dbpath =../dbs/   存放数据文件,相同机器时要不同
logpath=../logs/mongdb.log  存放日志文件, 相同机器时要不同
logappend=true
fork=true
bind_ip=0.0.0.0
replSet = myrepl   集群名称,自己定义

副本集初始化命令:

rs.initiate({_id:"myrepl",members:[
{_id:0,host:'192.168.3.112:27117'},
{_id:1,host:'192.168.3.112:27217'}]
})

注意:这里的IP不是本机的话,一定要填写内网具体的地址,不能用回写地址127.0.0.1;因为这个IP是要和客户端通信的。

配置成功后可以使用rs.config() 来查看下,如果初始化时候信息填错,可以使用rs.reconfig({...})来重新设置下

{
    "_id" : "myrepl",
    "version" : 2,
    "protocolVersion" : NumberLong(1),
    "writeConcernMajorityJournalDefault" : true,
    "members" : [ 
        {
            "_id" : 0,
            "host" : "192.168.3.112:27117",
            "arbiterOnly" : false,
            "buildIndexes" : true,
            "hidden" : false,
            "priority" : 1.0,
            "tags" : {},
            "slaveDelay" : NumberLong(0),
            "votes" : 1
        }, 
        {
            "_id" : 1,
            "host" : "192.168.3.112:27217",
            "arbiterOnly" : false,
            "buildIndexes" : true,
            "hidden" : false,
            "priority" : 1.0,
            "tags" : {},
            "slaveDelay" : NumberLong(0),
            "votes" : 1
        }
    ],
    "settings" : {
        "chainingAllowed" : true,
        "heartbeatIntervalMillis" : 2000,
        "heartbeatTimeoutSecs" : 10,
        "electionTimeoutMillis" : 10000,
        "catchUpTimeoutMillis" : -1,
        "catchUpTakeoverDelayMillis" : 30000,
        "getLastErrorModes" : {},
        "getLastErrorDefaults" : {
            "w" : 1,
            "wtimeout" : 0
        },
        "replicaSetId" : ObjectId("5b6aca1b7ef20d701c739318")
    }
}

Springboot环境准备

  • 加入依赖:本人比较喜欢使用Gradle来管理包依赖,喜欢Maven的同学可以自己根据其语法配置。
    需要注意的是依赖包的版本是要有严格限制的,因为我们目前使用的是spring-data-mongodb的RC版本,目前稳定版中还不支持,所以drive包必须是3.8的,common包里必须是对应的RC版,我们要把自动依赖的低版本包去除掉。依赖包如下所示:
dependencies {
    compile('org.springframework.boot:spring-boot-starter-data-mongodb'){
        exclude group: 'org.springframework.data', module: 'spring-data-mongodb'
        exclude group: 'org.mongodb', module: 'mongodb-driver'
    }
    compile ('org.springframework.data:spring-data-mongodb:2.1.0.RC1'){
        exclude group: 'org.springframework.data', module: 'spring-data-commons'
    }
    compile ('org.mongodb:mongo-java-driver:3.8.0')
    compile('org.springframework.boot:spring-boot-starter-web')
    compile('org.springframework.data:spring-data-commons:2.1.0.RC1')

数据库查看根据

如果还用客户端直接访问服务端来查看MongoDB集合或文档的话,那效率也太低了,工欲善其事必先利其器嘛。所以免费的工具还是推荐Robo 3T,当然如果你使用的是收费的Studio 3T那真的最好了。

其操作界面如下:

与Springboot结合

用Spring注解方式来控制事务

要使用Spring注解方式控制事务,需要使用使用到MongoDB提供的事务管理类,我们要先配置一个

@Bean
MongoTransactionManager transactionManager(MongoDbFactory dbFactory) {
    return new MongoTransactionManager(dbFactory);
}

编写测试类代码

然后创建好需要测试的控制类,服务类,资源类。篇幅原因就不把各个部分的代码贴上来了,只简单介绍下注意点,源代码请到我的个人仓库查看:https://gitee.com/hzqiuxm/mddemo.git
文档实体类Person.java

@Document
public class Person {
    @Id
    private ObjectId id;
    private String name;
    private int age;
    private List<String> address;
    private Map<String,String> des;
     ......
   }

TestController.java是控制类,里面的方法testAdd()用来测试添加一个文档

@GetMapping("/testAdd")
public void testAdd(){

    Person person = new Person("fengxiaoxiong",29);

    ArrayList<String> strings = new ArrayList<>();
    strings.add("浙江省杭州市西湖区");
    strings.add("浙江省杭州市之江区");
    person.setAddress(strings);

    Map<String,String> descrption = new HashMap<>();
    descrption.put("ext1","my");
    descrption.put("ext2","bear");

    person.setDes(descrption);

    personService.addPerson(person);
}

PersonService 是服务类,自动注入实体操作类,并增加一个addPerson()方法,用来添加一个文档。

@Autowired
private PersonRepository personRepository;
@Transactional
public void addPerson(Person person){
    personRepository.insert(person);
}

我们使用注解@Transctional来控制事务
最后的PersonRespository接口是用来操作数据集集合文档的,继承自MongoDBRepository接口,简单的增删改查方法框架都帮我们生成好了,所以不用增加任何方法。

public interface PersonRepository extends MongoRepository<Person,ObjectId> {
}

创建数据库集合和配置文件

本次用到的数据库名称:sale;集合名称Person
在Springboot工程里application.yml配置文件中加入:

spring:
    data:
      mongodb:
        host: 192.168.3.112  //主副本的地址
        port: 27117          //主副本的端口
        replica-set: myrepl  //集群名称
        database: sale       //数据库名称

注意,这里只要配置主副本的地址和端口就可以了,工程启动的时候我们可以从启动日志看到通过连接主副本后,会解析集群的信息然后分别连接到各个副本

测试

无事务方法中出现异常情况

在服务方法中我们加入一个除0的异常代码

public void addPerson(Person person){
    personRepository.insert(person);
    int a = 5/0;
}

这种情况下,因为没有事务文档是会添加成功的,执行后报java.lang.ArithmeticException: / by zero异常错误,但是我们查看数据库,发现记录被添加了:

/* 6 */
{
    "_id" : ObjectId("5b6bb1a29391ca5a04d13514"),
    "name" : "fengxiaoxiong",
    "age" : 29,
    "address" : [ 
        "浙江省杭州市西湖区", 
        "浙江省杭州市之江区"
    ],
    "des" : {
        "ext2" : "bear",
        "ext1" : "my"
    },
    "_class" : "com.example.md.mddemo.Person"
}

有事务方法中出现异常情况

因为加了事务,所以会避免提交,我们把要把TestController.java中testAdd()代码中插入内容修改下:

Person person = new Person("jinguanghui",31);
ArrayList<String> strings = new ArrayList<>();
strings.add("浙江省杭州市余杭区");
strings.add("浙江省杭州市之江区");
person.setAddress(strings);
Map<String,String> descrption = new HashMap<>();
descrption.put("ext1","so");
descrption.put("ext2","cool");

然后把事务注解加回到服务方法addPerson()上

@Transactional
public void addPerson(Person person){
    personRepository.insert(person);
    int a = 5/0;
}

执行后报java.lang.ArithmeticException: / by zero异常错误,但是我们查看数据库,发现记录未被添加事务控制起作用了

有事务无异常情况

最后我们把产生异常的代码 a=5/0删除掉执行,文档被正常插入了

/* 7 */
{
    "_id" : ObjectId("5b6bb3c49391ca5a788cfcfa"),
    "name" : "jinguanghui",
    "age" : 31,
    "address" : [ 
        "浙江省杭州市余杭区", 
        "浙江省杭州市之江区"
    ],
    "des" : {
        "ext2" : "cool",
        "ext1" : "so"
    },
    "_class" : "com.example.md.mddemo.Person"
}

总结

虽然上面只简单使用了一个除0异常来演示,但是原理上是相同的,我们可以在除0异常后加入其它文档操作,最后效果类似。

通过这个简单示例,我们现在如果要使用MongoDB的多文档事务控制功能,必须要注意下面几点:

  • MongoDB的版本必须是4.0
  • 事务功能必须是在多副本集的情况下才能使用
  • 如果不和其他框架结合使用,直接参考官方文档的显示调用方式
  • 事务控制只能用在已存在的集合中
  • 如果想和Springboot框架结合,借用spring-data-mongodb来简化操作,那目前只能使用RC包,并且要剔除老版本包的依赖,自己添加符合要求的包

相信本次事务功能的更新,对MongoDB数据库模式设计会带来一些变化,嵌入文档,数组结合引用和仿真事务的一些设计会有所变化,一些棘手的问题会在一定程度上得到解决。

最后我们期待分片集群事务早点到来,广大Springboot的使用者们相信更期待spring-data-mongodb稳定版对事务支持早日推出。

JAVA语言系列

hzqiuxm阅读(347)评论(0)

前言

Oracle 官方宣称在每年的三月和九月将分别发布两次 Java 的新版本。这意味着 Java 11 将在五个月内面世。得益于如此频繁的发布速度,只要有新功能便会立即加入到新版的 Java 中,这样一来开发者就可以很快享有 Java 的新功能,而不必再像以前那样等待多年。

Java 10的发布相比于 Java 9 只有短短的六个月,然而我们在 Java 10中看到了大量的新功能。对于即将发布的 Java 11,我们同样期待能够有更多的新功能加入。

如今,这些新功能能够快速在 Java 新版本中发布,那么这些新功能到底有哪些?本文将重点介绍当前正在开发和提及的一些有趣的 Java 新功能。

新特性介绍

局部变量类型推断

Java 10 引入了局部变量类型推断的特性。该特性使用var关键字来定义局部变量,并让编译器根据初始化的方式来确定变量类型。如果你使用过 Java 的 lambda 表达式,那么这个特性对你来说并不会太陌生。

实际上,使用过lambda表达式的你可能已经耳熟能详了:

Function<String, String> foo = (s) -> s.toLowerCase();

上面这段代码你会发现没有必要把 s 定义为 String,它的类型会由编译器自动推断出来。使用 Java 10,你可以编写如下代码:

var list = new ArrayList<String>();

编译器会推断 list 是 ArrayList 类型。使用 var关键字可以帮助你减少一些代码的冗长度,尤其是泛型已经存在于变量初始化或者变量名称中时。它容易获得且易于理解,不必依靠 IDE 来告诉你变量的具体类型。

Java 11会进一步增强这一点,所以var也是 lambda 参数的合法类型,这意味着前面提到的 lambda 也可以写成 (var s) -> s.toLowerCase();。你可能会问为什么可以这么写,实际上,忽略类型的效果是一样的。一个主要的原因是,有一个类型的话意味着你可以注释得更好。

具体哪些情况应该使用var,请阅读:http://openjdk.java.net/projects/amber/LVTIstyle.html

原始字符串

目前另一项增强功能被提出并正在积极研究中,那就是原始字符串文字功能。在原始字符串中,字符串中的每个字符都按原样读取,包括换行符!这个功能对于那些需要大量转义或者跨越多行的字符串来说特别有用。例如,这可以是硬编码的 HTML 或 SQL 查询:

String html = "<html>\n" +
             "  <body>\n" +
             "    <p>Hello World.</p>\n" +
             "  </body>\n" +
             "</html>\n";

有了原始字符串文字特性,以上代码可以写成这样:

String html = `<html>
                <body>
                  <p>Hello World.</p>
                </body>
              </html>
             `;

在新的语法中当,反引号(`)用作原始字符串符号。如果你需要在字符串文本中使用反引号,则只需使用双反引号将字符串包围起来,或者三元、四元反引号也可以,只要开始和结束的反引号数量相同即可。

swich表达式

有关 switch 语句的多项改进正在进行,其中就包括了全开模式匹配。我对 switch 表达式充满期待。如果你曾经写过 switch 语句,那么代码可能是这样的:

int val;
switch (str) {
  case "foo": val = 1; break;
  case "bar": val = 2; break;
  case "baz": val = 3; break;
  default: val = -1;
}

有了 switch 表达式,上面的代码可以简化为:

int val = switch (str) {
  case "foo": break 1;
  case "bar": break 2;
  case "baz": break 3;
  default: break -1;
}

这意味着 break 语句能够获得 switch 表达式的结果值,就像 return 获得方法的返回值一样。在以上例子中,break 是 case 的唯一语句,如果继续简化,一种类似 lambda 语法的写法可以将上述代码改为:

int val = switch (str) {
  case "foo" -> 1;
  case "bar" -> 2;
  case "baz" -> 3;
  default -> -1;
}

如上所述,添加 switch 表达式是模式匹配的一个步骤,它使你不仅能够切换编译时常量,还可以是类型、条件检查等等。

目前 switch 表达式已经实现了,经过测试、代码审查和批准之后,相信将其发布到 JDK 中只是一个时间问题。离 Java 11发布还有足够长的时间,在 Java 11中加入 switch 表达式应该没问题。

其他特性

除了上述提到的语言变化之外,还有很多其他的东西可能在近期或者不远的将来出现在 Java 的新版本中。JEP 的提案越来越多,比如支持枚举泛型类型参数的增强枚举、前文已经提到的模式匹配、动态调用、值类型,等等。

如何将单个文件类作为脚本启动,类似于shell、Perl 或者 Python 脚本在命令行的运行方式一样?Java 后台引擎也进行了很多改进。这包括对 JVM 本身的更改,这样的更改对语言层面不会有影响,例如新的超低延迟 GC,用于与本机代码更好地实现互操作性的 API,改变内部类在 JVM 级别(基于嵌套的访问控制)以及一些其他的改进。

欢迎加入紫牛小筑

进入小筑关于作者