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

Cucumber简明教程

hzqiuxm阅读(361)评论(0)

Cucumber简明教程

入门篇

简单介绍

  • 用途:BDD(行为驱动开发)自动化测试产品,可以和目前很多语言结合在一起。
  • 有明确的可执行规范,自动化测试,记录系统的实际行为

  • 特点:它使用自然语言来描述测试,使得非程序员可以理解他们
  • 官方安装地址:https://cucumber.io/docs/installation/
  • 依赖包:
dependencies {
    testCompile 'io.cucumber:cucumber-java8:4.3.1'
    testCompile 'io.cucumber:cucumber-junit:4.3.1'
    testCompile 'info.cukes: cucumber-java:1.2.5' //2016年之前的包
}
  • 如果你用的是IEAD可以检查是否自动安装了该插件:Cucumber for Java plugin,如果没有自己手动安装下

Gherkin语法部分

概念介绍

  • Feature:一个测试用例集
  • SCENARIOS:类似一个具体测试用例或场景
  • STEPS:测试步骤,每个SCENARIOS包含多个STEPS,STEPS可以使用如下关键词:Gievn,When,Then,But,And等
  • Given:创建测试环境需要的前提条件
  • When:触发某个业务事件
  • Then:验证事件产生的结果
  • And:多个前提条件时使用,连接前一个条件,作为正向条件
  • But:多个前提条件时使用,连接前一个条件,作为反向条件
  • Background:执行SCENARIOS之前会执行Bankground,在一个Feature里只能有一个,作用上有点SCENARIOS,作为公共的步骤
  • arguments:步骤中传入参数,支持单个参数和复杂参数datatable
  • SCENARIOS Outline:重复场景数据

基本原理

对国家语言支持

  • 支持40多种语言
  • https://github.com/cucumber/cucumber/blob/master/gherkin/gherkin-languages.json

几个feature文件的例子

  • 一个加减法的例子
Feature: Basic Add Test

  Background: give x and y value
    Given x and y value

  Scenario: Addition
    Given x is 4 and y is 5
    When invoke add Method
    Then the result is 9

  Scenario: autoX
    Given x is 1
    When invoke autoX Method
    Then the result is 2

  Scenario: Sub
    And sub operation
    When invoke calculate button
    Then the result is x-y
  • 一个复杂参数例子
Feature: Complex data type

  Scenario: multiple then keywords
    Given the user account infomation
    Then we can found user "hzqiuxm", with password "123456", phone "13989461462"
    Then we can found user "linjiangxian", with password "123456", phone "13989461462"
    Then we can found user "queqiaoxian", with password "123456", phone "13989461462"
  Scenario:
    Given use complex data
    Then 验证下面的一些用户账号信息
      | name    | password | phone |
      | hzqiuxm | 123456   | 13989461462 |
      | linjiangxian | 123456   | 13989461462 |
      | queqiaoxian | 123456   | 13989461462 |

  • 一个公共场景和中文支持的例子
Feature: With Scentrio Outline

  Background: 公共的登录操作
    Given 进行用户登录来测试Scentrio Outline

  Scenario Outline: 用户名或密码错误
    When 使用错误用户名 "<UserName>" 和密码 "<Password>" 来登录
    Then 不正确的用户名或密码

    Examples:
      | UserName | Password |
      | hzqiuxm  | 123321   |
      | simon    | 123321   |

  Scenario: 正确的用户名密码登录测试
    When 使用正确用户名 "hzqiuxm", 密码 "123456" 来登录
    Then 用户名密码正确,登录成功

操作步骤详情部分

