微服務架構技術棧:程式設計師必須掌握的微服務架構框架詳細解析

攻城獅Chova發表於2021-07-17

主要技術

  • 基礎框架: springboot
  • 微服務架構: dubbo,springboot cloud
  • ORM框架: mybatis plus
  • 資料庫連線池: Alibaba Druid
  • 閘道器(統一對外介面 ): zuul
  • 快取: redis
  • 註冊中心: zookeeper,eureka
  • 訊息佇列:
  • 作業排程框架: Quartz
  • 分散式檔案系統:
  • 介面測試框架: Swagger2
  • 資料庫版本控制: Liquibase (flyway)
  • 部署: docker
  • 持續整合: jenkins
  • 自動化測試: testNG

ORM框架-Mybatis Plus

MyBatis Plus是在 MyBatis 的基礎上只做增強不做改變,可以簡化開發,提高效率.

Mybatis Plus核心功能

  • 支援通用的 CRUD,程式碼生成器與條件構造器
  • 通用CRUD: 定義好Mapper介面後,只需要繼承 BaseMapper介面即可獲得通用的增刪改查功能,無需編寫任何介面方法與配置檔案
  • 條件構造器: 通過EntityWrapper(實體包裝類),可以用於拼接SQL語句,並且支援排序,分組查詢等複雜的 SQL
  • 程式碼生成器: 支援一系列的策略配置與全域性配置,比 MyBatis 的程式碼生成更好用
    BaseMapper介面中通用的 CRUD 方法:
    在這裡插入圖片描述

MyBatis Plus與SpringBoot整合

  • 資料庫USER
