微服務項目後臺技術棧

微服務項目後臺相關技術整理

主要技術

  • 基礎框架: 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();
        }
    }
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章