Step Definitions

  • 不要定义重复的或模棱两可(正则匹配多个符合)的steps
  • 正则表达式的使用(参数要使用是小括号分组,字符串要用双引号)
  • 想多种情况下匹配又不想抓取参数时,使用?:正则表达式来进行说明
  • DataTable数据格式:dataTable类型,用户自定义类型(https://github.com/cucumber/cucumber/tree/master/datatable),list map类型,list list类型
  • DataTable的compare来做一些结果对比,用于CURD或其它操作结果的一些检测

Tagging

  • 可以按照标签来执行场景用例,使用"@"符号,例子:@v1.0.0 @hzqiuxm
  • 使用~ 取反,放在一个不同""里表示and关系,放在同一个""里表示or关系
tags = {"@v1.0.0","not @santai"}  //执行v1.0.0标签并且不包含santai标签的场景用例

Hooks

  • 类似Junit中的before,after作用,执行顺序:Before Hook, Background,Scenario,After Hook
  • @Before代表之前,@After代表之后
  • 多个Before后After可以使用Order属性值来控制,默认是按照代码中顺序来执行
  • Before和After可以结合tag属性,控制其作用范围,默认情况下是所有feature都有效的,tag属性也只是and和or的关系

Options

  • 作用在启动类上,用来控制测试报告report输出、plugin插件、标签Tag选择、环境配置、Feature文件选择、严格模式等
@CucumberOptions(plugin = {"pretty","json:target/cucumber-report3.json"},tags = {"@v1.0.0","~@santai"})
  • Feature:只执行指定路径下的feature
  • gule:指定feature对应的测试类路径
  • tags:指定执行的标签规范
  • dryRun:不会真正的去执行steps,但会检查哪个feature没被实现
  • strict:严格模式下出现未实现的steps或断言失败就会失败报错
  • monochrome:影响控制台输出的样式效果
  • 支持的输出格式:html:target/Cucumber;json:target_json/Cucumber.json;junit:target_json/Cucumber_junit.xml

第三方整合

  • Jenkins整合,在Jenkins中安装cucumber 插件

自动构建后就会生产相关的报告

  • 结合Assured进行RESTful API测试
http://rest-assured.io/

https://www.baeldung.com/rest-assured-tutorial/

  • 结合selenium进行自动化测试
// https://mvnrepository.com/artifact/org.seleniumhq.selenium/selenium-java
compile group: 'org.seleniumhq.selenium', name: 'selenium-java', version: '3.14.0'

几个gradle下的cucumber插件

  • https://github.com/samueltbrown/gradle-cucumber-plugin
  • https://plugins.gradle.org/plugin/se.thinkcode.cucumber-runner
  • https://plugins.gradle.org/plugin/com.github.spacialcircumstances.gradle-cucumber-reporting
  • https://github.com/awbdallas/gradle-cucumber-jvm-plugin

个人总结

  • cucumber是敏捷开发团队常用的一种测试框架,它鼓励了系统开发环节中各个参与者来进行协作,其中也包括非技术人员
  • cucumber中的测试场景一般由纯自然语言来进行描述,很易懂,因此,非技术人员也可以来编写测试用例,然后通过技术人员来进行实现它。
  • Cucumber可以让人们用近似自然的语言去描述Feature和场景,根据Feature驱动开发。用作软件技术人员和非技术之间验收测试的桥梁。
  • 如果刚开始践行BDD,通常最好让开发人员编写
  • 如果只是用作测试自动化工具,可以由测试人员和开发人员编写
  • 特性(Feature)文件应该描述特性,而不是应用程序的组成部分,每个特性文件应有一个好的命名,并保持特性的专注
  • 避免特性与领域逻辑的不一致性,确保使用客户的领域语言。这一活动的最佳做法是让客户也参与编写故事。
  • 用组织代码的思想来组织你的特性与场景(Scenary)
  • 灵活使用标签Tag
  • 您的方案应该描述系统的预期行为,而不是实现。换句话说,它应该描述什么,而不是如何描述

Springboot教程系列(3)

hzqiuxm阅读(214)评论(0)

Springboot的外部化配置

外部化配置概念理解

什么是外部化配置

这个名词来源于Springboot官方文档的某一个章节名称,官方并没有对其下过准确的定义。一般研发人员,运维人员之间沟通时,经常会提及它。

有外部化配置也就有内部化配置,一般我们把在代码中枚举类,或硬性编码的部分称之为内部化配置。内部化配置缺少灵活性。

一个很熟悉的场景:一般公司的系统都会划分为开发(dev),测试(test),生产(prod)三个环境,每个环境的数据库、参数配置肯定是不一样的。一般公司都会借助spring的profile结合maven或gradle构建软件实现灵活的构建,部署好的软件系统自动对应到相应的环境,不用进行源码的修改。

还有springcloud的微服务实践中,通常也会引入总线配置方式,实现在系统不重启的情况下,实现修改某些参数或配置的目的。这些业务场景下配置其实就是外部化配置思想。抽象下概念外部化配置可以理解为:对于可扩展性应用系统,其内部组件是可配置化的,比如:认证信息、端口范围、线程池属性等。

  • 官方链接:https://docs.spring.io/spring-boot/docs/2.0.2.RELEASE/reference/htmlsingle/#boot-features-external-config

springboot中外部化配置

springboot官方提供了三种外部化配置应用方式:

  • Bean的@Value注入
  • Spring Eviroment读取
  • @ConfigurationProperties綁定到结构化对象

外部化配置实际应用

XML Bean的属性占位符

  • 比如在spring的xml配置文件中添加如下配置:
<bean id="user" class="com.hzqiuxm.configuration.domain.User">
<property name="id" value="${user.id}"/>
<property name="name" value="${user.name}"/>
</bean>
  • 另外一个xml配置文件内容:
<!-- 属性占位符配置-->
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
    <!-- Properties 文件 classpath 路径 -->
    <property name="location" value="classpath:/config/application.properties"/>
    <!-- 文件字符编码 -->
    <property name="fileEncoding" value="UTF-8"/>
</bean>
  • 在属性配置文件(application.properties)中添加
# 用户配置属性
user.id = 10
user.name =临江仙2018

创建对应的实体类user,添加对应字段的get/set方法后,启动Spirng启动到类获取到实体类User的Bean,可以看到其id和name字段的值为配置文件中配置的值

public class SpringXmlConfigPlaceholderBootstrap {
    public static void main(String[] args) {
        String[] locations = {"META-INF/spring/spring-context.xml", "META-INF/spring/user-context.xml"};
        ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext(locations);
        User user = applicationContext.getBean("user", User.class);
        System.err.println("用户对象 : " + user);
        // 关闭上下文
        applicationContext.close();
    }
}

输出结果:id=10, name='临江仙 2018',符合预期和我们配置文件中的值一致。这种方式是SpringFrame中普遍使用的方式,springboot出现后使用频率已经越来越少了。

接下来我们来搞点事情,换成springboot的方式启动

@ImportResource("META-INF/spring/user-context.xml") // 加载 Spring 上下文 XML 文件
@EnableAutoConfiguration
public class XmlPlaceholderExternalizedConfigurationBootstrap {

    public static void main(String[] args) {

        ConfigurableApplicationContext context =
                new SpringApplicationBuilder(XmlPlaceholderExternalizedConfigurationBootstrap.class)
                        .web(WebApplicationType.NONE) // 非 Web 应用
                        .run(args);

        User user = context.getBean("user", User.class);

        System.err.println("用户对象 : " + user);
        // 关闭上下文
        context.close();
    }
}

输出结果:id=10, name='hzqiuxm',为什么不符合预期?id是能对应上的,name的值确不对了?其实是 PropertySources顺序问题捣的鬼,这也是本文需要介绍的内容之一,相信看完文章后你就恍然大悟了。

@Value的注解方式

这种应用在平时开发中非常常见,很多开发人员都采用这种方式注入自定义的一些配置属性值。主要有三种注入方式:

  • 字段注入
  • 构造器注入
  • 方法注入

主要的用法举例:

@Value("${user.id}") //普通属性注入
private Long userId;
@Value("${user.age:${my.user.age:32}}") //嵌套属性注入,非常适合新老API兼容的设计,user.age代表老的,my.user.age代表新的,32代表默认的
private int age;
@Value("#{'${list}'.split(',')}") //list注入
private List<String> list;
@Value("#{${maps}}")  //map注入
private Map<String,String> maps;
--------------------------------------
对应配置文件:
user.id:1
my.user.age:32
list: topic1,topic2,topic3
maps: "{key1: 'value1', key2: 'value2'}"

Eviroment方式读取

  • 方法/构造器依赖注入
@Override
public void setEnvironment(Environment environment) {
    if (this.environment != environment) {
        throw new IllegalStateException();
    }
}
  • @Autowired依赖注入
@Autowired
@Qualifier(ENVIRONMENT_BEAN_NAME)
private Environment environment;
  • EviromentAware 接口回调
    实现 EnvironmentAwarek接口

  • BeanFactory 依赖查找Environment
    实现 BeanFactoryAware接口

@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
    if (this.environment != beanFactory.getBean(ENVIRONMENT_BEAN_NAME, Environment.class)) {
        throw new IllegalStateException();
    }
}