DROP TABLE IF EXISTS user;
CREATE TABLE user(
  id bigint(20) DEFAULT NULL COMMENT '唯一標示',
  code varchar(20) DEFAULT NULL COMMENT '編碼',
  name varchar(64) DEFAULT NULL COMMENT '名稱',
  status char(1) DEFAULT 1 COMMENT '狀態 1啟用 0 停用',
  gmt_create datetime DEFAULT CURRENT_TIMESTAMP COMMENT '建立時間',
  gmt_modified datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改時間'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
  • pom.xml依賴
 <!--mybatis plus -->
<dependency>
	<groupId>com.baomidou</groupId>
	<artifactId>mybatisplus-spring-boot-starter</artifactId>
	<version>1.0.5</version>
</dependency>
<dependency>
	<groupId>com.baomidou</groupId>
	<artifactId>mybatis-plus</artifactId>
	<version>2.1.9</version>
</dependency>
  • spring-mybatis.xml配置檔案
    也可以直接使用@Bean的方式進行或者通過application配置檔案進行
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">

    <!--建立jdbc資料來源 這裡直接使用阿里的druid資料庫連線池 -->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
        <property name="driverClassName" value="${mysql.driver}"/>
        <property name="url" value="${mysql.url}"/>
        <property name="username" value="${mysql.username}"/>
        <property name="password" value="${mysql.password}"/>
        <!-- 初始化連線大小 -->
        <property name="initialSize" value="0"/>
        <!-- 連線池最大使用連線數量 -->
        <property name="maxActive" value="20"/>
        <!-- 連線池最大空閒 -->
        <property name="maxIdle" value="20"/>
        <!-- 連線池最小空閒 -->
        <property name="minIdle" value="0"/>
        <!-- 獲取連線最大等待時間 -->
        <property name="maxWait" value="60000"/>

        <property name="validationQuery" value="${validationQuery}"/>
        <property name="testOnBorrow" value="false"/>
        <property name="testOnReturn" value="false"/>
        <property name="testWhileIdle" value="true"/>

        <!-- 配置間隔多久才進行一次檢測,檢測需要關閉的空閒連線,單位是毫秒 -->
        <property name="timeBetweenEvictionRunsMillis" value="60000"/>
        <!-- 配置一個連線在池中最小生存的時間,單位是毫秒 -->
        <property name="minEvictableIdleTimeMillis" value="25200000"/>

        <!-- 開啟removeAbandoned功能 -->
        <property name="removeAbandoned" value="true"/>
        <!-- 1800秒,也就是30分鐘 -->
        <property name="removeAbandonedTimeout" value="1800"/>
        <!-- 關閉abanded連線時輸出錯誤日誌 -->
        <property name="logAbandoned" value="true"/>

        <!-- 監控資料庫 -->
        <property name="filters" value="mergeStat"/>
    </bean>

    <!-- (事務管理)transaction manager, use JtaTransactionManager for global tx -->
    <bean id="transactionManager"
          class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource" />
    </bean>

    <!-- 可通過註解控制事務 -->
    <tx:annotation-driven transaction-manager="transactionManager"/>

    <!--mybatis-->
    <bean id="sqlSessionFactory" class="com.baomidou.mybatisplus.spring.MybatisSqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
		<!-- 自動掃描mapper.xml檔案,支援萬用字元 -->
        <property name="mapperLocations" value="classpath:mapper/**/*.xml"/>
		<!-- 配置檔案,比如引數配置(是否啟動駝峰等)、外掛配置等 -->
        <property name="configLocation" value="classpath:mybatis/mybatis-config.xml"/>
		<!-- 啟用別名,這樣就無需寫全路徑類名了,具體可自行查閱資料 -->
        <property name="typeAliasesPackage" value="cn.lqdev.learning.springboot.chapter9.biz.entity"/>
        <!-- MP 全域性配置注入 -->
        <property name="globalConfig" ref="globalConfig"/>
    </bean>
    <bean id="globalConfig" class="com.baomidou.mybatisplus.entity.GlobalConfiguration">
        <!--
            AUTO->`0`("資料庫ID自增")QW
             INPUT->`1`(使用者輸入ID")
            ID_WORKER->`2`("全域性唯一ID")
            UUID->`3`("全域性唯一ID")
        -->
        <property name="idType" value="3" />
    </bean>
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
	<!-- 自動掃描包路徑,介面自動註冊為一個bean類 -->
        <property name="basePackage" value="cn.lqdev.learning.springboot.chapter9.biz.dao"/>
    </bean>

</beans>
  • 編寫啟動類,應用啟動時自動載入配置xml檔案
@Configuration
@ImportResource(locations = {"classpath:/mybatis/spring-mybatis.xml"})
//@MapperScan("cn.lqdev.learning.springboot.chapter9.biz.dao")
//@EnableTransactionManagement
public class MybatisPlusConfig {
}

MyBatis Plus整合Spring

  • 資料表結構
DROP TABLE IF EXISTS tbl_employee;
CREATE TABLE tbl_employee(
  id int(11) NOT NULL AUTO_INCREMENT,
  last_name varchar(50) DEFAULT NULL,
  email varchar(50) DEFAULT NULL,
  gender char(1) DEFAULT NULL,
  age int(11) DEFAULT NULL,
  PRIMARY KEY (id)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8;
  • pom.xml
    <dependencies>
        <!-- MP -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus</artifactId>
            <version>2.3</version>
        </dependency>
        <!-- 測試 -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>
        <!-- 資料來源 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.10</version>
        </dependency>
        <!-- 資料庫驅動 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.39</version>
        </dependency>
        <!-- Spring 相關 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>4.3.9.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-orm</artifactId>
            <version>4.3.9.RELEASE</version>
        </dependency>
    </dependencies>
  • MyBatis全域性配置檔案mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<!-- 不作任何配置 -->
<configuration />
  • 資料來源db.properties
jdbc.url=jdbc:mysql://localhost:3306/mp
jdbc.username=mp
jdbc.password=mp
  • Spring 配置檔案applicationContext.xml
    <!-- 資料來源 -->
    <context:property-placeholder location="classpath:db.properties"/>
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="url" value="${jdbc.url}"></property>
        <property name="username" value="${jdbc.username}"></property>
        <property name="password" value="${jdbc.password}"></property>
    </bean>

    <!-- MP 提供的 MybatisSqlSessionFactoryBean -->
    <bean id="sqlSessionFactoryBean" 
          class="com.baomidou.mybatisplus.spring.MybatisSqlSessionFactoryBean">
        <!-- 資料來源 -->
        <property name="dataSource" ref="dataSource"></property>
        <!-- mybatis 全域性配置檔案 -->
        <property name="configLocation" value="classpath:mybatis-config.xml"></property>
        <!-- 別名處理 -->
        <property name="typeAliasesPackage" value="com.jas.bean"></property>
        <!-- 注入全域性MP策略配置 -->
        <property name="globalConfig" ref="globalConfiguration"></property>
        <!-- 外掛註冊 -->
        <property name="plugins">
            <list>
                <!-- 註冊分頁外掛 -->
                <bean class="com.baomidou.mybatisplus.plugins.PaginationInterceptor" />
                <!-- 注入 SQL 效能分析外掛,建議在開發環境中使用,可以在控制檯檢視 SQL 執行日誌 -->
                <bean class="com.baomidou.mybatisplus.plugins.PerformanceInterceptor">
                    <property name="maxTime" value="1000" />
                    <!--SQL 是否格式化 預設false-->
                    <property name="format" value="true" />
                </bean>
            </list>
        </property>
    </bean>

    <!-- 定義 MybatisPlus 的全域性策略配置-->
    <bean id ="globalConfiguration" class="com.baomidou.mybatisplus.entity.GlobalConfiguration">
        <!-- 在 2.3 版本以後,dbColumnUnderline 預設值是 true -->
        <property name="dbColumnUnderline" value="true"></property>
        <!-- 全域性的主鍵策略 -->
        <property name="idType" value="0"></property>
        <!-- 全域性的表字首策略配置 -->
        <property name="tablePrefix" value="tbl_"></property>
    </bean>
    
    <!-- 配置mybatis 掃描mapper介面的路徑 -->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.jas.mapper"></property>
    </bean>

MyBatis Plus使用示例

  • 實體類Employee
@TableName(value = "tbl_employee")
public class Employee {
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;
    @TableField(value = "last_name")
    private String lastName;
    private String email;
    private Integer gender;
    private Integer age;

    public Employee() {
        super();
    }
    
    public Employee(Integer id, String lastName, String email, Integer gender, Integer age) {
        this.id = id;
        this.lastName = lastName;
        this.email = email;
        this.gender = gender;
        this.age = age;
    }
    // 省略 set、get 與 toString() 方法
  • mapper介面
/**
* 不定義任何介面方法
*/
public interface EmployeeMapper extends BaseMapper<Employee> {}
  • 在測試類中生成測試的mapper物件
    private ApplicationContext context = 
            new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
            
    private EmployeeMapper employeeMapper = 
            context.getBean("employeeMapper", EmployeeMapper.class);
  • 查詢:
    @Test
    public void getEmpByIdTest() {
        Employee employee = employeeMapper.selectById(1);
        
        System.out.println(employee);
    }
  • 分頁查詢:
    @Test
    public void getEmpByPage() {
        Page<?> page = new Page<>(1, 5);
        List<Employee> list = employeeMapper.selectPage(page, null);
        
        System.out.println("總記錄數:" + page.getTotal());
        System.out.println("總頁數" + page.getPages());
        System.out.println(list);
    }
  • 條件構造器:
    @Test
    public void getEmpByName() {
        EntityWrapper<Employee> wrapper = new EntityWrapper<>();
        
        // 'last_name' 與 'age' 對應資料庫中的欄位 
        wrapper.like("last_name", "張");
        wrapper.eq("age", 20);
        
        List<Employee> list = employeeMapper.selectList(wrapper);
        System.out.println(list);
    }

控制檯輸出的SQL分析日誌
在這裡插入圖片描述
簡單的資料庫操作不需要在 EmployeeMapper 介面中定義任何方法,也沒有在配置檔案中編寫SQL語句,而是通過繼承BaseMapper介面獲得通用的的增刪改查方法,複雜的SQL也可以使用條件構造器拼接.不過複雜的業務需求還是要編寫SQL語句的,流程和MyBatis一樣.

MyBatis Plus使用場景

程式碼生成器
  • 程式碼生成器依賴velocity模版引擎,引入依賴
<dependency>
	<groupId>org.apache.velocity</groupId>
	<artifactId>velocity-engine-core</artifactId>
	<version>2.0</version>
	<scope>test</scope>
</dependency>
  • 程式碼生成器類MysqlGenerator:
public class MysqlGenerator {

	private static final String PACKAGE_NAME = "cn.lqdev.learning.springboot.chapter9";
	private static final String MODULE_NAME = "biz";
	private static final String OUT_PATH = "D:\\develop\\code";
	private static final String AUTHOR = "oKong";

	private static final String DRIVER = "com.mysql.jdbc.Driver";
	private static final String URL = "jdbc:mysql://127.0.0.1:3306/learning?useUnicode=true&characterEncoding=UTF-8";
	private static final String USER_NAME = "root";
	private static final String PASSWORD = "123456";

	/**
	 * <p>
	 * MySQL 生成演示
	 * </p>
	 */
	public static void main(String[] args) {
		// 自定義需要填充的欄位
		List<TableFill> tableFillList = new ArrayList<TableFill>();

		// 程式碼生成器
		AutoGenerator mpg = new AutoGenerator().setGlobalConfig(
				// 全域性配置
				new GlobalConfig().setOutputDir(OUT_PATH)// 輸出目錄
						.setFileOverride(true)// 是否覆蓋檔案
						.setActiveRecord(true)// 開啟 activeRecord 模式
						.setEnableCache(false)// XML 二級快取
						.setBaseResultMap(false)// XML ResultMap
						.setBaseColumnList(true)// XML columList
						.setAuthor(AUTHOR)
						// 自定義檔案命名,注意 %s 會自動填充表實體屬性!
						.setXmlName("%sMapper").setMapperName("%sDao")
		// .setServiceName("MP%sService")
		// .setServiceImplName("%sServiceDiy")
		// .setControllerName("%sAction")
		).setDataSource(
				// 資料來源配置
				new DataSourceConfig().setDbType(DbType.MYSQL)// 資料庫型別
						.setTypeConvert(new MySqlTypeConvert() {
							// 自定義資料庫表欄位型別轉換【可選】
							@Override
							public DbColumnType processTypeConvert(String fieldType) {
								System.out.println("轉換型別:" + fieldType);
								// if ( fieldType.toLowerCase().contains( "tinyint" ) ) {
								// return DbColumnType.BOOLEAN;
								// }
								return super.processTypeConvert(fieldType);
							}
						}).setDriverName(DRIVER).setUsername(USER_NAME).setPassword(PASSWORD).setUrl(URL))
				.setStrategy(
						// 策略配置
						new StrategyConfig()
								// .setCapitalMode(true)// 全域性大寫命名
								.setDbColumnUnderline(true)// 全域性下劃線命名
								// .setTablePrefix(new String[]{"unionpay_"})// 此處可以修改為您的表字首
								.setNaming(NamingStrategy.underline_to_camel)// 表名生成策略
								// .setInclude(new String[] {"citycode_org"}) // 需要生成的表
								// .setExclude(new String[]{"test"}) // 排除生成的表
								// 自定義實體,公共欄位
								// .setSuperEntityColumns(new String[]{"test_id"})
								.setTableFillList(tableFillList)
								// 自定義實體父類
								// .setSuperEntityClass("com.baomidou.demo.common.base.BsBaseEntity")
								// // 自定義 mapper 父類
								// .setSuperMapperClass("com.baomidou.demo.common.base.BsBaseMapper")
								// // 自定義 service 父類
								// .setSuperServiceClass("com.baomidou.demo.common.base.BsBaseService")
								// // 自定義 service 實現類父類
								// .setSuperServiceImplClass("com.baomidou.demo.common.base.BsBaseServiceImpl")
								// 自定義 controller 父類
								// .setSuperControllerClass("com.baomidou.demo.TestController")
								// 【實體】是否生成欄位常量(預設 false)
								// public static final String ID = "test_id";
								.setEntityColumnConstant(true)
								// 【實體】是否為構建者模型(預設 false)
								// public User setName(String name) {this.name = name; return this;}
								.setEntityBuilderModel(true)
								// 【實體】是否為lombok模型(預設 false)<a href="https://projectlombok.org/">document</a>
								.setEntityLombokModel(true)
				// Boolean型別欄位是否移除is字首處理
				// .setEntityBooleanColumnRemoveIsPrefix(true)
				// .setRestControllerStyle(true)
				// .setControllerMappingHyphenStyle(true)
				).setPackageInfo(
						// 包配置
						new PackageConfig().setModuleName(MODULE_NAME).setParent(PACKAGE_NAME)// 自定義包路徑
								.setController("controller")// 這裡是控制器包名,預設 web
								.setXml("mapper").setMapper("dao")

				).setCfg(
						// 注入自定義配置,可以在 VM 中使用 cfg.abc 設定的值
						new InjectionConfig() {
							@Override
							public void initMap() {
								Map<String, Object> map = new HashMap<String, Object>();
								map.put("abc", this.getConfig().getGlobalConfig().getAuthor() + "-mp");
								this.setMap(map);
							}
						}.setFileOutConfigList(
								Collections.<FileOutConfig>singletonList(new FileOutConfig("/templates/mapper.xml.vm") {
									// 自定義輸出檔案目錄
									@Override
									public String outputFile(TableInfo tableInfo) {
										return OUT_PATH + "/xml/" + tableInfo.getEntityName() + "Mapper.xml";
									}
								})))
				.setTemplate(
						// 關閉預設 xml 生成,調整生成 至 根目錄
						new TemplateConfig().setXml(null)
		// 自定義模板配置,模板可以參考原始碼 /mybatis-plus/src/main/resources/template 使用 copy
		// 至您專案 src/main/resources/template 目錄下,模板名稱也可自定義如下配置:
		// .setController("...");
		// .setEntity("...");
		// .setMapper("...");
		// .setXml("...");
		// .setService("...");
		// .setServiceImpl("...");
		);

		// 執行生成
		mpg.execute();
	}

}
通用CRUD
  • 通用CRUD測試類GeneralTest:
@RunWith(SpringRunner.class)
//SpringBootTest 是springboot 用於測試的註解,可指定啟動類或者測試環境等,這裡直接預設。
@SpringBootTest 
@Slf4j
public class GeneralTest {

    @Autowired
    IUserService userService;

    @Test
    public void testInsert() {
        User user = new User();
        user.setCode("001");
        user.setName("okong-insert");
        //預設的插入策略為:FieldStrategy.NOT_NULL,即:判斷 null
        //對應在mapper.xml時寫法為:<if test="field!=null">
        //這個可以修改的,設定欄位的@TableField(strategy=FieldStrategy.NOT_EMPTY)
        //所以這個時候,為null的欄位是不會更新的,也可以開啟效能外掛,檢視sql語句就可以知道
        userService.insert(user);

        //新增所有欄位,
        userService.insertAllColumn(user);
        log.info("新增結束");
    }

    @Test
    public void testUpdate() {

        User user = new User();
        user.setCode("101");
        user.setName("oKong-insert");
        //這就是ActiveRecord的功能
        user.insert();
        //也可以直接 userService.insert(user);

        //更新
        User updUser = new User();
        updUser.setId(user.getId());
        updUser.setName("okong-upd");

        updUser.updateById();
        log.info("更新結束");
    }

    @Test
    public void testDelete() {
        User user = new User();
        user.setCode("101");
        user.setName("oKong-delete");

        user.insert();

        //刪除
        user.deleteById();
        log.info("刪除結束");

    }

    @Test
    public void testSelect() {
        User user = new User();
        user.setCode("201");
        user.setName("oKong-selecdt");

        user.insert();

        log.info("查詢:{}",user.selectById());
    }
}
  • MyBatis Plus定義的資料庫操作方法
    在這裡插入圖片描述
    對於通用程式碼如何注入的,可檢視com.baomidou.mybatisplus.mapper.AutoSqlInjector類,這個就是注入通用的CURD方法的類.
條件構造器

條件構造器主要提供了實體包裝器,用於處理SQL語句拼接,排序,實體引數查詢:使用的是資料庫欄位,不是Java屬性

  • sql條件拼接:
    SQL條件拼接測試類ConditionTest
@RunWith(SpringRunner.class)
//SpringBootTest 是springboot 用於測試的註解,可指定啟動類或者測試環境等,這裡直接預設。
@SpringBootTest 
@Slf4j
public class ConditionTest {

    @Autowired
    IUserService userService;

    @Test
    public void testOne() {
        User user =  new User();
        user.setCode("701");
        user.setName("okong-condition");
        user.insert();

        EntityWrapper<User> qryWrapper = new EntityWrapper<>();

        qryWrapper.eq(User.CODE, user.getCode());
        qryWrapper.eq(User.NAME, user.getName());

        //也可以直接 
//        qryWrapper.setEntity(user);

        //列印sql語句
        System.out.println(qryWrapper.getSqlSegment());

        //設定select 欄位 即:select code,name from 
        qryWrapper.setSqlSelect(User.CODE,User.NAME);
        System.out.println(qryWrapper.getSqlSelect());

        //查詢
        User qryUser = userService.selectOne(qryWrapper);
        System.out.println(qryUser);
        log.info("拼接一結束");
    }

    @Test
    public void testTwo() {
        User user =  new User();
        user.setCode("702");
        user.setName("okong-condition");
        user.insert();

        EntityWrapper<User> qryWrapper = new EntityWrapper<>();
        qryWrapper.where("code = {0}", user.getCode())
        .and("name = {0}",user.getName())
        .andNew("status = 0");
        System.out.println(qryWrapper.getSqlSegment());
        //等等很複雜的。
        //複雜的建議直接寫在xml裡面了,要是非動態的話 比較xml一眼看得懂呀
        //查詢
        User qryUser = userService.selectOne(qryWrapper);
        System.out.println(qryUser);
        log.info("拼接二結束");
    }

}

MyBatis Plus提供的條件構造方法com.baomidou.mybatisplus.mapper.Wrapper
在這裡插入圖片描述

  • 自定義SQL使用條件構造器:
    UserDao.java加入介面方法:
/**
     * 
     * @param rowBounds 分頁物件 直接傳入page即可
     * @param wrapper 條件構造器
     * @return
     */
    List<User> selectUserWrapper(RowBounds rowBounds, @Param("ew") Wrapper<User> wrapper);

UserMapper.xml加入對應的xml節點:

    <!-- 條件構造器形式 -->
    <select id="selectUserWrapper" resultType="user">
        SELECT
        <include refid="Base_Column_List" />
        FROM USER
        <where>
            ${ew.sqlSegment}
        </where>
    </select>

自定義SQL使用條件構造器測試類:

@Test
    public void testCustomSql() {
        User user = new User();
        user.setCode("703");
        user.setName("okong-condition");
        user.insert();

        EntityWrapper<User> qryWrapper = new EntityWrapper<>();
        qryWrapper.eq(User.CODE, user.getCode());

        Page<User> pageUser = new Page<>();
        pageUser.setCurrent(1);
        pageUser.setSize(10);

        List<User> userlist = userDao.selectUserWrapper(pageUser, qryWrapper);
        System.out.println(userlist.get(0));
        log.info("自定義sql結束");
    }
  • xml形式使用wrapper:
    UserDao.java:
/**
     * 
     * @param rowBounds 分頁物件 直接傳入page即可
     * @param wrapper 條件構造器
     * @return
     */
    List<User> selectUserWrapper(RowBounds rowBounds, @Param("ew") Wrapper<User> wrapper);

UserMapper.xml:


    <!-- 條件構造器形式 -->
    <select id="selectUserWrapper" resultType="user">
        SELECT
        <include refid="Base_Column_List" />
        FROM USER
        <where>
            ${ew.sqlSegment}
        </where>
    </select>
  • 條件引數說明:
查詢方式 使用說明
setSqlSelect 設定SELECT查詢欄位
where WHERE語句,拼接+WHERE條件
and AND語句,拼接+AND 欄位=值
andNew AND 語句,拼接+AND(欄位=值)
or OR 語句,拼接+OR 欄位=值
orNew OR 語句,拼接+OR(欄位=值)
eq 等於=
allEq 基於map內容等於=
ne 不等於<>
gt 大於>
ge 大於等於>=
lt 小於<
le 小於等於<=
like 模糊查詢 LIKE
notLike 模糊查詢NOT LIKE
in IN 查詢
notIn NOT IN查詢
isNull NULL值查詢
isNotNull IS NOT NULL
groupBy 分組GROUP BY
having HAVING關鍵詞
orderBy 排序ORDER BY
orderAsc 排序ASC ORDER BY
orderDesc 排序DESC ORDER BY
exists EXISTS條件語句
notExists NOT EXISTS條件語句
between BETWEEN條件語句
notBetween NOT BETWEEN條件語句
addFilter 自由拼接SQL
last 拼接在最後
自定義SQL語句

在多表關聯時,條件構造器和通用CURD都無法滿足時,可以編寫SQL語句進行擴充套件.這些都是mybatis的用法.首先改造UserDao介面,有兩種方式:

  • 註解形式:
@Select("SELECT * FROM USER WHERE CODE = #{userCode}")
    List<User> selectUserCustomParamsByAnno(@Param("userCode")String userCode);
  • xml形式:
List<User> selectUserCustomParamsByXml(@Param("userCode")String userCode);

UserMapper.xml新增一個節點:

    <!-- 由於設定了別名:typeAliasesPackage=cn.lqdev.learning.mybatisplus.samples.biz.entity,所以resultType可以不寫全路徑了。 -->
    <select id="selectUserCustomParamsByXml" resultType="user">
        SELECT 
        <include refid="Base_Column_List"/> 
        FROM USER 
       WHERE CODE = #{userCode}
    </select>

自定義SQL語句測試類CustomSqlTest:

@RunWith(SpringRunner.class)
//SpringBootTest 是springboot 用於測試的註解,可指定啟動類或者測試環境等,這裡直接預設。
@SpringBootTest 
@Slf4j
public class CustomSqlTest {

    @Autowired
    UserDao userDao;

    @Test
    public void testCustomAnno() {
        User user = new User();
        user.setCode("901");
        user.setName("okong-sql");
        user.insert();
        List<User> userlist = userDao.selectUserCustomParamsByAnno(user.getCode());
        //由於新增的 肯定不為null 故不判斷了。
        System.out.println(userlist.get(0).toString());
        log.info("註解形式結束------");
    }

    @Test
    public void testCustomXml() {
        User user = new User();
        user.setCode("902");
        user.setName("okong-sql");
        user.insert();
        List<User> userlist = userDao.selectUserCustomParamsByXml(user.getCode());
        //由於新增的 肯定不為null 故不判斷了。
        System.out.println(userlist.get(0).toString());
        log.info("xml形式結束------");
    }

}

注意:
在使用spring-boot-maven-plugin外掛打包成springboot執行jar時,需要注意:由於springboot的jar掃描路徑方式問題,會導致別名的包未掃描到,所以這個只需要把mybatis預設的掃描設定為Springboot的VFS實現.修改spring-mybatis.xml檔案:

  <!--mybatis-->
    <bean id="sqlSessionFactory" class="com.baomidou.mybatisplus.spring.MybatisSqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <!-- 自動掃描mapper.xml檔案,支援萬用字元 -->
        <property name="mapperLocations" value="classpath:mapper/**/*.xml"/>
        <!-- 配置檔案,比如引數配置(是否啟動駝峰等)、外掛配置等 -->
        <property name="configLocation" value="classpath:mybatis/mybatis-config.xml"/>
        <!-- 啟用別名,這樣就無需寫全路徑類名了,具體可自行查閱資料 -->
        <property name="typeAliasesPackage" value="cn.lqdev.learning.mybatisplus.samples.biz.entity"/>
        <!-- MP 全域性配置注入 -->
        <property name="globalConfig" ref="globalConfig"/>
        <!-- 設定vfs實現,避免路徑掃描問題 -->
        <property name="vfs"  value="com.baomidou.mybatisplus.spring.boot.starter.SpringBootVFS"></property>
    </bean>
分頁外掛,效能分析外掛

mybatis的外掛機制使用只需要註冊即可

  • mybatis-config.xml
    <plugins>
      <!-- SQL 執行效能分析,開發環境使用,線上不推薦。 -->
      <plugin interceptor="com.baomidou.mybatisplus.plugins.PerformanceInterceptor"></plugin>
      <!-- 分頁外掛配置 -->
      <plugin interceptor="com.baomidou.mybatisplus.plugins.PaginationInterceptor"></plugin>
    </plugins>
  • 分頁測試類(效能分析,配置後可以輸出sql及取數時間):
@RunWith(SpringRunner.class)
//SpringBootTest 是springboot 用於測試的註解,可指定啟動類或者測試環境等,這裡直接預設。
@SpringBootTest 
@Slf4j
public class PluginTest {

    @Autowired
    IUserService userService;

    @Test
    public void testPagination() {
        Page<User> page = new Page<>();
        //每頁數
        page.setSize(10);
        //當前頁碼
        page.setCurrent(1);

        //無條件時
        Page<User> pageList = userService.selectPage(page);
        System.out.println(pageList.getRecords().get(0));

        //新增資料 避免查詢不到資料
        User user = new User();
        user.setCode("801");
        user.setName("okong-Pagination");
        user.insert();
        //加入條件構造器
        EntityWrapper<User> qryWapper = new EntityWrapper<>();
        //這裡也能直接設定 entity 這是條件就是entity的非空欄位值了
//        qryWapper.setEntity(user);
        //這裡建議直接用 常量 
    //    qryWapper.eq(User.CODE, user.getCode());
        pageList = userService.selectPage(page, qryWapper);
        System.out.println(pageList.getRecords().get(0));
        log.info("分頁結束");
    }

}
  • 效能外掛體現,控制檯輸出:
 Time:4 ms - ID:cn.lqdev.learning.mybatisplus.samples.biz.dao.UserDao.selectPage
 Execute SQL: SELECT id AS id,code,`name`,`status`,gmt_create AS gmtCreate,gmt_modified AS gmtModified FROM user WHERE id=1026120705692434433 AND code='801' AND `name`='okong-Pagination' LIMIT 0,10
公共欄位自動填充

通常,每個公司都有自己的表定義,在《阿里巴巴Java開發手冊》中,就強制規定表必備三欄位:id,gmt_create,gmt_modified.所以通常我們都會寫個公共的攔截器去實現自動填充比如建立時間和更新時間的,無需開發人員手動設定.而在MP中就提供了這麼一個公共欄位自動填充功能

  • 設定填充欄位的填充型別:
  • User
    注意可以在程式碼生成器裡面配置規則的,可自動配置
    /**
     * 建立時間
     */
    @TableField(fill=FieldFill.INSERT)
    private Date gmtCreate;
    /**
     * 修改時間
     */
    @TableField(fill=FieldFill.INSERT_UPDATE)
    private Date gmtModified;
  • 定義處理類:
  • MybatisObjectHandler
public class MybatisObjectHandler extends MetaObjectHandler{

    @Override
    public void insertFill(MetaObject metaObject) {
        //新增時填充的欄位
        setFieldValByName("gmtCreate", new Date(), metaObject);
        setFieldValByName("gmtModified", new Date(), metaObject);

    }

    @Override
    public void updateFill(MetaObject metaObject) {
        //更新時 需要填充欄位
        setFieldValByName("gmtModified", new Date(), metaObject);
    }
}
  • 修改springb-mybatis.xml檔案,加入此配置
    <bean id="globalConfig" class="com.baomidou.mybatisplus.entity.GlobalConfiguration">
        <!--
            AUTO->`0`("資料庫ID自增")QW
             INPUT->`1`(使用者輸入ID")
            ID_WORKER->`2`("全域性唯一ID")
            UUID->`3`("全域性唯一ID")
        -->
        <property name="idType" value="2" />
        <property name="metaObjectHandler" ref="mybatisObjectHandler"></property>
    </bean>

    <bean id="mybatisObjectHandler" class="cn.lqdev.learning.mybatisplus.samples.config.MybatisObjectHandler"/>

再新增或者修改時,對應時間就會進行更新:

 Time:31 ms - ID:cn.lqdev.learning.mybatisplus.samples.biz.dao.UserDao.insert
 Execute SQL: INSERT INTO user ( id, code, `name`, gmt_create,gmt_modified ) VALUES ( 1026135016838037506, '702', 'okong-condition', '2018-08-05 23:57:07.344','2018-08-05 23:57:07.344' )

資料庫連線池-Alibaba Druid

  • Druid是JDBC元件,包括三個部分:
    • DruidDriver: 代理Driver,能夠提供基於Filter-Chain模式的外掛體系
    • DruidDataSource: 高效可管理的資料庫連線池
    • SQL Parser: Druid內建使用SQL Parser來實現防禦SQL隱碼攻擊(WallFilter),合併統計沒有引數化的SQL(StatFilter的mergeSql),SQL格式化,分庫分表
  • Druid的作用:
    • 監控資料庫訪問效能: Druid內建提供了一個功能強大的StatFilter外掛,能夠詳細統計SQL的執行效能,提升線上分析資料庫訪問效能
    • 替換DBCP和C3P0: Druid提供了一個高效,功能強大,可擴充套件性好的資料庫連線池
    • 資料庫密碼加密: 直接把資料庫密碼寫在配置檔案容易導致安全問題,DruidDruiver和DruidDataSource都支援PasswordCallback
    • 監控SQL執行日誌: Druid提供了不同的LogFilter,能夠支援Common-Logging,Log4j和JdkLog,可以按需要選擇相應的LogFilter,監控資料庫訪問情況
    • 擴充套件JDBC: 通過Druid提供的Filter-Chain機制,編寫JDBC層的擴充套件
  • 配置引數: Druid的DataSource:com.alibaba.druid.pool.DruidDataSource
配置引數 預設值 說明
name 如果存在多個資料來源,監控時可以通過name屬性進行區分,如果沒有配置,將會生成一個名字:"DataSource-"+System.identityHashCode(this)
jdbcUrl 連線資料庫的url,不同的資料庫url表示方式不同:
mysql:jdbc:mysql://192.16.32.128:3306/druid2
oracle : jdbc:oracle:thin:@192.16.32.128:1521:druid2
username 連線資料庫的使用者名稱
password 連線資料庫的密碼,密碼不出現在配置檔案中可以使用ConfigFilter
driverClassName 根據jdbcUrl自動識別 可以不配置,Druid會根據jdbcUrl自動識別dbType,選擇相應的driverClassName
initialSize 0 初始化時建立物理連線的個數.
初始化過程發生在:顯示呼叫init方法;第一次getConnection
maxActive 8 最大連線池數量
minIdle 最小連線池數量
maxWait 獲取連線時最大等待時間,單位毫秒.
配置maxWait預設使用公平鎖等待機制,併發效率會下降.可以配置useUnfairLock為true使用非公平鎖
poolPreparedStatements false 是否快取preparedStatement,即PSCache.
PSCache能夠提升對支援遊標的資料庫效能.
Oracle中使用,在MySQL中關閉
maxOpenPreparedStatements -1 要啟用PSCache,必須配置引數值>0,poolPreparedStatements自動觸發修改為true.
Oracle中可以配置數值為100,Oracle中不會存在PSCache過多的問題
validationQuery 用來檢測連線的是否為有效SQL,要求是一個查詢語句
如果validationQuery=null,那麼testOnBorrow,testOnReturn,testWhileIdle都不會起作用
testOnBorrow true 申請連線時執行validationQuery檢測連線是否有效,會降低效能
testOnReturn false 歸還連線時執行validationQuery檢測連線是否有效,會降低效能
testWhileIdle false 申請連線時,空閒時間大於timeBetweenEvictionRunsMillis時,執行validationQuery檢測連線是否有效
不影響效能,保證安全性,建議配置為true
timeBetweenEvictionRunsMillis Destroy執行緒會檢測連線的間隔時間
testWhileIdle的判斷依據
connectionInitSqls 物理連線初始化時執行SQL
exceptionSorter 根據dbType自動識別 當資料庫跑出不可恢復的異常時,拋棄連線
filters 通過別名的方式配置擴充套件外掛,屬性型別是字串:
常用的外掛:
監控統計用的filter:stat
日誌用的filter:log4j
防禦sql注入的filter:wall
proxyFilters 型別是List<com.alibaba.druid.filter.Filter>,如果同時配置了filters和proxyFilters是組合關係,不是替換關係

Druid的架構

Druid資料結構
  • Druid架構相輔相成的是基於DataSource和Segment的資料結構
  • DataSource資料結構:邏輯概念, 與傳統的關係型資料庫相比較DataSource可以理解為表
    • 時間列: 表明每行資料的時間值
    • 維度列: 表明資料的各個維度資訊
    • 指標列: 需要聚合的列的資料
  • Segment結構: 實際的物理儲存格式,
    • Druid通過Segment實現了橫縱向切割操作
    • Druid將不同的時間範圍內的資料存放在不同的Segment檔案塊中,通過時間實現了橫向切割
    • Segment也面向列進行資料壓縮儲存,實現縱向切割
  • Druid架構包含四個節點和一個服務:
    • 實時節點(RealTime Node): 即時攝入實時資料,並且生成Segment檔案
    • 歷史節點(Historical Node): 載入已經生成好的資料檔案,以供資料查詢使用
    • 查詢節點(Broker Node): 對外提供資料查詢服務,並且從實時節點和歷史節點彙總資料,合併後返回
    • 協調節點( Coordinator Node): 負責歷史節點的資料的負載均衡,以及通過規則管理資料的生命週期
  • 索引服務(Indexing Service): 有不同的獲取資料的方式,更加靈活的生成segment檔案管理資源
實時節點
  • 主要負責即時攝入實時資料,以及生成Segment檔案
  • 實時節點通過firehose進行資料的攝入,firehose是Druid實時消費模型
通過kafka消費,就是kafkaFireHose.
同時,實時節點的另外一個模組Plumer,用於Segment的生成,並且按照指定的週期,
將本週期內生成的所有資料塊合併成一個
  • Segment檔案從製造到傳播過程:
1.實時節點生產出Segment檔案,並且存到檔案系統中
2.Segment檔案的<MetaStore>存放到Mysql等其他外部資料庫中
3.Master通過Mysql中的MetaStore,通過一定的規則,將Segment分配給屬於它的節點
4.歷史節點得到Master傳送的指令後會從檔案系統中拉取屬於自己的Segment檔案,並且通過zookeeper,告知叢集,自己提供了此塊Segment的查詢服務
5.實時節點丟棄Segment檔案,並且宣告不在提供此塊檔案的查詢服務
歷史節點
  • 歷史節點再啟動的時候:
    • 優先檢查自己的本地快取中是否已經有了快取的Segment檔案
    • 然後從檔案系統中下載屬於自己,但還不存在的Segment檔案
    • 無論是何種查詢,歷史節點首先將相關的Segment從磁碟載入到記憶體.然後再提供服務
  • 歷史節點的查詢效率受記憶體空間富餘程度的影響很大:
    • 記憶體空間富餘,查詢時需要從磁碟載入資料的次數減少,查詢速度就快
    • 記憶體空間不足,查詢時需要從磁碟載入資料的次數就多,查詢速度就相對較慢
    • 原則上歷史節點的查詢速度與其記憶體大小和所負責的Segment資料檔案大小成正比關係
查詢節點
  • 查詢節點便是整個叢集的查詢中樞:
    • 在常規情況下,Druid叢集直接對外提供查詢的節點只有查詢節點, 而查詢節點會將從實時節點與歷史節點查詢到的資料合併後返回給客戶端
  • Druid使用了Cache機制來提高自己的查詢效率.
  • Druid提供兩類介質作為Cache:
    • 外部cache:Memcached
    • 內部Cache: 查詢節點或歷史節點的記憶體, 如果用查詢節點的記憶體作為Cache,查詢的時候會首先訪問其Cache,只有當不命中的時候才會去訪問歷史節點和實時節點查詢資料
協調節點
  • 對於整個Druid叢集來說,其實並沒有真正意義上的Master節點.
  • 實時節點與查詢節點能自行管理並不聽命於任何其他節點,
  • 對於歷史節點來說,協調節點便是他們的Master,因為協調節點將會給歷史節點分配資料,完成資料分佈在歷史節點之間的負載均衡.
  • 歷史節點之間是相互不進行通訊的,全部通過協調節點進行通訊
  • 利用規則管理資料的生命週期:
    • Druid利用針對每個DataSoure設定的規則來載入或者丟棄具體的檔案資料,來管理資料的生命週期
    • 可以對一個DataSource按順序新增多條規則,對於一個Segment檔案來說,協調節點會逐條檢查規則
    • 當碰到當前Segment檔案負責某條規則的情況下,協調節點會立即命令歷史節點對該檔案執行此規則,載入或者丟棄,並停止餘下的規則,否則繼續檢查
索引服務

除了通過實時節點生產Segment檔案之外,druid還提供了一組索引服務來攝入資料

  • 索引服務的優點:
    • 有不同的獲取資料的方式,支援pull和push
    • 可以通過API程式設計的方式來配置任務
    • 可以更加靈活地使用資源
    • 靈活地操作Segment檔案
  • 索引服務的主從架構:
    索引服務包含一組元件,並以主從結構作為架構方式,統治節點 Overload node為主節點,中間管理者Middle Manager為從節點
    • Overload node: 索引服務的主節點.對外負責接收任務請求,對內負責將任務分解並下發到從節點即Middle Manager.有兩種執行模式:
      • 本地模式(預設): 此模式主節點不僅需要負責叢集的排程,協調分配工作,還需要負責啟動Peon(苦工)來完成一部分具體的任務
      • 遠端模式: 主從節點分別執行在不同的節點上,主節點只負責協調分配工作.不負責完成任務,並且提供rest服務,因此客戶端可以通過HTTP POST來提交任務
Middle Manager與Peon(苦工):
Middle Manager即是Overload node 的工作節點,負責接收Overload node分配的任務,
然後啟動相關的Peon來完成任務這種模式和yarn的架構比較類似

1.Overload node相當於Yarn的ResourceManager,負責資源管理和任務分配
2.Middle Manager相當於Yarn的NodeManager,負責管理獨立節點的資源,並且接收任務
3.Peon 相當於Yarn的Container,啟動在具體節點上具體任務的執行

閘道器-Zuul

  • Zuul是netflix開源的一個API Gateway 伺服器, 本質上是一個web servlet應用
    -Zuul是一個基於JVM路由和服務端的負載均衡器,提供動態路由,監控,彈性,安全等邊緣服務的框架,相當於是裝置和 Netflix 流應用的 Web 網站後端所有請求的前門
Zuul工作原理
  • 過濾器機制
    • Zuul提供了一個框架,可以對過濾器進行動態的載入,編譯,執行
    1.Zuul的過濾器之間沒有直接的相互通訊,他們之間通過一個RequestContext的靜態類來進行資料傳遞的。RequestContext類中有ThreadLocal變數來記錄每個Request所需要傳遞的資料
    2.Zuul的過濾器是由Groovy寫成,這些過濾器檔案被放在Zuul Server上的特定目錄下面,Zuul會定期輪詢這些目錄,修改過的過濾器會動態的載入到Zuul Server中以便過濾請求使用
    
    • 標準過濾器型別:
      Zuul大部分功能都是通過過濾器來實現的。Zuul中定義了四種標準過濾器型別,這些過濾器型別對應於請求的典型生命週期
      • PRE: 在請求被路由之前呼叫,利用這種過濾器實現身份驗證、在叢集中選擇請求的微服務、記錄除錯資訊等
      • ROUTING: 請求路由到微服務,用於構建傳送給微服務的請求,使用Apache HttpClient或Netfilx Ribbon請求微服務
      • POST: 在路由到微服務以後執行,用來為響應新增標準的HTTP Header、收集統計資訊和指標、將響應從微服務傳送給客戶端等
      • ERROR: 在其他階段發生錯誤時執行該過濾器
    • 內建的特殊過濾器:
      • StaticResponseFilter: StaticResponseFilter允許從Zuul本身生成響應,而不是將請求轉發到源
      • SurgicalDebugFilter: SurgicalDebugFilter允許將特定請求路由到分隔的除錯叢集或主機
    • 自定義的過濾器:
      除了預設的過濾器型別,Zuul還允許我們建立自定義的過濾器型別。如STATIC型別的過濾器,直接在Zuul中生成響應,而不將請求轉發到後端的微服務
  • 過濾器的生命週期
    Zuul請求的生命週期詳細描述了各種型別的過濾器的執行順序
    在這裡插入圖片描述
  • 過濾器排程過程
    在這裡插入圖片描述
  • 動態載入過濾器
    在這裡插入圖片描述
Zuul的作用

Zuul可以通過載入動態過濾機制實現Zuul的功能:

  • 驗證與安全保障: 識別面向各類資源的驗證要求並拒絕那些與要求不符的請求
  • 審查與監控: 在邊緣位置追蹤有意義資料及統計結果,得到準確的生產狀態結論
  • 動態路由: 以動態方式根據需要將請求路由至不同後端叢集處
  • 壓力測試: 逐漸增加指向叢集的負載流量,從而計算效能水平
  • 負載分配: 為每一種負載型別分配對應容量,並棄用超出限定值的請求
  • 靜態響應處理: 在邊緣位置直接建立部分響應,從而避免其流入內部叢集
  • 多區域彈性: 跨越AWS區域進行請求路由,旨在實現ELB使用多樣化並保證邊緣位置與使用者儘可能接近
Zuul與應用的整合方式
  • ZuulServlet - 處理請求(排程不同階段的filters,處理異常等)
    • 所有的Request都要經過ZuulServlet的處理,
    • Zuul對request處理邏輯的三個核心的方法: preRoute(),route(), postRoute()
    • ZuulServletZuulServlet交給ZuulRunner去執行。由於ZuulServlet是單例,因此ZuulRunner也僅有一個例項。ZuulRunner直接將執行邏輯交由FilterProcessor處理,FilterProcessor也是單例,其功能就是依據filterType執行filter的處理邏輯
    • FilterProcessor對filter的處理邏輯:
      1.首先根據Type獲取所有輸入該Type的filter:List<ZuulFilter> list
      2.遍歷該list,執行每個filter的處理邏輯:processZuulFilter(ZuulFilter filter)
      3.RequestContext對每個filter的執行狀況進行記錄,應該留意,此處的執行狀態主要包括其執行時間、以及執行成功或者失敗,如果執行失敗則對異常封裝後丟擲
      4.到目前為止,Zuul框架對每個filter的執行結果都沒有太多的處理,它沒有把上一filter的執行結果交由下一個將要執行的filter,僅僅是記錄執行狀態,如果執行失敗丟擲異常並終止執行
      
    • ContextLifeCycleFilter - RequestContext 的生命週期管理:
      • ContextLifecycleFilter的核心功能是為了清除RequestContext;請求上下文RequestContext通過ThreadLocal儲存,需要在請求完成後刪除該物件RequestContext提供了執行filter Pipeline所需要的Context,因為Servlet是單例多執行緒,這就要求RequestContext即要執行緒安全又要Request安全。context使用ThreadLocal儲存,這樣每個worker執行緒都有一個與其繫結的RequestContext,因為worker僅能同時處理一個Request,這就保證了Request Context 即是執行緒安全的由是Request安全的。
    • GuiceFilter - GOOLE-IOC(Guice是Google開發的一個輕量級,基於Java5(主要運用泛型與註釋特性)的依賴注入框架(IOC).Guice非常小而且快.)
    • StartServer - 初始化 zuul 各個元件(ioc,外掛,filters,資料庫等)
    • FilterScriptManagerServlet - uploading/downloading/managing scripts, 實現熱部署
      Filter原始碼檔案放在zuul 服務特定的目錄, zuul server會定期掃描目錄下的檔案的變化,動態的讀取\編譯\執行這些filter,如果有Filter檔案更新,原始檔會被動態的讀取,編譯載入進入服務,接下來的Request處理就由這些新加入的filter處理

快取-Redis

  • Redis: Redis是一個開源的記憶體中的資料結構儲存系統,可以用作資料庫,快取訊息中介軟體
  • 操作工具:Redis Desktop Manager

整合Redis快取

  • 在pom.xml中引入redis依賴
<dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
  • 配置redis,在application.properties中配置redis
spring.redis.host=192.168.32.242
  • RedisTemplate:(操作k-v都是物件)
@Bean
    @ConditionalOnMissingBean(
        name = {"redisTemplate"}
    )
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
        RedisTemplate<Object, Object> template = new RedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }
  • 儲存物件時,使用JDK的序列化機制,將序列化後的資料儲存到redis
  • 為了增強Redis資料庫中的資料可讀性:
    • 將物件資料以json方式儲存:
      • 將物件轉化為json
      • 配置redisTemplate的json序列化規則
   @Configuration
public class MyRedisConfig {
 @Bean
 public RedisTemplate<Object, Employee> empRedisTemplate(RedisConnectionFactory redisConnectionFactory){
     RedisTemplate<Object,Employee> redisTemplate=new RedisTemplate<Object,Employee>();
     redisTemplate.setConnectionFactory(redisConnectionFactory);
     Jackson2JsonRedisSerializer<Employee> serializer = new Jackson2JsonRedisSerializer<Employee>(Employee.class);
     redisTemplate.setDefaultSerializer(serializer);
     return redisTemplate;
 }
}
Redis常見的資料型別:
	String-字串
	List-列表
	Set-集合
	Hash-雜湊
	ZSet-有序集合

redisTemplate.opsForValue()--String(字串)
redisTemplate.opsForList()--List(列表)
redisTemplate.opsForSet()--Set(集合)
redisTemplate.opsForHash()--Hash(雜湊)
redisTemplate.opsForZSet()--ZSet(有序集合)
  • StringRedisTemplate(操作k-v都是字串)
    在RedisAutoConfiguration中:
@Bean
    @ConditionalOnMissingBean
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
        StringRedisTemplate template = new StringRedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

在StringRedisTemplate中:

public class StringRedisTemplate extends RedisTemplate<String, String> {
    public StringRedisTemplate() {
        this.setKeySerializer(RedisSerializer.string());
        this.setValueSerializer(RedisSerializer.string());
        this.setHashKeySerializer(RedisSerializer.string());
        this.setHashValueSerializer(RedisSerializer.string());
    }

    public StringRedisTemplate(RedisConnectionFactory connectionFactory) {
        this();
        this.setConnectionFactory(connectionFactory);
        this.afterPropertiesSet();
    }

    protected RedisConnection preProcessConnection(RedisConnection connection, boolean existingConnection) {
        return new DefaultStringRedisConnection(connection);
    }
}
Redis常見的資料型別:
	String-字串
	List-列表
	Set-集合
	Hash-雜湊
	ZSet-有序集合

stringRedisTemplate.opsForValue()--String(字串)
stringRedisTemplate.opsForList()--List(列表)
stringRedisTemplate.opsForSet()--Set(集合)
stringRedisTemplate.opsForHash()--Hash(雜湊)
stringRedisTemplate.opsForZSet()--ZSet(有序集合)

註冊中心-Zookeeper,Eureka

Zookeeper基本概念

  • Zookeeper是一個分散式的,開放原始碼的分散式應用程式協調服務
  • Zookeeper是hadoop的一個子專案
  • 包含一個簡單的原語集, 分散式應用程式可以基於它實現同步服務,配置維護和命名服務等
  • 在分散式應用中,由於工程師不能很好地使用鎖機制,以及基於訊息的協調機制不適合在某些應用中使用,Zookeeper提供一種可靠的,可擴充套件的,分散式的,可配置的協調機制來統一系統的狀態
  • Zookeeper中的角色:
    在這裡插入圖片描述
  • 系統模型圖:
    在這裡插入圖片描述
  • Zookeeper特點:
    • 最終一致性: client不論連線到哪個Server,展示給它都是同一個檢視,這是Zookeeper最重要的效能
    • 可靠性: 具有簡單,健壯,良好的效能,如果訊息m被到一臺伺服器接受,那麼它將被所有的伺服器接受
    • 實時性: Zookeeper保證客戶端將在一個時間間隔範圍內獲得伺服器的更新資訊,或者伺服器失效的資訊.但由於網路延時等原因,Zookeeper不能保證兩個客戶端能同時得到剛更新的資料,如果需要最新資料,應該在讀資料之前呼叫sync()介面
    • 等待無關(wait-free): 慢的或者失效的client不得干預快速的client的請求,使得每個client都能有效的等待
    • 原子性: 更新只能成功或者失敗,沒有中間狀態
    • 順序性: 包括全域性有序偏序兩種:全域性有序是指如果在一臺伺服器上訊息a在訊息b前釋出,則在所有Server上訊息a都將在訊息b前被髮布.偏序是指如果一個訊息b在訊息a後被同一個傳送者釋出,a必將排在b前面

Zookeeper工作原理

  • Zookeeper的核心是原子廣播,這個機制保證了各個Server之間的同步實現這個機制的協議叫做Zab協議
  • Zab協議有兩種模式:恢復模式(選主),廣播模式(同步)
    • 當服務啟動或者在領導者崩潰後,Zab就進入了恢復模式,當領導者被選舉出來,且大多數Server完成了和leader的狀態同步以後,恢復模式就結束了
    • 狀態同步保證了leader和Server具有相同的系統狀態
  • 為了保證事務的順序一致性,zookeeper採用了遞增的事務id號(zxid)來標識事務
  • 所有的提議(proposal)都在被提出的時候加上了zxid.實現中zxid是一個64位的數字,它高32位是epoch用來標識leader關係是否改變,每次一個leader被選出來,它都會有一個新的epoch,標識當前屬於那個leader的統治時期.低32位用於遞增計數
  • 每個Server在工作過程中有三種狀態:
  • LOOKING: 當前Server不知道leader是誰,正在搜尋
  • LEADING: 當前Server即為選舉出來的leader
  • FOLLOWING: leader已經選舉出來,當前Server與之同步
選主流程
  • 當leader崩潰或者leader失去大多數的follower這時候Zookeeper進入恢復模式
  • 恢復模式需要重新選舉出一個新的leader,讓所有的Server都恢復到一個正確的狀態.
  • Zookeeper的選舉演算法有兩種:系統預設的選舉演算法為fast paxos
    • 基於fast paxos演算法
    • 基於basic paxos演算法
  • 基於fast paxos演算法:
    fast paxos流程是在選舉過程中,某Server首先向所有Server提議自己要成為leader,當其它Server收到提議以後,解決epoch和zxid的衝突,並接受對方的提議,然後向對方傳送接受提議完成的訊息,重複這個流程,最後一定能選舉出Leader
    在這裡插入圖片描述
  • 基於basic paxos演算法:
    • 選舉執行緒由當前Server發起選舉的執行緒擔任,其主要功能是對投票結果進行統計,並選出推薦的Server
    • 選舉執行緒首先向所有Server發起一次詢問(包括自己)
    • 選舉執行緒收到回覆後,驗證是否是自己發起的詢問(驗證zxid是否一致),然後獲取對方的id(myid),並儲存到當前詢問物件列表中,最後獲取對方提議的leader相關資訊(id,zxid),並將這些資訊儲存到當次選舉的投票記錄表中
    • 收到所有Server回覆以後,就計算出zxid最大的那個Server,並將這個Server相關資訊設定成下一次要投票的Server;
    • 執行緒將當前zxid最大的Server設定為當前Server要推薦的Leader,如果此時獲勝的Server獲得n/2+1的Server票數,設定當前推薦的leader為獲勝的Server,將根據獲勝的Server相關資訊設定自己的狀態,否則,繼續這個過程,直到leader被選舉出來
  • 通過流程分析我們可以得出:要使Leader獲得多數Server的支援,則Server總數必須是奇數2n+1,且存活的Server的數目不得少於n+1.每個Server啟動後都會重複以上流程.在恢復模式下,如果是剛從崩潰狀態恢復的或者剛啟動的server還會從磁碟快照中恢復資料和會話資訊,Zookeeper會記錄事務日誌並定期進行快照,方便在恢復時進行狀態恢復.選主的具體流程圖如下所示:
    在這裡插入圖片描述
同步流程
  • 選完leader以後,Zookeeper就進入狀態同步過程:
    • leader等待server連線
    • Follower連線leader,將最大的zxid傳送給leader
    • Leader根據follower的zxid確定同步點
    • 完成同步後通知follower已經成為uptodate狀態
    • Follower收到uptodate訊息後,又可以重新接受client的請求進行服務
      -
工作流程
  • Leader工作流程:
    Leader主要有三個功能:
    • 恢復資料
    • 維持與Learner的心跳,接收Learner請求並判斷Learner的請求訊息型別
    • Learner的訊息型別主要有PING訊息,REQUEST訊息,ACK訊息,REVALIDATE訊息,根據不同的訊息型別,進行不同的處理
      • PING訊息: Learner的心跳資訊
      • REQUEST訊息: Follower傳送的提議資訊,包括寫請求及同步請求
      • ACK訊息: Follower的對提議的回覆.超過半數的Follower通過,則commit該提議
      • REVALIDATE訊息: 用來延長SESSION有效時間
  • Leader的工作流程簡圖如下所示,在實際實現中,流程要比下圖複雜得多,啟動了三個執行緒來實現功能:
    在這裡插入圖片描述
  • Follower工作流程:
  • Follower主要有四個功能:
    • 向Leader傳送請求(PING訊息,REQUEST訊息,ACK訊息,REVALIDATE訊息)
    • 接收Leader訊息並進行處理
    • 接收Client的請求,如果為寫請求,傳送給Leader進行投票
    • 返回Client結果
  • Follower的訊息迴圈處理如下幾種來自Leader的訊息:
    • PING訊息: 心跳訊息
    • PROPOSAL訊息: Leader發起的提案,要求Follower投票
    • COMMIT訊息: 伺服器端最新一次提案的資訊
    • UPTODATE訊息: 表明同步完成
    • REVALIDATE訊息: 根據Leader的REVALIDATE結果,關閉待revalidate的session還是允許其接受訊息
    • SYNC訊息: 返回SYNC結果到客戶端,這個訊息最初由客戶端發起,用來強制得到最新的更新
  • Follower的工作流程簡圖如下所示,在實際實現中,Follower是通過5個執行緒來實現功能的:
    在這裡插入圖片描述
  • observer流程和Follower的唯一不同的地方就是observer不會參加leader發起的投票

Zookeeper應用場景

配置管理
  • 集中式的配置管理在應用叢集中是非常常見的,一般都會實現一套集中的配置管理中心,應對不同的應用叢集對於共享各自配置的需求,並且在配置變更時能夠通知到叢集中的每一個機器,也可以細分進行分層級監控
  • Zookeeper很容易實現這種集中式的配置管理,比如將APP1的所有配置配置到/APP1 znode下,APP1所有機器一啟動就對/APP1這個節點進行監控(zk.exist("/APP1",true)),並且實現回撥方法Watcher,那麼在zookeeper上/APP1 znode節點下資料發生變化的時候,每個機器都會收到通知,Watcher方法將會被執行,那麼應用再取下資料即可(zk.getData("/APP1",false,null))
    -
叢集管理
  • 應用叢集中,我們常常需要讓每一個機器知道叢集中(或依賴的其他某一個叢集)哪些機器是活著的,並且在叢集機器因為當機,網路斷鏈等原因能夠不在人工介入的情況下迅速通知到每一個機器
  • Zookeeper同樣很容易實現這個功能,比如我在zookeeper伺服器端有一個znode叫 /APP1SERVERS, 那麼叢集中每一個機器啟動的時候都去這個節點下建立一個EPHEMERAL型別的節點,比如server1建立/APP1SERVERS/SERVER1(可以使用ip,保證不重複),server2建立/APP1SERVERS/SERVER2,然後SERVER1和SERVER2都watch /APP1SERVERS這個父節點,那麼也就是這個父節點下資料或者子節點變化都會通知對該節點進行watch的客戶端.因為EPHEMERAL型別節點有一個很重要的特性,就是客戶端和伺服器端連線斷掉或者session過期就會使節點消失,那麼在某一個機器掛掉或者斷鏈的時候,其對應的節點就會消失,然後叢集中所有對/APP1SERVERS進行watch的客戶端都會收到通知,然後取得最新列表即可

  • 另外有一個應用場景就是叢集選master: 一旦master掛掉能夠馬上能從slave中選出一個master,實現步驟和前者一樣,只是機器在啟動的時候在APP1SERVERS建立的節點型別變為EPHEMERAL_SEQUENTIAL型別,這樣每個節點會自動被編號
  • 我們預設規定編號最小的為master,所以當我們對/APP1SERVERS節點做監控的時候,得到伺服器列表,只要所有叢集機器邏輯認為最小編號節點為master,那麼master就被選出,而這個master當機的時候,相應的znode會消失,然後新的伺服器列表就被推送到客戶端,然後每個節點邏輯認為最小編號節點為master,這樣就做到動態master選舉
    在這裡插入圖片描述

Zookeeper監視

  • Zookeeper所有的讀操作-getData(),getChildren(),和exists() 都可以設定監視(watch),監視事件可以理解為一次性的觸發器. 官方定義如下: a watch event is one-time trigger, sent to the client that set the watch, which occurs when the data for which the watch was set changes:
    • One-time trigger(一次性觸發)
      • 當設定監視的資料發生改變時,該監視事件會被髮送到客戶端
      • 例如:如果客戶端呼叫了getData("/znode1", true)並且稍後/znode1節點上的資料發生了改變或者被刪除了,客戶端將會獲取到/znode1發生變化的監視事件,而如果/znode1再一次發生了變化,除非客戶端再次對/znode1設定監視,否則客戶端不會收到事件通知
    • Sent to the client(傳送至客戶端)
      • Zookeeper客戶端和服務端是通過socket進行通訊的,由於網路存在故障,所以監視事件很有可能不會成功地到達客戶端,監視事件是非同步傳送至監視者的
      • Zookeeper本身提供了保序性(ordering guarantee):即客戶端只有首先看到了監視事件後,才會感知到它所設定監視的znode發生了變化(a client will never see a change for which it has set a watch until it first sees the watch event).網路延遲或者其他因素可能導致不同的客戶端在不同的時刻感知某一監視事件,但是不同的客戶端所看到的一切具有一致的順序
    • The data for which the watch was set(被設定watch的資料)
      • znode 節點本身具有不同的改變方式
      • 例如:Zookeeper 維護了兩條監視連結串列:資料監視和子節點監視(data watches and child watches) getData() and exists()設定資料監視,getChildren()設定子節點監視
      • 又例如:Zookeeper設定的不同監視返回不同的資料,getData()和exists()返回znode節點的相關資訊,而getChildren()返回子節點列表.因此,setData()會觸發設定在某一節點上所設定的資料監視(假定資料設定成功),而一次成功的create()操作則會出發當前節點上所設定的資料監視以及父節點的子節點監視.一次成功的delete()操作將會觸發當前節點的資料監視和子節點監視事件,同時也會觸發該節點父節點的child watch
  • Zookeeper中的監視是輕量級的,因此容易設定,維護和分發.當客戶端與 Zookeeper 伺服器端失去聯絡時,客戶端並不會收到監視事件的通知,只有當客戶端重新連線後,若在必要的情況下,以前註冊的監視會重新被註冊並觸發,對於開發人員來說這通常是透明的.只有一種情況會導致監視事件的丟失,即:通過exists()設定了某個znode節點的監視,但是如果某個客戶端在此znode節點被建立和刪除的時間間隔內與zookeeper伺服器失去了聯絡,該客戶端即使稍後重新連線zookeeper伺服器後也得不到事件通知

Eureka(服務發現框架)

  • Eureka是一個基於REST的服務,主要用於定位執行在AWS域中的中間層服務,以達到負載均衡和中間層服務故障轉移的目的. SpringCloud將它整合在其子專案spring-cloud-netflix中,以實現SpringCloud的服務發現功能
Eureka的兩個元件
  • Eureka Server: Eureka Server提供服務註冊服務,各個節點啟動後,會在Eureka Server中進行註冊,這樣EurekaServer中的服務登錄檔中將會儲存所有可用服務節點的資訊,服務節點的資訊可以在介面中看到. Eureka Server之間通過複製的方式完成資料的同步
  • Eureka Client: 是一個java客戶端,用於簡化與Eureka Server的互動,客戶端同時也就是一個內建的、使用輪詢(round-robin)負載演算法的負載均衡器
  • Eureka通過心跳檢查、客戶端快取等機制,確保了系統的高可用性、靈活性和可伸縮性
    • 在應用啟動後,將會向Eureka Server傳送心跳, 如果Eureka Server在多個心跳週期內沒有接收到某個節點的心跳,Eureka Server將會從服務登錄檔中把這個服務節點移除。
    • Eureka還提供了客戶端快取機制,即使所有的Eureka Server都掛掉,客戶端依然可以利用快取中的資訊消費其他服務的API。Eureka通過心跳檢查、客戶端快取等機制,確保了系統的高可用性、靈活性和可伸縮性

作業排程框架-Quartz

Quartz作業排程框架概念

  • Quartz是一個完全由java編寫的開源作業排程框架,是OpenSymphony開源組織在Job scheduling領域的開源專案,它可以與J2EE與J2SE應用程式相結合也可以單獨使用,Quartz框架整合了許多額外功能.Quartz可以用來建立簡單或執行十個,百個,甚至是好幾萬個Jobs這樣複雜的程式
  • Quartz三個主要的概念:
    • 排程器:
      • Quartz框架的核心是排程器
      • 排程器負責管理Quartz應用執行時環境
      • 排程器不是靠自己做所有的工作,而是依賴框架內一些非常重要的部件
      • Quartz怎樣能併發執行多個作業的原理: Quartz不僅僅是執行緒和執行緒池管理,為確保可伸縮性,Quartz採用了基於多執行緒的架構.啟動時,框架初始化一套worker執行緒,這套執行緒被排程器用來執行預定的作業.
      • Quartz依賴一套鬆耦合的執行緒池管理部件來管理執行緒環境
    • 任務:
      • 自己編寫的業務邏輯,交給quartz執行
    • 觸發器:
      • 排程作業,什麼時候開始執行,什麼時候結束執行

Quartz設計模式

  • Builer模式
  • Factory模式
  • 元件模式
  • 鏈式寫法

Quartz體系結構

Quartz框架中的核心類:

  • JobDetail:
    • Quartz每次執行都會直接建立一個JobDetail,同時建立一個Job例項.
    • 不直接接受一個Job的例項,接受一個Job的實現類
    • 通過new instance()的反射方式來例項一個Job,在這裡Job是一個介面,需要編寫類去實現這個介面
  • Trigger:
    • 它由SimpleTrigger和CronTrigger組成
    • SimpleTrigger實現類似Timer的定時排程任務,CronTrigger可以通過cron表示式實現更復雜的排程邏輯
  • Scheduler:
    • 排程器
    • JobDetail和Trigger可以通過Scheduler繫結到一起

Quartz重要元件

Job介面
  • 可以通過實現該介面來實現我們自己的業務邏輯,該介面只有execute()一個方法,我們可以通過下面的方式來實現Job介面來實現我們自己的業務邏輯
public class HelloJob implements Job{

    public void execute(JobExecutionContext context) throws JobExecutionException {
    //編寫我們自己的業務邏輯
    }
JobDetail
  • 每次都會直接建立一個JobDetail,同時建立一個Job例項,它不直接接受一個Job的例項,但是它接受一個Job的實現類,通過new instance()的反射方式來例項一個Job.可以通過下面的方式將一個Job實現類繫結到JobDetail中
		JobDetail jobDetail=JobBuilder.newJob(HelloJob.class).
                withIdentity("myJob", "group1")
                .build();
JobBuiler
  • 主要是用來建立JobDeatil例項
JobStore
  • 繫結了Job的各種資料
Trigger
  • 主要用來執行Job實現類的業務邏輯的,我們可以通過下面的程式碼來建立一個Trigger例項
	CronTrigger trigger = (CronTrigger) TriggerBuilder
                .newTrigger()
                .withIdentity("myTrigger", "group1")    //建立一個識別符號
                .startAt(date)//什麼時候開始觸發
                //每秒鐘觸發一次任務
                .withSchedule(CronScheduleBuilder.cronSchedule("* * * * * ? *"))

                .build();
Scheduler

建立Scheduler有兩種方式

  • 通過StdSchedulerFactory來建立
SchedulerFactory sfact=new StdSchedulerFactory();
Scheduler scheduler=sfact.getScheduler();
  • 通過DirectSchedulerFactory來建立
DiredtSchedulerFactory factory=DirectSchedulerFactory.getInstance();
Scheduler scheduler=factory.getScheduler();

Scheduler配置引數一般儲存在quartz.properties中,我們可以修改引數來配置相應的引數.通過呼叫getScheduler() 方法就能建立和初始化排程物件

  • Scheduler的主要函式:
    • Date schedulerJob(JobDetail,Trigger trigger): 返回最近觸發的一次時間
    • void standby(): 暫時掛起
    • void shutdown(): 完全關閉,不能重新啟動
    • shutdown(true): 表示等待所有正在執行的job執行完畢之後,再關閉scheduler
    • shutdown(false): 直接關閉scheduler
  • quartz.properties資原始檔:
    在org.quartz這個包下,當我們程式啟動的時候,它首先會到我們的根目錄下檢視是否配置了該資原始檔,如果沒有就會到該包下讀取相應資訊,當我們咋實現更復雜的邏輯時,需要自己指定引數的時候,可以自己配置引數來實現
org.quartz.scheduler.instanceName: DefaultQuartzScheduler
org.quartz.scheduler.rmi.export: false
org.quartz.scheduler.rmi.proxy: false
org.quartz.scheduler.wrapJobExecutionInUserTransaction: false

org.quartz.threadPool.class: org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount: 10
org.quartz.threadPool.threadPriority: 5
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread: true

org.quartz.jobStore.misfireThreshold: 60000

org.quartz.jobStore.class: org.quartz.simpl.RAMJobStore
  • quartz.properties資原始檔主要組成部分:
    • 排程器屬性
    • 執行緒池屬性
    • 作業儲存設定
    • 外掛設定
  • 排程器屬性:
    • org.quartz.scheduler.instanceName屬性用來區分特定的排程器例項,可以按照功能用途來給排程器起名
    • org.quartz.scheduler.instanceId屬性和前者一樣,也允許任何字串,但這個值必須是在所有排程器例項中是唯一的,尤其是在一個叢集當中,作為叢集的唯一key.假如想quartz生成這個值的話,可以設定為Auto
  • 執行緒池屬性:
    • threadCount: 設定執行緒的數量
    • threadPriority: 設定執行緒的優先順序
    • org.quartz.threadPool.class: 執行緒池的實現
  • 作業儲存設定:
    • 描述了在排程器例項的宣告週期中,job和trigger資訊是怎麼樣儲存的
  • 外掛配置:
    • 滿足特定需求用到的quartz外掛的配置

監聽器

對事件進行監聽並且加入自己相應的業務邏輯,主要有以下三個監聽器分別對Job,Trigger,Scheduler進行監聽:

  • JobListener
  • TriggerListener
  • SchedulerListener

Cron表示式

欄位 允許值 允許特殊字元
0-59 , - * /
0-59 , - * /
小時 0-23 , - * /
日期 1-31 , - * ? / L W C
月份 1-12 , - * /
星期 0-7或SUN-SAT,0和7是SUN , - * /
特殊字元 含義
, 列舉
- 區間
* 任意
/ 步長
? 日和星期的衝突匹配
L 最後
w 工作日
C 與calendar聯絡後計算過的值
# 星期: 4#2-第2個星期三
second(秒),minute(分),hour(時),day of month(日),month(月),day of week(周幾)
0 * * * * MON-FRI
@Scheduled(cron="0 * * * * MON-FRI")
@Scheduled(cron="1,2,3 * * * * MON-FRI")-列舉: ,
@Scheduled(cron="0-15 * * * * MON-FRI")-區間: -
@Scheduled(cron="0/4 * * * * MON-FRI")-步長: / 從0開始,每4秒啟動一次
cron="0 0/5 14,18 * * ?"	每天14點整和18點整,每隔5分鐘執行一次
cron="0 15 10 ? * 1-6"	  	每個月的週一至週六10:15分執行一次
cron="0 0 2 ? * 6L" 		每個月的最後一個週六2點執行一次
cron="0 0 2 LW * ?"			每個月的最後一個工作日2點執行一次
cron="0 0 2-4 ? * 1#1" 		每個月的第一個週一2點到4點,每個整點執行一次

介面測試框架-Swagger2

Swagger介紹

  • Swagger是一款RESTful介面的文件線上生成和介面測試工具
  • Swagger是一個規範完整的框架,用於生成,描述,呼叫和視覺化RESTful風格的web服務
  • 總體目標是使客戶端和檔案系統作為伺服器以同樣的速度更新
  • 檔案的方法,引數和模型緊密整合到伺服器端程式碼,允許API始終保持同步

Swagger作用

  • 介面文件線上自動生成
  • 功能測試

Swagger主要專案

  • Swagger-tools: 提供各種與Swagger進行整合和互動的工具. 比如Swagger Inspector,Swagger Editor
  • Swagger-core: 用於Java或者Scala的Swagger實現,與JAX-RS,Servlets和Play框架進行整合
  • Swagger-js: 用於JavaScript的Swagger實現
  • Swagger-node-express: Swagger模組,用於node.js的Express Web應用框架
  • Swagger-ui: 一個無依賴的html,js和css集合,可以為Swagger的RESTful API動態生成文件
  • Swagger-codegen: 一個模板驅動引擎,通過分析使用者Swagger資源宣告以各種語言生成客戶端程式碼

Swagger工具

  • Swagger Codegen:
    • 通過Codegen可以將描述檔案生成html格式和cwiki形式的介面文件,同時也能生成多種語言的服務端和客戶端的程式碼
    • 支援通過jar,docker,node等方式在本地化執行生成,也可以在後面Swagger Editor中線上生成
  • Swagger UI:
    • 提供一個視覺化的UI頁面展示描述檔案
    • 介面的呼叫方,測試,專案經理等都可以在該頁面中對相關介面進行查閱和做一些簡單的介面請求
    • 該專案支援線上匯入描述檔案和本地部署UI專案
  • Swagger Editor:
    • 類似於markdown編輯器用來編輯Swagger描述檔案的編輯器
    • 該編輯器支援實時預覽描述檔案的更新效果
    • 提供了線上編輯器和本地部署編輯器兩種方式
  • Swagger Inspector:
    • 線上對介面進行測試
    • 會比Swagger裡面做介面請求會返回更多的資訊,也會儲存請求的實際請求引數等資料
  • Swagger Hub:
    • 整合上面的所有工具的各個功能
    • 可以以專案和版本為單位,將描述檔案上傳到Swagger Hub中,在Swagger Hub中可以完成上面專案的所有工作

Swagger註解

@Api

該註解將一個controller類標註為一個Swagger API. 在預設情況下 ,Swagger core只會掃描解析具有 @Api註解的類,而忽略其它類別的資源,比如JAX-RS endpoints, Servlets等註解. 該註解的屬性有:

  • tags: API分組標籤,具有相同標籤的API將會被歸併在一組內顯示
  • value: 如果tags沒有定義 ,value將作為Apitags使用
@ApiOperation

在指定介面路徑上,對一個操作或者http方法進行描述. 具有相同路徑的不同操作會被歸組為同一個操作物件. 緊接著是不同的http請求方法註解和路徑組合構成一個唯一操作. 該註解的屬性有:

  • value: 對操作進行簡單說明
  • notes: 對操作進行詳細說明
  • httpMethod: http請求動作名,可選值有 :GET, HEAD, POST, PUT, DELETE, OPTIONS, PATCH
  • code: 成功操作後的返回型別. 預設為200, 參照標準Http Status Code Definitions
@ApiParam

增加對引數的元資訊說明,緊接著使用Http請求引數註解. 主要屬性有:

  • required: 是否為必傳引數
  • value: 引數簡短說明
@ApiResponse

描述一個操作的可能返回結果. 當RESTful請求發生時,這個註解可用於描述所有可能的成功與錯誤碼.可以使用也可以不使用這個註解去描述操作返回型別. 但成功操作後的返回型別必須在 @ApiOperation中定義. 如果API具有不同的返回型別,那麼需要分別定義返回值,並將返回型別進行關聯. 但是Swagger不支援同一返回碼,多種返回型別的註解. 這個註解必須被包含在 @ApiResponses中:

  • code: http請求返回碼,參照標準Http Status Code Definitions
  • message: 更加易於理解的文字訊息
  • response: 返回型別資訊,必須使用完全限定類名,即類的完整路徑
  • responseContainer: 如果返回值型別為容器型別,可以設定相應的值. 有效值 :List, Set, Map. 其它的值將會被忽略
@ApiResponses

註解 @ApiResponse的包裝類,陣列結構. 即使需要使用一個 @ApiResponse註解,也需要將 @ApiResponse註解包含在註解 @ApiResponses

@ApiImplicitParam

對API的單一引數進行註解. 註解 @ApiParam需要同JAX-RS引數相繫結, 但這個 @ApiImplicitParam註解可以以統一的方式定義引數列表,這是在Servlet和非JAX-RS環境下唯一的方式引數定義方式. 注意這個註解 @ApiImplicitParam必須被包含在註解 @ApiImplicitParams之內,可以設定以下重要屬性:

  • name: 引數名稱
  • value: 引數簡短描述
  • required: 是否為必傳引數
  • dataType: 引數型別,可以為類名,也可以為基本型別,比如String,int,boolean
  • paramType: 引數的請求型別,可選的值有path, query, body, header, from
@ApiImplicitParams

註解 @ApiImplicitParam的容器類,以陣列方式儲存

@ApiModel

提供對Swagger model額外資訊的描述. 在標註 @ApiOperation註解的操作內,所有類將自動introspected. 利用這個註解可以做一些更詳細的model結構說明. 主要屬性值有:

  • value: model的別名,預設為類名
  • description: model的詳細描述
@ApiModelProperty

對model屬性的註解,主要屬性值有:

  • value: 屬性簡短描述
  • example: 屬性示例值
  • required: 是否為必須值

資料庫版本控制-Liquibase,flyway

Liquibase

Liquibase基本概念

  • Liquibase是一個用於跟蹤,管理和應用資料庫變化的資料重構和遷移的開源工具,通過日誌檔案的形式記錄資料庫的變更,然後執行日誌檔案中的修改,將資料庫更新或回滾到一致的狀態
  • Liquibase的主要特點:
    • 不依賴於特定的資料庫,支援所有主流的資料庫. 比如MySQL, PostgreSQL, Oracle, SQL Server, DB2等.這樣在資料庫的部署和升級環節可以幫助應用系統支援多資料庫
    • 提供資料庫比較功能,比較結果儲存在XML中,基於XML可以用Liquibase部署和升級資料庫
    • 支援多開發者的協作維護,以XML儲存資料庫變化,以authorid唯一標識一個changeSet, 支援資料庫變化的合併
    • 日誌檔案支援多種格式. 比如XML, YAML, JSON, SQL
    • 支援多種執行方式. 比如命令列, Spring整合, Maven外掛, Gradle外掛
    • 在資料庫中儲存資料庫修改歷史DatabaseChangeHistory, 在資料庫升級時自動跳過已應用的變化
    • 提供變化應用的回滾功能,可按時間,數量或標籤tag回滾已經應用的變化
    • 可生成html格式的資料庫修改文件

日誌檔案changeLog

  • changeLogLiquibase用來記錄資料庫變更的日誌檔案,一般放在classpath下,然後配置到執行路徑中
  • changeLog支援多種格式, 主要有XML, JSON, YAML, SQL, 推薦使用XML格式
  • 一個 < changeSet > 標籤對應一個變更集, 由屬性id, name, changelog的檔案路徑唯一標識組合而成
  • changelog在執行時不是按照id的順序,而是按照changSetchanglog中出現的順序
  • 在執行changelog,Liquibase會在資料庫中新建2張表,寫執行記錄:databasechangelog - changelog的執行日誌databasechangeloglock - changelog鎖日誌
  • 在執行changelog中的changeSet時,會首先檢視databasechangelog表,如果已經執行過,則會跳過,除非changeSetrunAlways屬性為true, 如果沒有執行過,則執行並記錄changelog日誌
  • changelog中的一個changeSet對應一個事務,在changeSet執行完後commit, 如果出現錯誤就會rollback

常用標籤及命令

changeSet標籤

< changeSet > 標籤的主要屬性有:

  • runAlways: 即使執行過,仍然每次都要執行
    • 由於databasechangelog中還記錄了changeSetMD5校驗值MD5SUM, 如果changeSetidname沒變,而內容變化.則MD5值變化,這樣即使runAlways的值為true, 也會導致執行失敗報錯.
    • 這時應該使用runOnChange屬性
  • runOnChange: 第一次的時候以及當changeSet發生變化的時候執行,不受MD5校驗值的約束
  • runInTransaction: 是否作為一個事務執行,預設為true.
    • 如果設定為false, 需要注意: 如果執行過程中出錯了不會rollback, 會導致資料庫處於不一致的狀態

< changeSet > 有一個 < rollback > 子標籤,用來定義回滾語句:

  • 對於create table, rename column, add column,Liquibase會自動生成對應的rollback語句
  • 對於drop table, insert data等需要顯式定義rollback語句
include標籤
  • changelog檔案越來越多時,需要使用 < include > 標籤將檔案管理起來:
    • file: 包含的changelog檔案的路徑,這個檔案可以是Liquibase支援的任意格式
    • relativeToChangelogFile: 相對於changelogFile的路徑,表示file屬性的檔案路徑是相對於changelogFile的而不是classpath的,預設為false
<?xml version="1.0" encoding="utf-8"?>
<databaseChangeLog
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
    xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
    http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
    <include file="logset-20160408/0001_authorization_init.sql" relativeToChangelogFile="true"/>
</databaseChangeLog>
  • < include >標籤存在迴圈引用和重複引用的問題,迴圈引用會導致無限迴圈,需要注意
includeAll標籤
  • < includeAll > 標籤指定的是changelog的目錄,而不是檔案
<includeAll path="com/example/changelogs/"/>
diff命令
  • diff命令用於比較資料庫之間的異同
java -jar liquibase.jar --driver=com.mysql.jdbc.Driver \
	 --classpath=./mysql-connector-java-5.1.29.jar \
	 --url=jdbc:mysql://127.0.0.1:3306/test \
	 --username=root --password=passwd \
	 diff \
	 --referenceUrl=jdbc:mysql://127.0.0.1:3306/authorization \
	 --referenceUsername=root --referencePassword=passwd
generateChangeLog
  • 在已有專案上使用LiquiBase, 需要生成當前資料的changeSet, 可以使用兩種方式:
    • 使用資料庫工具匯出SQL資料,然後在changLog檔案中以SQL格式記錄
    • 使用generateChangeLog命令生成changeLog檔案
liquibase --driver=com.mysql.jdbc.Driver \
		  - classpath=./mysql-connector-java-5.1.29.jar \
		  - changeLogFile=liquibase/db.changeLog.xml \
		  --url=jdbc:mysql://127.0.0.1:3306/test \
		  --username=root
		  --password=root
		  generateChangeLog 

generateChangeLog不支援儲存過程,函式以及觸發器

Liquibase使用示例

  • application.properties中配置changeLog路徑:
# Liquibase配置
liquibase=true
# changelog預設路徑
liquibase.change-log=classpath:/db/changelog/sqlData.xml
  • xml配置sample:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
	<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-2.0.xsd">
		<changeSet author="chova" id="sql-01">
			<sqlFile path="classpath:db/changelog/sqlfile/init.sql" encoding="UTF-8" />
			<sqlFile path="classpath:db/changelog/sqlfile/users.sql" encoding="UTF-8" />
		</changeSet>

		<changeSet author="chova" id="sql-02">
			<sqlFile path="classpath:db/changelog/sqlfile/users2.sql" encoding="UTF-8" />
		</changeSet>
	</databaseChangeLog>
  • 待執行的SQL語句 - init.sql:
CREATE TABLE usersTest(
	user_id varchar2(14)  DEFAULT '' NOT NULL,
	user_name varchar2(128) DEFAULT '' NOT NULL
)STORAGE(FREELISTS 20 FREELIST GROUPS 2) NOLOGGING TABLESPACE USER_DATA;

insert into usersTest(user_id,user_name) values ('0','test');
  • 啟動專案.
  • 在maven配置外掛生成已有資料庫的changelog檔案: 需要在pom.xml中增加配置,然後配置liquibase.properties
<build>
	<plugins>
		<plugin>
			<groupId>org.liquibase</groupId>
			<artifactId>liquibase-maven-plugin</artifactId>
			<version>3.4.2</version>
			<configuration>
				<propertyFile>src/main/resources/liquibase.properties</propertyFile>
				<propertyFileWillOverride>true</propertyFileWillOverride>
				<!--生成檔案的路徑-->
				<outputChangeLogFile>src/main/resources/changelog_dev.xml</outputChangeLogFile>
			</configuration>
		</plugin>
	</plugins>
</build>
changeLogFile=src/main/resources/db/changelog/sqlData.xml
driver=oracle.jdbc.driver.OracleDriver
url=jdbc:oracle:thin:@chova
username=chova
password=123456
verbose=true
# 生成檔案的路徑
outputChangeLogFile=src/main/resources/changelog.xml

然後執行 [ mvn liquibase:generateChangeLog ] 命令,就是生成changelog.xml檔案

  • liquibase:update
    • 執行changeLog中的變更
    mnv liquibase:update
    
  • liquibase:rollback
    • rollbackCount: 表示rollbackchangeSet的個數
    • rollbackDate: 表示rollback到指定日期
    • rollbackTag: 表示rollback到指定的tag, 需要使用liquibase在具體的時間點上打上tag
      • rollbackCount示例:
      mvn liquibase:rollback -Dliquibase.rollbackCount=3
      
      • rollbackDate示例: 需要注意日期格式,必須匹配當前平臺執行DateFormat.getDateInstance() 得到的格式,比如 MMM d, yyyy
      mvn liquibase:rollback -Dliquibase.rollbackDate="Apr 10, 2020"
      
      • rollbackTag示例: 使用tag標識,需要先打tag, 然後rollbacktag
      mvn liquibase:tag -Dliquibase.tag=tag20200410
      
      mvn liquibase:rollback -Dliquibase.rollbackTag=tag20200410
      

flyway

flyway基本概念

  • flyway是一款資料庫版本控制管理工具,支援資料庫版本自動升級,不僅支援Command LineJava API, 同時也支援Build構建工具和SpringBoot, 也可以在分散式環境下安全可靠地升級資料庫,同時也支援失敗恢復
  • flyway是一款資料庫遷移 (migration) 工具,也就是在部署應用的時候,執行資料庫指令碼的應用,支援SQLJava兩種型別的指令碼,可以將這些指令碼打包到應用程式中,在應用程式啟動時,由flyway來管理這些指令碼的執行,這些指令碼在flyway中叫作migration
    • 沒有使用flyway時部署應用的流程:
      • 開發人員將程式應用打包,按順序彙總並整理資料庫升級指令碼
      • DBA拿到資料庫升級指令碼檢查,備份,執行,以完成資料庫升級
      • 應用部署人員拿到應用部署包,備份,替換,完成應用程式升級
    • 引入flyway時部署應用的流程:
      • 開發人員將程式打包
      • 應用部署人員拿到應用部署包,備份,替換,完成應用程式升級.期間flyway自動執行升級,備份指令碼
  • flyway的核心: MetaData表 - 用於記錄所有版本演化和狀態
  • flyway首次啟動會建立預設名為SCHMA_VERSION表,儲存了版本,描述和要執行的SQL指令碼
flyway主要特性
  • 普通SQL:SQL指令碼,包括佔位符替換,沒有專有的XML格式
  • 無限制: 可以通過Java程式碼實現高階資料操作
  • 零依賴: 只需執行在Java 6以上版本及資料庫所需的JDBC驅動
  • 約定大於配置: 資料庫遷移時,自動查詢系統檔案和類路徑中的SQL檔案或Java
  • 高可靠性: 在叢集環境下進行資料庫的升級是安全可靠的
  • 雲支援: 完全支援Microsoft SQL Azure, Google Cloud SQL & App Engine, Heroku PostgresAmazon RDS
  • 自動遷移: 使用flyway提供的API, 可以讓應用啟動和資料庫遷移同時工作
  • 快速失敗: 損壞的資料庫或失敗的遷移可以防止應用程式啟動
  • 資料庫清理: 在一個資料庫中刪除所有的表,檢視,觸發器. 而不是刪除資料庫本身
SQL指令碼
  • 格式 : V + 版本號 + 雙下劃線 + 描述 + 結束符
V1_INIT_DATABASE.sql
  • V是預設值,可以進行自定義配置:
flyway.sql-migration-prefix=指定字首 

flyway工作原理

  • flyway對資料庫進行版本管理主要由Metadata表和6種命令 : Migrate, Clean, Info, Validate, Undo, Baseline, Repair完成
  • 在一個空資料庫上部署整合flyway應用:
    在這裡插入圖片描述
  • 應用程式啟動時 ,flyway在這個資料庫中建立一張表,用於記錄migration的執行情況,表名預設為:schema_version:
    在這裡插入圖片描述
  • 然後 ,flyway根據表中的記錄決定是否執行應用程式包中提供的migration:
    在這裡插入圖片描述
  • 最後,將執行結果寫入schema_version中並校驗執行結果:
    在這裡插入圖片描述
  • 下次版本迭代時,提供新的migration, 會根據schema_version的記錄執行新的migration:
    在這裡插入圖片描述
    在這裡插入圖片描述
    在這裡插入圖片描述

flyway核心

Metadata Table
  • flyway中最核心的就是用於記錄所有版本演化和狀態的Metadata表
  • flyway首次啟動時會建立預設表名為SCHEMA_VERSION的後設資料表,表結構如下:
列名 型別 是否為null 鍵值 預設值
version_rank int(11) MUL NULL
installed_rank int(11) MUL NULL
version varchar(50) PRI NULL
description varchar(200) NULL
type varchar(20) NULL
script varchar(1000) NULL
checksum int(11) NULL
installed_by varchar(100) NULL
installed_on timestamp CURRENT_TIMESTAMP
execution_time int(11) NULL
success tinyint(1) MUL NULL
Migration
  • flyway將每一個資料庫指令碼稱之為migration,flyway主要支援兩種型別的migrations:
    • Versioned migrations:
      • 最常用的migration,用於版本升級
      • 每一個版本都有一個唯一的標識並且只能被應用一次,並且不能再修改已經載入過的Migrations,因為Metadata表會記錄Checksum值
      • version標識版本號由一個或多個數字構成,數字之間的分隔符可以採用點或下劃線,在執行時下劃線其實也是被替換成點了,每一部分的前導數字0都會被自動忽略
    • Repeatable migrations:
      • 指的是可重複載入的Migrations,每一次的更新會影響Checksum值,然後都會被重新載入,並不用於版本升級.對於管理不穩定的資料庫物件更新時非常有用
      • Repeatable的Migrations總是在Versioned的Migrations之後按順序執行,開發者需要維護指令碼並且確保可以重複執行.通常會在sql語句中使用CREATE OR REPLACE來確保可重複執行
  • Migration命名規範:
    在這裡插入圖片描述
  1. prefix: 字首標識.可以配置,預設情況下: V - Versioned, R - Repeatable
  2. version: 標識版本號. 由一個或多個數字構成,數字之間的分隔符可以使用點或者下劃線
  3. separator: 用於分割標識版本號和描述資訊. 可配置,預設情況下是兩個下劃線 _ _
  4. description: 描述資訊. 文字之間可以用下劃線或空格分割
  5. suffix: 後續標識. 可配置,預設為 .sql
  • 確保版本號唯一 ,flyway按照版本號順序執行 . repeatable沒有版本號,因為repeatable migration會在內容改變時重複執行
  • 預設情況下 ,flyway會將單個migration放在一個事務裡執行,也可以通過配置將所有migration放在同一個事務裡執行
  • 每個Migration支援兩種編寫方式:
    • Java API
    • SQL指令碼
  • Java API: 通過實現org.flywaydb.core.api.migration.jdbc.JdbcMigration介面來建立一個Migration, 也就是通過JDBC來執行SQL, 對於類是CLOB或者BLOB這種不適合在SQL中實現的指令碼比較方便
public class V1_2_Another_user implements JdbcMigration {
	public void migrate(Connection connection) throws Exception {
		PreparedStatement statement = connection.prepareStatement("INSERT INTO test_user (name) VALUES ("Oxford")");
		try {
			statement.execute();
		} finally {
			statement.close();
		}
	}
}
  • SQL指令碼: 簡單的SQL指令碼檔案
// 單行命令
CREATE TABLE user (name VARCHAR(25) NOT NULL, PRIMARY KEY(name));
 
 // 多行命令
 -- Placeholder
 INSERT INTO ${tableName} (name) VALUES ("oxford");
Callbacks
  • flyway在執行migration時提供了一系列的hook, 可以在執行過程中進行額外的操作:
Name Execution
beforeMigrate Before Migrate runs
beforeEachMigrate Before every single migration during Migrate
afterEachMigrate After every single successful migration during Migrate
afterEachMigrateError After every single failed migration during Migrate
afterMigrate After successful Migrate runs
afterMigrateError After failed Migrate runs
beforeClean Before clean runs
afterClean After successful Clean runs
afterCleanError After failed Clean runs
beforeInfo Before Info runs
afterInfo After successful Info runs
afterInfoError After failed Info runs
beforeValidate Before Validate runs
afterValidate After successful Validate runs
afterValidateError After failed Validate runs
beforeBaseline Before Baseline runs
afterBaseline After successful Baseline runs
afterBaselineError After failed Baseline runs
beforeRepair BeforeRepair
afterRepair After successful Repair runs
afterRepairError After failed Repair runs
  • 只要將migration的名稱以hook開頭,這些hook就可以執行SQL和Java型別的migrations:
    • SQL型別的hook:
      • beforeMigrate.sql
      • beforeEachMigrate.sql
      • beforeRepair_vacuum.sql
    • Java型別的hook需要實現介面 : org.flyway.core.api.callback.CallBack
flyway中6種命令
  • Migrate:
    • 將資料庫遷移到最新版本,是flyway工作流的核心功能.
    • flywayMigrate時會檢查後設資料Metadata表.如果不存在會建立Metadata表,Metadata表主要用於記錄版本變更歷史以及Checksum之類
    • Migrate時會掃描指定檔案系統或classpath下的資料庫的版本指令碼Migrations, 並且會逐一比對Metadata表中已經存在的版本記錄,如果未應用的Migrations,flyway會獲取這些Migrations並按次序Apply到資料庫中,否則不會做任何事情
    • 通常會在應用程式啟動時預設執行Migrate操作,從而避免程式和資料庫的不一致
  • Clean:
    • 來清除掉對應資料庫的Schema的所有物件 .flyway不是刪除整個資料庫,而是清除所有表結構,檢視,儲存過程,函式以及所有相關的資料
    • 通常在開發和測試階段使用,能夠快速有效地更新和重新生成資料庫表結構.但是不應該在production的資料庫使用
  • Info:
    • 列印所有Migrations的詳細和狀態資訊,是通過Metadata表和Migrations完成的
    • 能夠快速定位當前資料庫版本,以及檢視執行成功和失敗的Migrations
  • Validate:
    • 驗證已經ApplyMigrations是否有變更 ,flyway是預設開啟驗證的
    • 操作原理是對比Metadata表與本地MigrationChecksum值,如果相同則驗證通過,否則驗證失敗,從而可以防止對已經Apply到資料庫的本地Migrations的無意修改
  • Baseline:
    • 針對已經存在Schema結構的資料庫的一種解決方案
    • 實現在非空資料庫中新建Metadata表,並將Migrations應用到該資料庫
    • 可以應用到特定的版本,這樣在已有表結構的資料庫中也可以實現新增Metadata表,從而利用flyway進行新的Migrations的管理
  • Repair:
    • 修復Metadata表,這個操作在Metadata表表現錯誤時很有用
    • 通常有兩種用途:
      • 移除失敗的Migration記錄,這個問題針對不支援DDL事務的資料庫
      • 重新調整已經應用的MigrationsChecksums的值. 比如,某個Migration已經被應用,但本地進行了修改,又期望重新應用並調整Checksum值. 不建議對資料庫進行本地修改

flyway的使用

正確建立Migrations
  • Migrations: flyway在更新資料庫時使用的版本指令碼
    • 一個基於sqlMigration命名為V1_ _init_tables.sql, 內容即為建立所有表的sql語句
    • flyway也支援基於JavaMigration
    • flyway載入Migrations的預設Locationsclasspath:db/migration, 也可以指定filesystem:/project/folder. Migrations的載入是在執行時自動遞迴執行的
  • 除了指定的Locations外,flyway需要遵從命名格式對Migrations進行掃描,主要分為兩類:
    • Versioned migrations:
      • Versioned型別是常用的Migration型別
      • 用於版本升級,每一個版本都有一個唯一的標識並且只能被應用一次. 並且不能再修改已經載入過的Migrations, 因為Metadata表會記錄Checksum
      • 其中的version標識版本號,由一個或者多個數字構成,數字之間的分隔符可以採用點或者下劃線,在執行時下劃線也是被替換成點了. 每一部分的前導零都會被省略
    • Repeatable migrations:
      • Repeatable是指可重複載入的Migrations, 其中每一次更新都會更新Checksum值,然後都會被重新載入,並不用於版本升級. 對於管理不穩定的資料庫物件的更新時非常有用
      • RepeatableMigrations總是在Versioned之後按順序執行,開發者需要維護指令碼並確保可以重複執行,通常會在sql語句中使用CREATE OR REPLACE來保證可重複執行
flyway資料庫
  • flyway支援多種資料庫:
    • Oracle
    • SQL Server
    • SQL Azure
    • DB2
    • DB2 z/OS
    • MySQL
    • Amazon RDS
    • Maria DB
    • Google Cloud SQL
    • PostgreSQL
    • Heroku
    • Redshift
    • Vertica
    • H2
    • Hsql
    • Derby
    • SQLite
    • SAP HANA
    • solidDB
    • Sybase ASE and Phoenix
  • 目前主流使用的資料庫有MySQL,H2,HsqlPostgreSQL. 對應的flyway.url配置如下:
# MySQL
flyway.url=jdbc:mysql://localhost:3306/db?serverTimezone=UTC&useSSL=true

# H2
flyway.url=jdbc:h2:./.tmp/db

# Hsql
flyway.url=jdbc:hsqldb:hsql//localhost:1476/db

# PostgreSQL
flyway.url=jdbc:postgresql://localhost:5432/postgres?currentSchema=schema
flyway命令列
  • flyway命令列工具支援直接在命令列中執行Migrate,Clean,Info,Validate,Baseline和Repair這6種命令
  • flyway會依次搜尋以下配置檔案:
    • /conf/flyway.conf
    • /flyway.conf
    • 後面的配置會覆蓋前面的配置
SpringBoot整合flyway
  • 引入flyway依賴:
<dependency>
	<groupId>org.flywaydb</groupId>
	<artifactId>flyway-core</artifactId>
	<version>5.0.3</version>
</dependency>

<plugin>
	<groupId>org.flywaydb</groupId>
	<artifactId>flyway-maven-plugin</artifactId>
	<version>5.0.3</version>
</plugin>
  • 建立的springboot的maven專案,配置資料來源資訊:
server.port=8080
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/db
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
  • 在classpath目錄下新建/db/migration資料夾,並建立SQL指令碼:
use db;

CREATE TABLE person (
	id int(1) NOT NULL AUTO_INCREMENT,
	firstname varchar(100) NOT NULL,
	lastname varchar(100) NOT NULL,
	dateofbirth DATE DEFAULT NULL,
	placeofbirth varchar(100) NOT NULL, PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

insert into person (firstname,lastname,dateofbirth,placeofbirth) values ('oxford','Eng',STR_TO_DATE('02/10/1997', '%m/%d/%Y'),'China');
insert into person (firstname,lastname,dateofbirth,placeofbirth) values ('oxfordd','Engg',STR_TO_DATE('02/10/1995', '%m/%d/%Y'),'China');
  • 啟動springboot專案:
    • 在專案啟動時 ,flyway載入了SQL指令碼並執行
  • 檢視資料庫:
    • 預設情況下,生成flyway-schema-history
    • 如果需要指定schema表的命名,可以配置屬性 : flyway.tableflyway
flyway配置
屬性名 預設值 描述
baseline-description / 對執行遷移時基準版本的描述
baseline-on-migrate false 當遷移發現目標schema非空,而且帶有沒有後設資料的表時,是否自動執行基準遷移
baseline-version 1 開始執行基準遷移時對現有的schema的版本設定標籤
check-location false 檢查遷移指令碼的位置是否存在
clean-on-validation-error false 校驗錯誤時是否自動呼叫clean操作清空資料
enabled true 是否開啟flyway
encoding UTF-8 設定遷移時的編碼
ignore-failed-future-migration false 當讀取後設資料時,是否忽略錯誤的遷移
init-sqls / 初始化連線完成時需要執行的SQL
locations db/migration 遷移指令碼的位置
out-of-order false 是否允許無序遷移
password / 目標資料庫密碼
placeholder-prefix ${ 設定每個placeholder的字首
placeholder-suffix } 設定每個placeholder的字尾
placeholders.[placeholder name] / 設定placeholder的value
placeholder-replacement true placeholders是否要被替換
schemas 預設的schema 設定flyway需要遷移的schema,大小寫敏感
sql-migration-prefix V 遷移檔案的字首
sql-migration-separator _ _ 遷移指令碼的檔名分隔符
sql-migration-suffix .sql 遷移指令碼的字尾
tableflyway schema_version 使用的後設資料表名
target latest version 遷移時使用的目標版本
url 配置的主資料來源 遷移時使用的JDBC URL
user / 遷移資料庫的使用者名稱
validate-on-migrate true 遷移時是否校驗

部署-Docker

Docker基本概念

  • Docker
    • 是用於開發應用,交付應用,執行應用的開源軟體的一個開放平臺
    • 允許使用者將基礎設施中的應用單獨分割出來,形成更細小的容器,從而提交交付軟體的速度
  • Docker容器:
    • 類似虛擬機器,不同點是:
      • Docker容器是將作業系統層虛擬化
      • 虛擬機器則是虛擬化硬體
    • Docker容器更具有便攜性,能夠高效地利用伺服器
    • 容器更多的是用於表示軟體的一個標準化單元,由於容器的標準化,因此可以無視基礎設施的差異,部署到任何一個地方
    • Docker也為容器提供更強的業界隔離相容
  • Docker利用Linux核心中的資源分離機制cgroups以及Linux核心的namespace來建立獨立的容器containers
    • 可以在Linux實體下運作,避免引導一個虛擬機器造成的額外負擔
    • Linux核心對namespace的支援可以完全隔離工作環境下的應用程式,包括:
      • 執行緒樹
      • 網路
      • 使用者ID
      • 掛載檔案系統
    • Linux核心的cgroups提供資源隔離,包括:
      • CPU
      • 儲存器
      • block I/O
      • 網路

Docker基礎架構

Docker引擎
  • Docker引擎: Docker Engine
    • 是一個服務端 - 客戶端結構的應用
    • 主要組成部分:
      • Docker守護程式: Docker daemons,也叫dockerd.
        • 是一個持久化程式,使用者管理容器
        • Docker守護程式會監聽Docker引擎API的請求
      • Docker引擎API: Docker Engine API
        • 用於與Docker守護程式互動使用的API
        • 是一個RESTful API,不僅可以被Docker客戶端呼叫,也可以被wget和curl等命令呼叫
      • Docker客戶端: docker
        • 是大部分使用者與Docker互動的主要方式
        • 使用者通過客戶端將命令傳送給守護程式
        • 命令遵循Docker Engine API
Docker註冊中心
  • Docker註冊中心: Docker registry,用於儲存Docker映象
  • Docker Hub: Docker的公共註冊中心,預設情況下,Docker在這裡尋找映象.也可以自行構建私有的註冊中心
Docker物件
  • Docker物件指的是 :Images,Containers,Networks, Volumes,Plugins等等
    • 映象: Images
      • 一個只讀模板,用於指示建立容器
      • 映象是分層構建的,定義這些層次的檔案叫作Dockerfile
    • 容器: Containers
      • 映象可執行的例項
      • 容器可以通過API或者CLI(命令列)進行操作
    • 服務: Services
      • 允許使用者跨越不同的Docker守護程式的情況下增加容器
      • 並將這些容器分為管理者(managers)和工作者(workers),來為swarm共同工作

Docker擴充套件架構

Docker Compose
  • Docker Compose是用來定義和執行多個容器Docker應用程式的工具
  • 通過Docker Compose, 可以使用YAML檔案來配置應用程式所需要的所有服務,然後通過一個命令,就可以建立並啟動所有服務
  • Docker Compose對應的命令為 : docker-compose
Swarm Mode
  • Docker 1.12以後 ,swarm mode整合到Docker引擎中,可以使用Docker引擎APICLI命令直接使用
  • Swarm Mode內建 k-v 儲存功能,特點如下:
    • 具有容錯能力的去中心化設計
    • 內建服務發現
    • 負載均衡
    • 路由網格
    • 動態伸縮
    • 滾動更新
    • 安全傳輸
  • Swarm Mode的相關特性使得Docker本地的Swarm叢集具備與Mesos.Kubernetes競爭的實力
  • cluster: 叢集
    • Docker將叢集定義為 - 一群共同作業並提供高可用性的機器
  • swarm:
    • 一個叢集的Docker引擎以swarm mode形式執行
      • swarm mode是指Docker引擎內嵌的叢集管理和編排功能
      • 當初始化一個cluster中的swarm或者將節點加入一個swarm,Docker引擎就會以swarm mode的形式執行
  • Swarm Mode原理:
    • swarm中的Docker機器分為兩類:
      • managers: 管理者. 用於處理叢集關係和委派
      • workers: 工作者. 用於執行swarm服務
        • 當建立swarm服務時,可以增加各種額外的狀態: 數量,網路,埠,儲存資源等等
        • Docker會去維持使用者需要的狀態:
          • 比如,一個工作節點當機後,那麼Docker就會把這個節點的任務委派給另外一個節點
          • 這裡的任務task是指: 被swarm管理者管理的一個執行中的容器
  • swarm相對於單獨容器的優點:
    • 修改swarm服務的配置後無需重啟
    • Dockerswarm mode形式執行時,可以選擇直接啟動單獨的容器
    • swarm mode下,可以通過docker stack deploy使用Compose檔案部署應用棧
  • swarm服務分為兩種:
    • replicated services: 可以指定節點任務的總數量
    • global services: 每個節點都會執行一個指定任務
  • swarm管理員可以使用ingress負載均衡使服務可以被外部接觸
  • swarm管理員會自動地給服務分配PublishedPort, 或者手動配置.
    • 外部元件,比如雲負載均衡器能通過叢集中任何節點上的PublishedPort去介入服務,無論服務是否啟動
  • Swarm Mode有內部DNS元件,會為每個服務分配一個DNS條目 . swarm管理員使用internal load balancing去分發請求時,就是依靠的這個DNS元件
  • Swarm Mode功能是由swarmkit提供的,實現了Docker的編排層,使得swarm可以直接被Docker使用

檔案格式

  • Docker有兩種檔案格式:
    • Dockerfile: 定義了單個容器的內容和啟動時候的行為
    • Compose檔案: 定義了一個多容器應用
Dockerfile
  • Docker可以依照Dockerfile的內容,自動化地構建映象
    • Dockerfile包含著使用者想要如何構建映象的所有命令的文字
    FROM ubuntu:18.04
    COPY . /app
    RUN make /app
    CMD python /app/app.py
    
    • RUN:
      • RUN會在當前映象的頂層上新增新的一層layer,並在該層上執行命令,執行結果將會被提交
      • 提交後的結果將會應用於Dockerfile的下一步
    • ENTRYPOINT:
      • 入口點
      • ENTRYPOINT允許配置容器,使之成為可執行程式. 即ENTRYPOINT允許為容器增加一個入口點
      • ENTRYPOINT與CMD類似,都是在容器啟動時執行,但是ENTRYPOINT的操作穩定並且不可被覆蓋
      • 通過在命令列中指定 - -entrypoint命令的方式,可以在執行時將Dockerfile檔案中的ENTRYPOINT覆蓋
    • CMD:
      • command的縮寫
      • CMD用於為已經建立的映象提供預設的操作
      • 如果不想使用CMD提供的預設操作,可以使用docker run IMAGE [:TAG|@DIGEST] [COMMAND] 進行替換
      • 當Dockerfile擁有入口點的情況下,CMD用於為入口點賦予引數
Compose檔案
  • Compose檔案是一個YAML檔案,定義了服務, 網路 和卷:
    • service: 服務. 定義各容器的配置,定義內容將以命令列引數的方式傳給docker run命令
    • network: 網路. 定義各容器的配置,定義內容將以命令列引數的方式傳給docker network create命令
    • volume: 卷. 定義各容器的配置,定義內容將以命令列引數的方式傳給docker volume create命令
  • docker run命令中有一些選項,和Dockerfile檔案中的指令效果是一樣的: CMD, EXPOSE, VOLUME, ENV. 如果Dockerfile檔案中已經使用了這些命令,那麼這些指令就被視為預設引數,所以無需在Compose檔案中再指定一次
  • Compose檔案中可以使用Shell變數:
db:
 image: "postgres:${POSTGRES_VERSION}" 
  • Compse檔案可通過自身的ARGS變數,將引數傳遞給Dockerfile中的ARGS指令

網路

bridge
  • Docker中的網橋使用的軟體形式的網橋
  • 使用相同的網橋的容器連線進入該網路,非該網路的容器無法進入
  • Docker網橋驅動會自動地在Docker主機上安裝規則,這些規則使得不同橋接網路之間不能直接通訊
  • 橋接經常用於:
    • 在單獨容器上執行應用時,可以通過網橋進行通訊
  • 網橋網路適用於容器執行在相同的Docker守護程式的主機上
  • 不同Docker守護程式主機上的容器之間的通訊需要依靠作業系統層次的路由,或者可以使用overlay網路進行代替
  • bridge: 網橋驅動
    • 是Docker預設的網路驅動,介面名為docker0
    • 當沒有為容器指定一個網路時,Docker將使用這個驅動
    • 可以通過daemon.json檔案修改相關配置
  • 自定義網橋可以通過 brtcl 命令進行配置
host
  • host: 主機模式
    • 用於單獨容器,該網路下容器只能和Docker主機進行直接連線
    • 這種host主機模式只適用於Docker 17.06以後版本的swarm服務
  • host網路和VirtualBox的僅主機網路Host-only Networking類似
overlay
  • overlay: 覆蓋模式
    • 網路驅動將會建立分散式網路,該網路可以覆蓋若干個Docker守護程式主機
    • overlay是基於主機特定網路host-specific networks, 當加密功能開啟時,允許swarm服務和容器進行安全通訊
    • 在覆蓋網路overlay下,Docker能夠清晰地掌握資料包路由以及傳送接收容器
  • overlay有兩種網路型別網路:
    • ingress: 是可掌控swarm服務的網路流量, ingress網路是overlay的預設網路
    • docker_gwbridge: 網橋網路, docker_gwbridge網路會將單獨的Docker守護程式連線至swarm裡的另外一個守護程式
  • overlay網路下:
    • 單獨的容器swarm服務的行為和配置概念是不一樣的
  • overlay策略不需要容器具有作業系統級別的路由,因為Docker負責路由
macvlan
  • macvlan:
    • 允許賦予容器MAC地址
    • 在該網路裡,容器會被認為是物理裝置
none
  • 在該策略下,容器不使用任何網路
  • none常常用於連線自定義網路驅動的情況下
其它網路策略模式
  • 要想運用其它網路策略模式需要依賴其它第三方外掛

資料管理

  • 在預設情況下,Docker所有檔案將會儲存在容器裡的可寫的容器層container layer:
    • 資料與容器共為一體: 隨著容器的消失,資料也會消失. 很難與其它容器程式進行資料共享
    • 容器的寫入層與宿主機器緊緊耦合: 很難移動資料到其它容器
    • 容器的寫入層是通過儲存驅動storage driver管理檔案系統: 儲存驅動會使用Linux核心的鏈合檔案系統union filesystem進行掛載,相比較於直接操作宿主機器檔案系統的資料卷,這個額外的抽象層會降低效能
  • 容器有兩種永久化儲存方式:
    • volumes:
    • bind mounts: 繫結掛載
  • Linux中可以使用tmpfs進行掛載, windows使用者可以使用命名管道named pipe.
  • 在容器中,不管使用哪種永久化儲存,表現形式都是一樣的
  • 卷: volumes.
    • 是宿主機器檔案系統的一部分
    • Docker進行管理. 在Linux中,卷儲存於 /var/lib/docker/volumes/
    • Docker程式不應該去修改這些檔案
  • Docker推薦使用捲進行持久化資料
  • 卷可以支援卷驅動volume drivers: 該驅動允許使用者將資料儲存到遠端主機雲服務商cloud provider或其它
  • 沒有名字的卷叫作匿名卷anonymous volume. 有名字的卷叫作命名卷named volume. 匿名卷沒有明確的名字,當被初始化時,會被賦予一個隨機名字
繫結掛載
  • 繫結掛載: bind mounts
    • 通過將宿主機器的路徑掛載到容器裡的這種方式,從而實現資料持續化,因此繫結掛載可將資料儲存在宿主機器的檔案系統中的任何地方
    • 非Docker程式可以修改這些檔案
    • 繫結掛載在Docker早起就已經存在,與卷儲存相比較,繫結掛載十分簡單明瞭
    • 在開發Docker應用時,應使用命名卷named volume代替繫結掛載,因為使用者不能對繫結掛載進行Docker CLI命令操作
  • 繫結掛載的使用場景:
    • 同步配置檔案
      • 將宿主機的DNS配置檔案(/etc/resolv.conf)同步到容器中
    • 在開發程式過程中,將原始碼或者Artifact同步至容器中. 這種用法與Vagrant類似
tmpfs掛載
  • tmpfs掛載: tmpfs mounts
    • 僅僅儲存於記憶體中,不操作宿主機器的檔案系統.即不持久化於磁碟
    • 用於儲存一些非持久化狀態,敏感資料
      • swarm服務通過tmpfs將secrets資料(密碼,金鑰,證照等)儲存到swarm服務
命名管道
  • 命名管道: named pipes
    • 通過pipe掛載的形式,使Docker主機和容器之間互相通訊
      • 在容器內執行第三方工具,並使用命名管道連線到Docker Engine API
覆蓋問題
  • 掛載空的卷至一個目錄中,目錄中你的內容會被複制於卷中,不會覆蓋
  • 如果掛載非空的卷繫結掛載至一個目錄中,那麼該目錄的內容將會被隱藏obscured,當解除安裝後內容將會恢復顯示

日誌

  • 在Linux和Unix中,常見的I/O流分為三種:
    • STDIN: 輸入
    • STDOUT: 正常輸出
    • STDERR: 錯誤輸出
  • 預設配置下,Docker的日誌所記載的是命令列的輸出結果:
    • STDOUT : /dev/stdout
    • STDERR : /dev/stderr
  • 也可以在宿主主機上檢視容器的日誌,使用命令可以檢視容器日誌的位置:
docker inspect --format='{{.LogPath}}' $INSTANCE_ID

持續整合-jenkins

jenkins基本概念

  • jenkins是一個開源的,提供友好操作頁面的持續整合(CI)工具
  • jenkins主要用於持續,自動的構建或者測試軟體專案,監控外部任務的執行
  • jenkins使用Java語言編寫,可以在Tomcat等流行的servlet容器中執行,也可以獨立執行
  • 通常與版本管理工具SCM, 構建工具結合使用
  • 常用的版本控制工具有SVN,GIT
  • 常見的構建工具有Maven,Ant,Gradle

CI/CD

  • CI: Continuous integration, 持續整合
    • 持續整合強調開發人員提交新程式碼之後,like進行構建,單元測試
    • 根據測試結果,可以確定新程式碼和原有程式碼能否正確地合併在一起
      在這裡插入圖片描述
  • CD: Continuous Delivery, 持續交付
    • 在持續整合的基礎上,將整合後的程式碼部署到更貼近真實執行環境中,即類生產環境中
      • 比如在完成單元測試後,可以將程式碼部署到連線資料庫的Staging環境中進行更多的測試
    • 如果程式碼沒有問題,可以繼續手動部署到生產環境
      在這裡插入圖片描述

jenkins使用配置

  • 登入jenkins,點選新建,建立一個新的構建任務:
    在這裡插入圖片描述
  • 跳轉到新建介面:
    • 任務名稱可以自行設定,但需要全域性唯一
    • 輸入名稱後,選擇構建一個自由風格的軟體專案
    • 點選下方的建立按鈕
    • 這樣就建立了一個構建任務,然後會跳轉到該任務的配置頁面
      在這裡插入圖片描述
  • 在構建任務頁面,可以看到幾個選項:
    • General
    • 原始碼管理
    • 構建觸發器
    • 構建環境
    • 構建
    • 構建後操作
      -
General
  • General用於構建任務的一些基本配置: 名稱,描述等
    在這裡插入圖片描述
    在這裡插入圖片描述
    • 專案名稱: 剛才建立構建任務設定的名稱,可以在這裡進行修改
    • 描述: 對構建任務的描述
    • 丟棄舊的構建: 服務資源是有限的,如果儲存太多的歷史構建,會導致jenkins速度變慢,並且伺服器硬碟資源也會被佔滿
      • 保持構建天數: 可以自定義,根據實際情況確定一個合理的值
      • 保持構建的最大個數: 可以自定義,根據實際情況確定一個合理的值
原始碼管理
  • 原始碼管理用於配置程式碼的存放位置
    在這裡插入圖片描述
  • Git: 支援主流的github和gitlab程式碼倉庫
  • Repository URL: 倉庫地址
  • Credentials: 憑證. 可以使用HTTP方式的使用者名稱和密碼,也可以是RSA檔案.但是要通過後面的[ADD]按鈕新增憑證
  • Branches to build: 構建分支. */master表示master分支,也可以設定為另外的分支
  • 原始碼瀏覽器: 所使用的程式碼倉庫管理工具,如github,gitlab
  • URL: 填入上方的倉庫地址即可
  • Version: gitlab伺服器版本
  • Subversion: 就是SVN
構建觸發器
  • 構建任務的觸發器
    在這裡插入圖片描述
  • 觸發遠端構建(例如,使用指令碼): 這個選項會提供一個介面,可以用來在程式碼層面觸發構建
  • Build after other project are built: 在其它專案構建後構建
  • Build periodically: 週期性地構建.每隔一段時間進行構建
    • 日程表: 類似linux cronttab書寫格式. 下圖表示每隔30分鐘進行一次構建
      -
    • Build when a change is pushed to Gitlab: 常用的構建觸發器,當有程式碼push到gitlab程式碼倉庫時就進行構建
      • webhooks: 觸發構建的地址,需要將這個地址配置到gitlab中
    • Poll SCM: 這個功能需要與上面的這個功能配合使用. 當程式碼倉庫發生變動時,jekins並不知道. 這時,需要配置這個選項,週期性地檢查程式碼倉庫是否發生變動
      -
構建環境
  • 構建環境: 構建之前的準備工作. 比如指定構建工具,這裡使用Ant
    在這裡插入圖片描述
  • With Ant: 選擇這個選項,並指定Ant版本和JDK版本. 需要事先在jenkins伺服器上安裝這兩個版本的工具,並且在jenkins全域性工具中配置好
構建
  • 點選下方的增加構建步驟:
    -
    這裡有多種增加構建步驟的方式,在這裡介紹Execute shellInvoke Ant
  • Execute shell: 執行shell命令. 該工具是針對linux環境的,windows中對應的工具是 [Execute Windows batch command]. 在構建之前,需要執行一些命令: 比如壓縮包的解壓等等
  • Invoke Ant: Ant是一個Java專案構建工具,也可以用來構建PHP
    在這裡插入圖片描述
    • Ant Version: 選擇Ant版本. 這個Ant版本是安裝在jenkins伺服器上的版本,並且需要在jenkins[系統工具]中設定好
    • Targets: 需要執行的操作. 一行一個操作任務: 比如上圖的build構建,tar打包
    • Build File: Ant構建的配置檔案. 如果不指定,預設是在專案路徑下的workspace目錄中的build.xml
    • properties: 設定一些變數. 這些變數可以在build.l中被引用
  • Send files or execute commands over SSH: 傳送檔案到遠端主機或者執行命令指令碼
    -
    • Name: SSH Server的名稱. SSH Server可以在jenkins[系統設定]中配置
    • Source files: 需要傳送給遠端主機的原始檔
    • Remove prefix: 移除前面的路徑. 如果不設定這個引數,預設情況下遠端主機會自動建立構建源source file包含的路徑
    • Romote directory: 遠端主機目錄
    • Exec command: 在遠端主機上執行的命令或者指令碼
構建後操作
  • 構建後操作: 對構建完成的專案完成一些後續操作:比如生成相應的程式碼測試報告
    -
    在這裡插入圖片描述
  • Publish Clover PHP Coverage Report: 釋出程式碼覆蓋率的xml格式的報告. 路徑在build.xml中定義
  • Publish HTML reports: 釋出程式碼覆蓋率的HTML報告
  • Report Crap: 釋出Crap報告
  • E-mail Notification: 郵件通知. 構建完成後傳送郵件到指定的郵箱
    配置完成後,點選[儲存]

其它配置

SSH Server配置
  • 登入jenkins
  • 系統管理
  • 系統設定
    在這裡插入圖片描述
  • SSH Servers: jenkins伺服器公鑰檔案配置好之後新增SSH Server只需要配置這一個選項即可
    • name: 服務名稱.自定義,需要全域性唯一
    • HostName: 主機名. 直接使用IP地址即可
    • Username: 新增Server的使用者名稱,這裡配置的是root
    • Remote Directory: 遠端目錄. jenkins伺服器傳送檔案給新增的server時預設在這個目錄

Ant配置檔案 - build.xml

  • Ant構建配置檔案build.xml :
    -
  • project name: 專案名稱. 和jenkins所構建的專案名稱對應
  • target name="build": 構建的名稱. 和jekins構建步驟中的targets對應.
    • depends: 指明構建需要進行的一些操作
  • property: 用來設定變數
  • fileset: 指明一個資料夾
    • include: 指明需要包含的檔案
    • exclude: 指明不需要包含的檔案
    • tar: 打包這個資料夾匹配到的檔案
  • target: 實際的操作步驟:
    • make_runtime: 建立一些目錄
    • phpcs: 利用PHP_CodeSniffer工具對PHP程式碼規範與質量檢查工具
      在這裡插入圖片描述
      在這裡插入圖片描述
  • target name="tar": 打包檔案
    • 因為build中沒有包含這個target.所以預設情況下,執行build是不會打包檔案的
    • 所以在jenkins配置介面中Ant構建步驟中的[targets],才會有[build]和[tar]這兩個targets
    • 如果build.xml中build這個target depends中已經包含tar, 就不需要在jenkins中增加tar

配置Gitlab webhooks

  • 在gitlab的project頁面開啟settings
  • 開啟web hooks
  • 點選[ADD WEB HOOK] 來新增webhook
  • 將之前的jenkins配置中的url新增到這裡
  • 新增完成後,點選 [TEST HOOK] 進行測試,如果顯示SUCCESS則表示新增成功
    -
    在這裡插入圖片描述
    在這裡插入圖片描述在這裡插入圖片描述在這裡插入圖片描述
配置phpunit.xml
  • phpunit.xml: 是phpunit工具用來單元測試所需要的配置檔案
  • 這個檔案的名稱是可以自定義的,只要在build.xml中配置好名字即可
  • 預設情況下,如果使用phpunit.xml, 就不需要在build.xml中配置檔名
    在這裡插入圖片描述
  • fileset dir: 指定單元測試檔案所在路徑.
    • include: 指定包含哪些檔案,支援萬用字元
    • exclude: 指定不包含的檔案
      -

構建jenkins project

  • 第一次配置好jenkins project後,會觸發一次構建
  • 此後,每當有commit提交到master分支(根據配置中的分支觸發), 就會觸發一次構建
  • 也可以在project頁面手動觸發構建: 點選 [立即構建] 即可手動觸發構建
    -

構建結果說明

構建狀態
  • Successful: 藍色. 構建完成,並且是穩定的
  • Unstable: 黃色. 構建完成,但是是不穩定的
  • Failed: 紅色. 構建失敗
  • Disable: 灰色. 構建已禁用
構建穩定性
  • 構建穩定性用天氣表示: 天氣越好表示構建越穩定
    • 晴轉多雲
    • 多雲
    • 小雨
    • 雷陣雨
構建歷史介面
  • console output: 輸出構建的日誌資訊

jenkins許可權管理

  • jenkins中預設的許可權管理體系不支援使用者組和角色配置,因此需要安裝第三方外掛來支援角色的配置
  • 使用Role Strategy Plugin進行許可權管理:
  • 專案檢視:
    在這裡插入圖片描述
  • 安裝Role Strategy Plugin外掛
  • 安裝Role Stratey Plugin後進入系統設定頁面,按照如下配置後,點選 [儲存] :
    在這裡插入圖片描述
  • 點選 [系統管理] -> [Manage and Assign Roles] 進入角色管理頁面:
    在這裡插入圖片描述
  • 選擇 [Manager Roles], 按照下圖配置後點選 [儲存]:
    在這裡插入圖片描述
    • job_read只加overallread許可權
    • job_create只加jobcreate許可權
    • project roles中Pattern正規表示式和指令碼里的是不一樣的:
      • 比如過濾TEST開頭的jobs,要寫成 : TEST.,而不是 TEST
  • 進入[系統設定] -> [Manage and Assign Roles] -> [Assign Roles] , 按照如下模板配置後,點選 [儲存]
    在這裡插入圖片描述
    • Anonymous必須變成使用者,給job_create組和job_read組許可權,否則將沒有OverAllread許可權
    • project roles: 用於對應使用者不同的許可權
  • 驗證: 登入對應的使用者許可權後檢視使用者相關許可權
    在這裡插入圖片描述
  • 檢視通過正規表示式過濾job: 設定正規表示式為wechat.*,表示過濾所有以wechat開頭的專案
    在這裡插入圖片描述
  • 設定後的效果如圖:
    在這裡插入圖片描述
    在這裡插入圖片描述

自動化測試-TestNG

TestNG基本概念

  • TestNG是一個Java語言的開源測試框架,類似JUnit和NUnit,但是功能強大,更易於使用
  • TestNG的設計目標是為了覆蓋更廣泛的測試類別範圍:
    • 單元測試
    • 功能測試
    • 端到端測試
    • 整合測試
  • TestNG的主要功能:
    • 支援註解
    • 支援引數化和資料驅動測試: 使用@DataProvider或者XML配置
    • 支援同一類的多個例項: @Factory
    • 靈活的執行模式:
      • TestNG的執行,既可以通過Antbuild.xml: 有或這沒有一個測試套定義. 又可以通過帶有視覺化效果的IDE外掛
      • 不需要TestSuite類,測試包,測試組以及選擇執行的測試. 都通過XML檔案來定義和配置
    • 併發測試:
      • 測試可以執行在任意大的執行緒池中,並有多種執行策略可以選擇: 所有方法都有自己的執行緒,或者每一個測試類一個執行緒等等
      • 測試程式碼是否執行緒安全
    • 嵌入BeanShell可以獲得更大的靈活性
    • 預設使用JDK執行和相關日誌功能,不需要額外增加依賴
    • 應用伺服器測試的依賴方法
    • 分散式測試: 允許在從機上進行分散式測試

TestNG環境配置

  • 配置好主機的Java環境,使用命令 java -version檢視
  • 在TestNG官網,下載TestNG對應系統下的jar檔案
  • 系統環境變數中新增指向jar檔案的路徑
  • 在IDEA中安裝TestNG

TestNG的基本用法

import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.testng.annotations.Test;

public class TestNGLearn1 {

    @BeforeClass
    public void beforeClass() {
        System.out.println("this is before class");
    }

    @Test
    public void TestNgLearn() {
        System.out.println("this is TestNG test case");
    }

    @AfterClass
    public void afterClass() {
        System.out.println("this is after class");
    }
}

TestNG的基本註解

註解 描述
@BeforeSuit 註解方法只執行一次,在此套件中所有測試之前執行
@AfterSuite 註解方法只執行一次,在此套件中所有測試之後執行
@BeforeClass 註解方法只執行一次,在當前類中所有方法呼叫之前執行
@AfterClass 註解方法只執行一次,在當前類中所有方法呼叫之後執行
@BeforeTest 只執行一次,在所有的測試方法執行之前執行
@AfterTest 只執行一次,在所有的測試方法執行之後執行
@BeforeGroups 組的列表,配置方法之前執行.
此方法是保證在執行屬於任何這些組的第一個測試,該方法將被呼叫
@AfterGroups 組的名單,配置方法之後執行.
此方法是保證執行屬於任何這些組的最後一個測試後不久,,該方法將被呼叫
@BeforeMethod 在每一個@test測試方法執行之前執行
比如:在執行完測試用例後要重置資料才能執行第二條測試用例時,可以使用這種註解方式
@AfterMethod 在每一個@test測試方法執行之後執行
@DataProvider 標誌一個方法,提供資料的一個測試方法
註解的方法必須返回一個Object[][],其中每個物件的[]的測試方法的引數列表可以分配
如果有@Test方法,想要使用從這個DataProvider中接收的資料,需要使用一個dataProvider名稱等於這個註解的名稱
@Factory 作為一個工廠,返回TestNG的測試類物件中被用於標記的方法
該方法必須返回Object[]
@Listeners 定義一個測試類的監聽器
@Parameters 定義如何將引數傳遞給@Test方法
@Test 標記一個類或者方法作為測試的一部分

testng.xml

屬性 描述
name 套件suite的名稱,這個名稱會出現在測試報告中
junit 是否以junit模式執行
verbose 設定在控制檯中的輸出方式. 這個設定不影響html版本的測試報告
parallel 是否使用多執行緒進行測試,可以加速測試
configfailurepolicy 是否在執行失敗了一次之後繼續嘗試或者跳過
thread-count 如果設定了parallel,可以設定執行緒數
annotations 如果有javadoc就在javadoc中尋找,沒有就使用jdk5的註釋
time-out 在終止method(parallel="methods")或者test(parallel="tests")之前設定以毫秒為單位的等待時間
skipfailedinvocationcounts 是否跳過失敗的呼叫
data-provider-thread-count 提供一個執行緒池的範圍來使用parallel data
object-factory 用來例項化測試物件的類,繼承自IObjectFactory類
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" >
<suite name="Suite" parallel="tests" thread-count="5">
    <test name="Test" preserve-order="true" verbose="2">
        <parameter name="userName" value="15952031403"></parameter>
        <parameter name="originPwd" value="c12345"></parameter>
        <classes>
            <class name="com.oxford.testng.RegisterTest">
            </class>
        </classes>
    </test>
    <test name="Test1" preserve-order="true">
        <classes>
        <class name="com.oxford.testng.Test2">
            </class>
        </classes>
    </test>
    <test name="Test2" preserve-order="true">
        <classes>
        <class name="com.oxford.testng.Test3">
            </class>
        </classes>
    </test>
</suite>
  • suite中,同時使用parallelthread-count:
    • parallel: 指定並行測試範圍tests,methods,classes
    • thread-count: 並行執行緒數
  • preserve-order: 當設定為true時,節點下的方法按順序執行
  • verbose: 表示記錄日誌的級別,在0 - 10之間取值
  • < parameter name="userName", value="15952031403" > : 給測試程式碼傳遞鍵值對引數,在測試類中通過註解 @Parameter({"userName"}) 獲取

引數化測試

  • 當測試邏輯一樣,只是引數不一樣時,可以採用資料驅動測試機制,避免重複程式碼
  • TestNG通過 @DataProvider實現資料驅動
  • 使用@DataProvider做資料驅動:
    • 資料來源檔案可以是EXCEL,XML,甚至可以是TXT文字
    • 比如讀取xml檔案:
      • 通過@DataProvider讀取XML檔案中的資料
      • 然後測試方法只要標示獲取資料來源的DataProvider
      • 對應的DataProvider就會將讀取的資料傳遞給該test方法
        -
構建XML資料檔案
<?xml version="1.0" encoding="UTF-8"?>
<data>
    <login>
        <username>user1</username>
        <password>123456</password>
    </login>
    <login>
        <username>user2</username>
        <password>345678</password>
    </login>
</data>
讀取XML檔案
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

public class ParseXml {
    /**
     * 利用Dom4j解析xml檔案,返回list
     * @param xmlFileName
     * @return
     */
    public static List parse3Xml(String xmlFileName){
    File inputXml = new File(xmlFileName);    
        List list= new ArrayList();                
        int count = 1;
        SAXReader saxReader = new SAXReader();
        try {
            Document document = saxReader.read(inputXml);
            Element items = document.getRootElement();
            for (Iterator i = items.elementIterator(); i.hasNext();) {
                Element item = (Element) i.next();
                Map map = new HashMap();
                Map tempMap = new HashMap();
                for (Iterator j = item.elementIterator(); j.hasNext();) {
                    Element node = (Element) j.next();                    
                    tempMap.put(node.getName(), node.getText());                    
                }
                map.put(item.getName(), tempMap);
                list.add(map);
            }
        } catch (DocumentException e) {
            System.out.println(e.getMessage());
        }
        System.out.println(list.size());
        return list;
    }    
}
DataProvider類
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.testng.Assert;
import org.testng.annotations.DataProvider;

public class GenerateData {
    public static List list = new ArrayList();
    
    @DataProvider(name = "dataProvider")
    public static Object[][] dataProvider(Method method){      
    list = ParseXml.parse3Xml("absolute path of  xml file");
        List<Map<String, String>> result = new ArrayList<Map<String, String>>();        
        for (int i = 0; i < list.size(); i++) {
            Map m = (Map) list.get(i);    
            if(m.containsKey(method.getName())){                            
                Map<String, String> dm = (Map<String, String>) m.get(method.getName());
                result.add(dm);    
            }
        }  
        if(result.size() > 0){
            Object[][] files = new Object[result.size()][];
            for(int i=0; i<result.size(); i++){
                files[i] = new Object[]{result.get(i)};
            }        
            return files;
        }else {
            Assert.assertTrue(result.size()!=0,list+" is null, can not find"+method.getName() );
        return null;
    }
    }
}
在test方法中引用DataProvider
public class LoginTest {
 @Test(dataProvider="dataProvider", dataProviderClass= GenerateData.class)
    public  void login(Map<String, String> param) throws InterruptedException{

        List<WebElement> edits = findElementsByClassName(AndroidClassName.EDITTEXT);
        edits.get(0).sendkeys(param.get("username"));
        edits.get(1).sendkeys(param.get("password"));
    }
}
  • xml中的父節點與test的方法名對應:
    • xml中同名父節點的個數就意味著該test方法會被重複執行多少次
  • 當DataProvider與test方法不在同一個類時,需要指明DataProvider類:
    • @Test(dataProvider="dataProvider", dataProviderClass= GenerateData.class)

TestNG重寫監聽類

  • TestNG會監聽每個測試用例的執行結果.可以使用監聽定製一些自定義的功能,比如自動截圖,傳送資料給伺服器:
    • 新建一個繼承自TestListenerAdapter的類
    • 重寫完成後,在test方法前新增 @Listener(TestNGListener.class) 註解
package com.oxford.listener;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import org.testng.ITestContext;
import org.testng.ITestResult;
import org.testng.TestListenerAdapter;

import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.unionpay.base.BaseTest;
import com.unionpay.constants.CapabilitiesBean;
import com.unionpay.constants.CaseCountBean;
import com.unionpay.constants.ResultBean;
import com.unionpay.util.Assertion;
import com.unionpay.util.PostService;
import com.unionpay.util.ReadCapabilitiesUtil;

/**
 * 帶有post請求的testng監聽
 * @author lichen2
 */
public class TestNGListenerWithPost extends TestListenerAdapter{
    
    //接收每個case結果的介面
    private String caseUrl;
    
    //接收整個test執行資料的介面
    private String countUrl;
    
    //接收test執行狀態的介面
    private String statusUrl;
    
    private JsonObject caseResultJson = new JsonObject();
    
    private JsonObject caseCountJson = new JsonObject();
    
    private Gson gson = new Gson();
    
    private ResultBean result = new ResultBean();
    
    private CaseCountBean caseCount = new CaseCountBean();
    
    private SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    
    private CapabilitiesBean capabilitiesBean = ReadCapabilitiesUtil.readCapabilities("setting.json");
    
    private String testStartTime;
    
    private String testEndTime;
    
    private String runId;
    
    //testng初始化
    @Override
    public void onStart(ITestContext testContext) {
        super.onStart(testContext);
        String serverUrl = capabilitiesBean.getServerurl();
    caseUrl = "http://"+serverUrl+"/api/testcaseResult";
    countUrl = "http://"+serverUrl+"/api/testcaseCount";
    statusUrl = "http://"+serverUrl+"/api/testStatus";
        runId = capabilitiesBean.getRunid();
        result.setRunId(runId);
        caseCount.setRunId(runId);
    }
    
    //case開始
    @Override
    public void onTestStart(ITestResult tr) {
    Assertion.flag = true;
    Assertion.errors.clear();
    sendStatus("執行中");
    result.setStartTime(format.format(new Date()));
    }
    
    //case成功執行
    @Override
    public void onTestSuccess(ITestResult tr) {
        super.onTestSuccess(tr);
        sendResult(tr);
        takeScreenShot(tr);
    }

    //case執行失敗
    @Override
    public void onTestFailure(ITestResult tr) {
        super.onTestFailure(tr);
        sendResult(tr);
        try {
            takeScreenShot(tr);
        } catch (SecurityException e) {
            e.printStackTrace();
        } catch (IllegalArgumentException e) {         
            e.printStackTrace();
        }
        this.handleAssertion(tr);
    }

    //case被跳過
    @Override
    public void onTestSkipped(ITestResult tr) {
        super.onTestSkipped(tr);
        takeScreenShot(tr);
        sendResult(tr);
        this.handleAssertion(tr);
    }

    //所有case執行完成
    @Override
    public void onFinish(ITestContext testContext) {
        super.onFinish(testContext);
        sendStatus("正在生成報告");
        sendFinishData(testContext);
    }
    
    /**
     * 傳送case測試結果
     * @param tr
     */
    public void sendResult(ITestResult tr){
    result.setTestcaseName(tr.getName());
    result.setEndTime(format.format(new Date()));
    float tmpDuration = (float)(tr.getEndMillis() - tr.getStartMillis());
    result.setDuration(tmpDuration / 1000);
    
    switch (tr.getStatus()) {
    case 1:
        result.setTestResult("SUCCESS");
        break;
    case 2:
        result.setTestResult("FAILURE");
        break;
    case 3:
        result.setTestResult("SKIP");
        break;
    case 4:
        result.setTestResult("SUCCESS_PERCENTAGE_FAILURE");
        break;
    case 16:
        result.setTestResult("STARTED");
        break;
    default:
        break;
    }
    caseResultJson.addProperty("result", gson.toJson(result));
    PostService.sendPost(caseUrl, caseResultJson.toString());
    }
    
    /**
     * 通知test完成
     * @param testContext
     */
    public void sendFinishData(ITestContext tc){
    testStartTime = format.format(tc.getStartDate());
    testEndTime = format.format(tc.getEndDate());
    long duration = getDurationByDate(tc.getStartDate(), tc.getEndDate());
    caseCount.setTestStartTime(testStartTime);
    caseCount.setTestEndTime(testEndTime);
    caseCount.setTestDuration(duration);
    caseCount.setTestSuccess(tc.getPassedTests().size());
    caseCount.setTestFail(tc.getFailedTests().size());
    caseCount.setTestSkip(tc.getSkippedTests().size());
    
    caseCountJson.addProperty("count", gson.toJson(caseCount));
    PostService.sendPost(countUrl, caseCountJson.toString());
    }
    
    /**
     * 通知test執行狀態
     */
    public void sendStatus(String status){
    JsonObject jsonObject = new JsonObject();
    jsonObject.addProperty("runId", runId);
    jsonObject.addProperty("status", status);
    JsonObject sendJson = new JsonObject();
    sendJson.addProperty("status", jsonObject.toString());
    PostService.sendPost(statusUrl, sendJson.toString());
    }
    
    //計算date間的時差(s)
    public long getDurationByDate(Date start, Date end){
    long duration = end.getTime() - start.getTime();
    return duration / 1000;
    }

    //截圖
    private void takeScreenShot(ITestResult tr) {
        BaseTest b = (BaseTest) tr.getInstance();
        b.takeScreenShot(tr);
    }
}
  • 執行測試
package com.oxford.base;

import org.testng.ITestResult;
import com.unionpay.listener.TestNGListenerWithPost;
@Listeners(TestNGListenerWithPost.class)
public abstract class BaseTest {
    public AndroidDriver<WebElement> driver;
    public BaseTest() {
    driver = DriverFactory.getDriverByJson();
    }

    /**
     * 截圖並儲存到本地
     * @param tr
     */
    public void takeScreenShot(ITestResult tr) {
    String fileName = tr.getName() + ".jpg";
    File dir = new File("target/snapshot");
    if (!dir.exists()) {
        dir.mkdirs();
    }
    String filePath = dir.getAbsolutePath() + "/" + fileName;
    if (driver != null) {
        try {
        File scrFile = driver.getScreenshotAs(OutputType.FILE);
        FileUtils.copyFile(scrFile, new File(filePath));
        } catch (IOException e) {
        e.printStackTrace();
        }
    }
    }
}

相關文章