三者的执行顺序:1 @Autowired; 2 BeanFactoryAware ;3 EviromentAware

@ConfigurationProperties Bean绑定

  • 类级别注入

  • @Bean方法声明

  • 嵌套类型绑定

外部化配置扩展

定义外部化属性源

  • PropertySources的顺序问题:官方提供的参考如下(Springboot版本需要1.5以上,低版本会缺少部分)


- 什么是PropertySource
带有名称的属性源,Properties文件、Map、YAML 文件等都可以称之为PropertySource。

  • 什么是Eviroment抽象
    Environment与PropertySources可以看成是一一对应的关系;PropertySource与PropertySources从单词的单数和复数关系也可以看的出是 1 对 多的关系;ConfigurableEnvironment与MutablePropertySources相对应。

PropertySources属性源使用时机

  • Spring Framework 中,尽量在org.springframework.context.support.AbstractApplicationContext#prepareBeanFactory方法前初始化。
  • Spring Boot 中,尽量在org.springframework.boot.SpringApplication#refreshContext(context)方法前初始化。

扩展外部化配置属性源

基于 SpringApplicationRunListener#environmentPrepared 扩展外部化配置属性源
  • 实现两个接口:SpringApplicationRunListener, Ordered
  • META-INF下新建spring.factories文件,添加如下配置
# Run Listeners
org.springframework.boot.SpringApplicationRunListener=\
com.imooc.diveinspringboot.externalized.configuration.configuration.ExtendPropertySourcesRunListener
  • 重写SpringApplicationRunListener#environmentPrepared 和 getOrder
@Override
public void environmentPrepared(ConfigurableEnvironment environment) {

    MutablePropertySources propertySources = environment.getPropertySources();
    Map<String, Object> source = new HashMap<>();
    source.put("user.id", "0"); //设置编号为0
    MapPropertySource propertySource = new MapPropertySource("from-environmentPrepared", source);
    propertySources.addFirst(propertySource);
}

@Override
public int getOrder() {
    return new EventPublishingRunListener(application,args).getOrder() + 1;//返回排在默认的后面
}

各个文件中的配置如下:

自定义environmentPrepared中: 0
application.properties : 10
META-INF/default.properties : 11
  • 定义引导类ExtendPropertySourcesBootstrap,并模拟一个Command line arguments(88) 和Default properties(99) 配置方式
@EnableAutoConfiguration
@Configuration
@PropertySource(name = "from default.properties", value = "classpath:META-INF/spring/default.properties")
public class ExtendPropertySourcesBootstrap {
    public static void main(String[] args) {
        ConfigurableApplicationContext context =
                new SpringApplicationBuilder(ExtendPropertySourcesBootstrap.class)
                        .web(WebApplicationType.NONE) // 非 Web 应用
                        .properties("user.id=99")        // Default properties
                        .run(of("--user.id=88")); // Command line arguments.
        // 获取 Environment 对象
        ConfigurableEnvironment environment = context.getEnvironment();
        System.err.printf("用户id : %d\n", environment.getProperty("user.id", Long.class));
        environment.getPropertySources().forEach(propertySource -> {
            System.err.printf("PropertySource[名称:%s] : %s\n", propertySource.getName(), propertySource);
        });

        // 关闭上下文
        context.close();
    }
    private static <T> T[] of(T... args) {
        return args;
    }

}

根据上一节提到的PropertySources的顺序问题,我们可以猜测 我们自定义的优先级应该最高,所以结果应该是0
输出结果如下,符合预期:

用户id : 0
PropertySource[名称:configurationProperties] : ConfigurationPropertySourcesPropertySource {name='configurationProperties'}
PropertySource[名称:from-environmentPrepared] : MapPropertySource {name='from-environmentPrepared'}
PropertySource[名称:commandLineArgs] : SimpleCommandLinePropertySource {name='commandLineArgs'}
PropertySource[名称:systemProperties] : MapPropertySource {name='systemProperties'}
PropertySource[名称:systemEnvironment] : OriginAwareSystemEnvironmentPropertySource {name='systemEnvironment'}
PropertySource[名称:random] : RandomValuePropertySource {name='random'}
PropertySource[名称:applicationConfig: [classpath:/config/application.properties]] : OriginTrackedMapPropertySource {name='applicationConfig: [classpath:/config/application.properties]'}
PropertySource[名称:from default.properties] : ResourcePropertySource {name='from default.properties'}
PropertySource[名称:defaultProperties] : MapPropertySource {name='defaultProperties'}

类似的我们还可以重载:contextPrepared和contextLoaded方式来实现自定义的配置。三个方法的执行顺序为:
1 environmentPrepared;2 contextPrepared;3 contextLoaded;因为我们采用的是addFirst方法,先执行会被后执行的覆盖,三者优先级是倒过来的,这点需要特别注意

执行顺序在SpringApplication#run中可以看到,可以翻看之前的一篇文章获得具体详情,这里给标注下源码和相应位置

基于 SpringApplicationRunListener#contextPrepared 扩展外部化配置属性源
  • 参考SpringApplicationRunListener#environmentPrepared
基于 SpringApplicationRunListener#contextLoaded 扩展外部化配置属性源
  • 参考SpringApplicationRunListener#environmentPrepared
基于 ApplicationEnvironmentPreparedEvent 扩展外部化配置属性源
  • 实现ApplicationListener接口,并重载onApplicationEvent方法
@Override
public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
    ConfigurableEnvironment environment = event.getEnvironment();
    MutablePropertySources propertySources = environment.getPropertySources();
    Map<String, Object> source = new HashMap<>();
    source.put("user.id", "9");//设置程9
    MapPropertySource propertySource = new MapPropertySource("from-ApplicationEnvironmentPreparedEvent", source);
    propertySources.addFirst(propertySource);
}
  • META-INF下新建spring.factories文件,添加如下配置
# Event Listeners
org.springframework.context.ApplicationListener=\
com.imooc.diveinspringboot.externalized.configuration.configuration.ExtendPropertySourcesEventListener
  • 启动之前引导类ExtendPropertySourcesBootstrap
    由于ApplicationListener是在SpringApplication构造的时候调用的,执行顺序肯定在SpringApplicationRunListener相关方法之前执行,根据执行被覆盖的原则,输出的值应该是0

  • 输出结果如下:

用户id : 0
PropertySource[名称:configurationProperties] : ConfigurationPropertySourcesPropertySource {name='configurationProperties'}
PropertySource[名称:from-environmentPrepared] : MapPropertySource {name='from-environmentPrepared'}
PropertySource[名称:from-ApplicationEnvironmentPreparedEvent] : MapPropertySource {name='from-ApplicationEnvironmentPreparedEvent'}
PropertySource[名称:commandLineArgs] : SimpleCommandLinePropertySource {name='commandLineArgs'}
PropertySource[名称:systemProperties] : MapPropertySource {name='systemProperties'}
PropertySource[名称:systemEnvironment] : OriginAwareSystemEnvironmentPropertySource {name='systemEnvironment'}
PropertySource[名称:random] : RandomValuePropertySource {name='random'}
PropertySource[名称:applicationConfig: [classpath:/config/application.properties]] : OriginTrackedMapPropertySource {name='applicationConfig: [classpath:/config/application.properties]'}
PropertySource[名称:from default.properties] : ResourcePropertySource {name='from default.properties'}
PropertySource[名称:defaultProperties] : MapPropertySource {name='defaultProperties'}

符合我们分析的结果

基于 EnvironmentPostProcessor 扩展外部化配置属性源
  • 实现 EnvironmentPostProcessor接口和Ordered接口,分别重载它们的postProcessEnvironment和getOrder方法
  • 在ApplicationEnvironmentPreparedEvent之前执行,所以如果实现了其它的自定义方式,它就会被覆盖
  • META-INF下新建spring.factories文件,添加如下配置
# EnvironmentPostProcessor
org.springframework.boot.env.EnvironmentPostProcessor=\
com.imooc.diveinspringboot.externalized.configuration.processor.ExtendPropertySourcesEnvironmentPostProcessor
  • 具体代码SpringApplicationRunListener#environmentPrepared
基于 ApplicationContextInitializer 扩展外部化配置属性源
  • 在ApplicationContextInitializer上下文初始化的时候进行配置,在SpringApplicationRunListener#environmentPrepared之后执行,所以会覆盖environmentPrepared
  • 不会覆盖contextPrepared和contextLoaded;
  • META-INF下新建spring.factories文件,添加如下配置
# ApplicationContextInitializer
org.springframework.context.ApplicationContextInitializer=\
com.imooc.diveinspringboot.externalized.configuration.initializer.ExtendPropertySourcesApplicationContextInitializer
  • 具体代码参考SpringApplicationRunListener#environmentPrepared

各自定义PropertySource优先级(从高到低)

PropertySource[名称:from-contextLoaded] : MapPropertySource {name='from-contextLoaded'}
PropertySource[名称:from-contextPrepared] : MapPropertySource {name='from-contextPrepared'}
PropertySource[名称:from-ApplicationContextInitializer] : MapPropertySource {name='from-ApplicationContextInitializer'}
PropertySource[名称:configurationProperties] : ConfigurationPropertySourcesPropertySource {name='configurationProperties'}
PropertySource[名称:from-environmentPrepared] : MapPropertySource {name='from-environmentPrepared'}
PropertySource[名称:from-ApplicationEnvironmentPreparedEvent] : MapPropertySource {name='from-ApplicationEnvironmentPreparedEvent'}
PropertySource[名称:from-EnvironmentPostProcessor] : MapPropertySource {name='from-EnvironmentPostProcessor'}
PropertySource[名称:commandLineArgs] : SimpleCommandLinePropertySource {name='commandLineArgs'}
PropertySource[名称:systemProperties] : MapPropertySource {name='systemProperties'}
PropertySource[名称:systemEnvironment] : OriginAwareSystemEnvironmentPropertySource {name='systemEnvironment'}
PropertySource[名称:random] : RandomValuePropertySource {name='random'}
PropertySource[名称:applicationConfig: [classpath:/config/application.properties]] : OriginTrackedMapPropertySource {name='applicationConfig: [classpath:/config/application.properties]'}
PropertySource[名称:from classpath:META-INF/default.properties] : ResourcePropertySource {name='from classpath:META-INF/default.properties'}
PropertySource[名称:defaultProperties] : MapPropertySource {name='defaultProperties'}

实时扩展外部化配置属性源:Eviroment支持,@Value和@ConfigurationProperties 不支持
理解清楚了各种自定义外部化配置的优先级,可以在自己设计框架的时候控制不想被开发人员影响到的配置。

Springboot教程系列(2)

hzqiuxm阅读(477)评论(3)

小谈SpringApplication启动

基于Springboot2.0+版本

前言

在Springboot装配入门指南中我们简单了解了下组合注解@SpringbootApplication,它的本质是一个配置角色注解模式,同时开启了自动装配等功能。那我们是如何启动一个Springboot项目的呢?

使用Spring官方提供的网页: https://start.spring.io/ 中生成的项目,都会自动生成一个启动类,该启动类都会使用@SpringbootApplication进行标注,main方法中会统一使用SpringApplication.run()方法来启动。

我们今天的主角就是SpringApplication,谈谈它的启动和运行过程,其中会涉及到上下文应用加载、应用事件加载、应用监听器,应用推断、引导类推断、应用广播等概念

SpringApplication启动

自定义启动

调用run方法启动,例如:SpringApplication.run(MocApplication.class, args);这个大家都很熟悉了,那如果我们自定义启动怎么去实现呢?

大概的步骤是定义一个SpringApplication实例,然后运行时传入run方法需要的两个参数即可。
我们自定义时有两种API方式进行选择:一种是通过SpringApplicationAPI 调整,一种是通过SpringApplicationBuilderAPI调整。二者实现方式分别如下:

第一种:SpringApplicationAPI 方式

public class MySpringApplication {
    public static void main(String[] args) {
        Set<String> sources = new HashSet();
        sources.add(ApplicationConfiguration.class.getName());
        SpringApplication springApplication = new SpringApplication();
        springApplication.setSources(sources);
        springApplication.setBannerMode(Banner.Mode.CONSOLE);//banner打印模式设置
        springApplication.setWebApplicationType(WebApplicationType.NONE);//web应用类型设置
        springApplication.setAdditionalProfiles("dev");//环境设置
        springApplication.setHeadless(true);//图形界面设置
        springApplication.run(args); //启动
    }
    @SpringBootApplication
    public static class ApplicationConfiguration {
        //故意不使用MySpringApplication类作为run的参数
    }
}

第二种:SpringApplicationBuilderAPI 方式,使用了生成器模式书写起来比较流畅

public class MySpringApplication {
    public static void main(String[] args) {
        new SpringApplicationBuilder(MySpringApplication.class)
            .bannerMode(Banner.Mode.CONSOLE)
            .web(WebApplicationType.NONE)
            .profiles("dev")
            .headless(true)
            .run(args);
    }
}

启动run方法源码简单说明

两种方式其实没有什么大的差别,只是书写的时候第二种采用了builder设计模式。我们跟踪原来可以发现,最后run方法返回的是一个ConfigurableApplicationContext,run方法的主要源码如下:

StopWatch stopWatch = new StopWatch(); //构造一个观察器,用来记录时间
  stopWatch.start();//启动观察器
  ConfigurableApplicationContext context = null; //最终返回的应用上下文
  Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();  //启动异常报告
  configureHeadlessProperty();//设置java.awt.headless系统属性为true (代表没有图形化界面)
  SpringApplicationRunListeners listeners = getRunListeners(args);// 获取应用监听器(注解一)
  listeners.starting(); //启动监听器
  try {
   ApplicationArguments applicationArguments = new DefaultApplicationArguments( //构造一个应用程序参数持有类
     args);
   ConfigurableEnvironment environment = prepareEnvironment(listeners,applicationArguments);// 准备Environment(注解二)
   configureIgnoreBeanInfo(environment);//过滤指定配置的Bean
   Banner printedBanner = printBanner(environment);//按指定的方式打印banner
   context = createApplicationContext(); //创建一个Spring应用上下文,即我们平时说的Spring容器(注解三)
   exceptionReporters = getSpringFactoriesInstances(
     SpringBootExceptionReporter.class,
     new Class[] { ConfigurableApplicationContext.class }, context);//准备异常报告
   prepareContext(context, environment, listeners, applicationArguments,printedBanner);//上下文前置处理
   refreshContext(context); //上下文刷新
   afterRefresh(context, applicationArguments);//上下文后置处理
   stopWatch.stop(); //停止观察器的计时
   ... ...
   listeners.started(context); //监听已经初始化完成启动的上下文
    ... ...
   listeners.running(context); //监听正在运行中的上下文
   ... ...
  return context; //返回Spring上下文容器

关于上面源码的额外注解会下面章节进行额外的说明,这里只是对启动过程有个大概了解,然后对主要的注解步骤有个印象。

配置Springboot Bean源

Java 配置 Class 或 XML 上下文配置文件集合,用于 Spring Boot BeanDefinitionLoader读取,并且将配置源解析加载为Spring Bean 定义,数量:一个或多个以上。

一般有两种来实现:一种是采用java配置class方式,就是使用 Spring 模式注解所标注的类,如@Configuration;另一种是传统XML方式,一般我们在新项目中使用第一种方式,无法支持或兼容老的XML配置可以使用@Import来导入XML配置文件。

推断Web应用类型

什么是推断Web应用类型?我们知道在Springboot2.0中(其实是Spring5.0中)加入了Reactive的异步编程模式,用来替代原来传统的servlet方式。所以我们的应用可以是新型的REACTIVE类型,可以是传统的SERVLET类型,还可以是不属于前二者的非WEB类型。

我们可以像之前例子中手动指定某个类型比如:WebApplicationType.NONE,你不指定的话,SpringApplication是会自动推断的。

怎么自动推断呢?在SpringApplication的构造函数中,我们可以看到一个方法 WebApplicationType.deduceFromClasspath(),这个方式就根据classpath中是否包含特定的类来推断属于哪一种,都没有特定类的时候为非WEB应用,SERVLET和REACTIVE以SERVLET为优先,具体逻辑大家可以查看源码。

三者对应关系类型如下:

Web Reactive:WebApplicationType.REACTIVE
Web Servlet:WebApplicationType.SERVLET
非 Web:WebApplicationType.NONE

推断引导类

除了对应用类型进行推断外,SpringApplication还会进行引导类(Main Class)推断。源码如下:

private Class deduceMainApplicationClass() {
  try {
   StackTraceElement[] stackTrace = new RuntimeException().getStackTrace();
   for (StackTraceElement stackTraceElement : stackTrace) {
    if ("main".equals(stackTraceElement.getMethodName())) {
     return Class.forName(stackTraceElement.getClassName());
    }
   }
  }
  catch (ClassNotFoundException ex) {
  }
  return null;
 }

```

从上面源码可以看出,它是根据 Main 线程执行堆栈信息来判断实际的引导类的,就像我们在自定义SpringApplicationAPI 方式时,故意将配置注解标注在新建了的一个类上。最后也是可以启动成功的。


#### 加载应用上下文初始器

在SpringApplication构造器中,除了上面介绍的二个推断外,另外一个重要的操作就是加载应用上下文初始器:ApplicationContextInitializer。其原理是利用 Spring 工厂加载机制,实例化ApplicationContextInitializer实现类,并排序对象集合。相关源码如下:
```
private  Collection getSpringFactoriesInstances(Class type,
   Class[] parameterTypes, Object... args) {
  ClassLoader classLoader = getClassLoader();
  Set<String> names = new LinkedHashSet<>(
    SpringFactoriesLoader.loadFactoryNames(type, classLoader));
  List<T> instances = createSpringFactoriesInstances(type, parameterTypes,
    classLoader, args, names);
  AnnotationAwareOrderComparator.sort(instances);
  return instances;
 }

实现类:SpringFactoriesLoader,在固定路径下配置相关资源:META-INF/spring.factories,顺序的设置依赖 AnnotationAwareOrderComparator#sort

加载应用事件监听器

我们自定义事件监听器的话也是利用 Spring 工厂加载机制,实例化ApplicationListener实现类,并采用排序对象AnnotationAwareOrderComparator来设置加载的顺序。下面是二个自定义ApplicationListener实现例子,顺序设置上分别采用@Order注解方式和实现Ordered接口方式。

第一种,@Order注解方式:

@Order(Ordered.HIGHEST_PRECEDENCE) //优先级最高,对应最小整数
public class OneApplicationListener implements ApplicationListener<ContextRefreshedEvent> {
    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        System.out.println("One: " + event.getApplicationContext().getId()
                + " , timestamp : " + event.getTimestamp());
    }
}

第二种,Ordered接口方式

public class TwoApplicationListener implements ApplicationListener<ContextRefreshedEvent>,Ordered {
    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        System.out.println("Two: " + event.getApplicationContext().getId()
                + " , timestamp : " + event.getTimestamp());
    }
    @Override
    public int getOrder() {
        return Ordered.LOWEST_PRECEDENCE; //最低优先级,对应最大整数
    }
}

对应的配置文件spring.factories内容:

org.springframework.context.ApplicationListener=\
com.imooc.diveinspringboot.listener.TwoApplicationListener,\
com.imooc.diveinspringboot.listener.OneApplicationListener

启动我们之前自定义的SpringApplication应用后,我们可以看到控制台日志中会输出两个应用事件监听器加载信息,虽然One配置在Two后面,但是最后打印的顺序是根据Ordered设置的值来决定的。

SpringApplication运行

至此介绍完了SpringApplication准备阶段的一些主要事情,接下来主要介绍运行阶段(run方法中)的一些主要事情

加载运行监听器(注解一)

类似ApplicationListener的实现,也利用 Spring 工厂加载机制,读取SpringApplicationRunListener对象集合,并且封装到组合类SpringApplicationRunListeners。

我们查看springboot的spring.factories可以看到SpringApplicationRunListeners 内部实现是EventPublishingRunListener,它利用 Spring Framework 事件API ,广播 Spring Boot 事件。

如果我们要自定义实现的话可以仿照EventPublishingRunListener去实现。
SpringApplicationRunListeners监听多个运行状态方法,具体如下:

监听事件

完成一个自定义监听事件的步骤分为三步: 1. 定义事件 2.注册到监听器 3.发布事件
Spring 应用事件可以分为两类:普通应用事件(ApplicationEvent)和应用上下文事件(ApplicationContextEvent),后者继承了前者。一般我们实现的时候继承前者即可。下面是一个自定义事件的例子:

public class MyEvent extends ApplicationEvent {
    public MyEvent(Object source) {
        super(source);
    }
}

注册到监听器有两种方式:接口编程(实现ApplicationListener,它是一个函数式接口)和注解编程(@EventListener),二者例子如下:
第一种,接口编程方式;

AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
// 注册应用事件监听器
        context.addApplicationListener(event -> {
            System.out.println("监听到事件: " + event);
        });

第二种,注解编程方式;

@Component
public class MyEventListener {
    @EventListener //注册应用事件监听器
    @Async //异步执行
    public void onListener(MyEvent event){
            System.out.println("The event is : " + event);
        }
    }
}

最后一步是发布事件,事件的刷新和关闭也属于一种事件,在上面第一种实现方式中增加下面的代码会触发5个事件:

 //会监听到5个事件
        context.refresh(); // 刷新上下文
        // 发送事件
        context.publishEvent("hello");
        context.publishEvent("world");
        context.publishEvent(new ApplicationEvent("hzqiuxm") {
        });
        context.close();  // 关闭上下文

EventPublishingRunListener监听方法与 Spring Boot 事件对应关系:

最后提下EventPublishingRunListener实现类中进行Spring 应用事件广播是通过SimpleApplicationEventMulticaster类来实现的,它的执行方式有同步和异步两种。
SimpleApplicationEventMulticaster实现的是Spring 广播器接口:ApplicationEventMulticaster。

创建应用上下文(注解三)

在注解三的代码处生成我们最终得到的应用上下文(ConfigurableApplicationContext)容器。
根据准备阶段的推断 Web 应用类型去创建对应的ConfigurableApplicationContext实例,不同类型对应的应用上下文也不同,具体关系如下:

Web Reactive:AnnotationConfigReactiveWebServerApplicationContext
Web Servlet:AnnotationConfigServletWebServerApplicationContext
非 Web:AnnotationConfigApplicationContext

具体细节这里就不叙述了。

创建Environment(注解二)

这里主要是根据准备阶段的推断 Web 应用类型创建对应的ConfigurableEnvironment实例,也有三种类型:

Web Reactive:StandardEnvironmentWeb
Servlet:StandardServletEnvironment
非 Web:StandardEnvironment

总结

经过介绍,我们大致清楚SpringApplication的启动主要分为两个阶段:启动阶段(准备阶段由构造方法完成)与运行阶段(调用run方法完成)。

启动阶段其中主要涉及到了web类型推断、引导类推断、初始化器以及监听器加载这几个概念,我们如果要实现自定义的监听器,它们都需要利用Spring工厂加载机制,再通过META-INF/spring.factories完成定义。

运行阶段其中主要有一个SpringApplicationRunListeners的概念,它作为Spring Boot容器初始化时各阶段事件的中转器,将事件派发给感兴趣的Listeners(启动阶段得到的)。这些阶段性事件将容器的初始化过程给构造起来,提供了比较强大的可扩展性。

如果作为应用开发者要对Spring Boot容器的启动阶段进行扩展会有哪些方式呢?我想至少有下面几种:

  • 自定义启动类
  • 自定义初始化器
  • 自定义监听器

玩转设计模式系列(1)

hzqiuxm阅读(261)评论(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教程系列(1)

hzqiuxm阅读(390)评论(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阅读(536)评论(2)

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阅读(421)评论(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阅读(436)评论(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阅读(595)评论(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阅读(514)评论(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的操作按钮,我们可以通过这里对镜像进行删除了。当然你也可以进入到容器的保存目录,手动去删除。

欢迎加入紫牛小筑

进入小筑关于作者