走進Java介面測試之從0到1搭建數據驅動框架(多數據源和業務持久層)

  • 2019 年 12 月 11 日
  • 筆記

前言

在前三篇文章中,我們分別介紹了需求、設計、以及測試管理的實現功能,本篇我們一起來實現多數據源和業務持久層開發。

走進Java介面測試之從0到1搭建數據驅動框架(需求篇)

走進Java介面測試之從0到1搭建數據驅動框架(設計篇)

走進Java介面測試之從0到1搭建數據驅動框架(用例管理)

全部程式碼骨架結構

├─logs  │  └─spring-boot-logback             # 日誌文件  │          all_api-test-logback.log # 所有日誌  │          err_api-test-logback.log # 錯誤日誌  ├─src  │  ├─main  │  │  ├─java  │  │  │  └─com  │  │  │      └─zuozewei  │  │  │          └─springbootdatadrivendemo  │  │  │              │  SpringbootDataDrivenDemoApplication.java # 啟動類  │  │  │              │  │  │  │              ├─db  │  │  │              │  ├─auto      # 存放MyBatis Generator生成器生成的數據層程式碼,可以隨時刪除再生成  │  │  │              │  │  ├─mapper # DAO 介面  │  │  │              │  │  └─model  # Entity 實體  │  │  │              │  └─manual    # 存放自定義的數據層程式碼,包括對MyBatis Generator自動生成程式碼的擴展  │  │  │              │      ├─mapper # DAO 介面  │  │  │              │      └─model  # Entity 實體  │  │  │              ├─handler  # 數據轉換  │  │  │              └─service # 業務邏輯  │  │  │                  └─impl # 實現類  │  │  │  │  │  └─resources  │  │      │  application.yml       # 全局配置文件  │  │      │  generatorConfig.xml # Mybatis Generator 配置文件  │  │      │  logback-spring.xml     # logback 配置文件  │  │      │  spy.properties      # P6Spy 配置文件  │  │      │  │  │      ├─db  │  │      ├─mapper  │  │      │  └─com  │  │      │      └─zuozewei  │  │      │          └─springbootdatadrivendemo  │  │      │              └─db  │  │      │                  ├─auto      # 存放MyBatis Generator生成器生成的數據層程式碼,可以隨時刪除再生成  │  │      │                  │  └─mapper # 資料庫 Mapping 文件  │  │      │                  │  │  │      │                  └─manual    # 存放自定義的數據層程式碼,包括對MyBatis Generator自動生成程式碼的擴展  │  │      │                      └─mapper # 資料庫 Mapping 文件  │  │      └─testng  │  │          │  APICollection-TestSuite.xml # 所用測試用例集  │  │          └─jdbcbapi  │  │                  jdbcAPI-TestSuite.xml  # 某API測試用例集  │  │  │  └─test  │      └─java  │          └─com  │              └─zuozewei  │                  └─springbootdatadrivendemo  │                      └─demo   # 介面測試用例  ├─pom.xml

多數據源

上面介紹了我們的用例管理使用的是 MySQL 資料庫,而本文我們演示的是業務資料庫使用的是 H2,那麼必然的我們需要面對處理多數據源的問題。而在需求篇我們分析過了傳統方式,確實可用,不足在於需要根據不同數據源建立不同的 package,一旦數據源發生變更,需要更改所在的 package。也看過了動態數據源,不能滿足我們多數據源的需求。

而經過查找,我們發現一個開源的項目,即 dynamic-datasource-spring-boot-starter 可以滿足我們的需求。

工具簡介

dynamic-datasource-spring-boot-starter 是一個基於 springboot 的快速集成多數據源的啟動器。其支援 Jdk 1.7+, SpringBoot 1.4.x 1.5.x 2.0.x。 特性:

  • 數據源分組,適用於多種場景 純粹多庫 讀寫分離 一主多從 混合模式。
  • 內置敏感參數加密和啟動初始化表結構 schema 資料庫 database。
  • 提供對 Druid,Mybatis-Plus,P6sy,Jndi 的快速集成。
  • 簡化 Druid 和 HikariCp 配置,提供全局參數配置。
  • 提供自定義數據源來源介面(默認使用 yml 或 properties 配置)。
  • 提供項目啟動後增減數據源方案。
  • 提供Mybatis環境下的 純讀寫分離 方案。
  • 使用 spel 動態參數解析數據源,如從 session,header 或參數中獲取數據源。(多租戶架構神器)
  • 提供多層數據源嵌套切換。(ServiceA >>> ServiceB >>> ServiceC,每個 Service 都是不同的數據源)
  • 提供 不使用註解 而 使用 正則 或 spel 來切換數據源方案(實驗性功能)。

約定:

  • 本框架只做 切換數據源 這件核心的事情,並不限制你的具體操作,切換了數據源可以做任何CRUD。
  • 配置文件所有以下劃線 _ 分割的數據源 首部 即為組的名稱,相同組名稱的數據源會放在一個組下。
  • 切換數據源可以是組名,也可以是具體數據源名稱。組名則切換時採用負載均衡演算法切換。
  • 默認的數據源名稱為 master ,你可以通過 spring.datasource.dynamic.primary 修改。
  • 使用 @DS 切換數據源。@DS 可以註解在方法上和類上,同時存在方法註解優先於類上註解。

常見示例:

# 多主多從                      純粹多庫(記得設置primary)                   混合配置  spring:                               spring:                               spring:    datasource:                           datasource:                           datasource:      dynamic:                              dynamic:                              dynamic:        datasource:                           datasource:                           datasource:          master_1:                             mysql:                                master:          master_2:                             oracle:                               slave_1:          slave_1:                              sqlserver:                            slave_2:          slave_2:                              postgresql:                           oracle_1:          slave_3:                              h2:                                   oracle_2:

我們這裡使用需要注意點有:

  • 與 Druid,P6sy 快速集成;
  • 對 Druid 進行全局參數配置;
  • 只使用 切換數據源 這件核心的事情,並不涉及具體其他操作,切換了數據源可以做任何 CRUD;
  • 設置默認的數據源;
  • 使用 @DS 切換數據源。@DS 同時註解在方法上和類上。

配置數據源

引包:

<!--快速集成多數據源的啟動器-->  <dependency>      <groupId>com.baomidou</groupId>      <artifactId>dynamic-datasource-spring-boot-starter</artifactId>      <version>2.5.6</version>  </dependency>

配置 application.yml 文件:

spring:    datasource:      dynamic:        primary: mysql    # 設置默認的數據源或者數據源組,默認值即為 master        strict: false    # 設置嚴格模式,默認 false 不啟動. 啟動後在未匹配到指定數據源時候回拋出異常,不啟動會使用默認數據源.        datasource:          mysql_1:     # 測試用例庫            username: zuozewei            password: zuozewei            url: jdbc:mysql://172.16.106.188:3306/autotest?useUnicode=true&characterEncoding=UTF-8&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC&useSSL=false            driver-class-name: com.mysql.cj.jdbc.Driver            continue-on-error: true   # 默認true,初始化失敗是否繼續            separator: ";"             # sql 默認分號分隔符          mysql_2:   # 業務資料庫            username: zuozewei            password: zuozewei            url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC&useSSL=false            driver-class-name: com.mysql.cj.jdbc.Driver            continue-on-error: true   # 默認true,初始化失敗是否繼續            separator: ";"             # sql 默認分號分隔符          h2:       # 業務資料庫            username: sa            password: ""            url: jdbc:h2:mem:test            driver-class-name: org.h2.Driver            schema: db/schema.sql       # 配置則生效,自動初始化表結構            data: db/data.sql           # 配置則生效,自動初始化數據            continue-on-error: true    # 默認true,初始化失敗是否繼續            separator: ";"             # sql 默認分號分隔符            druid:                      # 這裡可以重寫默認值              initial-size: 5              public-key: MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKUVA/IL/iON8f63bv2i/pIAK+1sXY228slLkTKrI9axwBMIoPV7+PqdRTv6uqMl3j6nei0EDBWEu/Wp/qOQ/ScCAwEAAQ==

使用 @DS 切換數據源。建議只註解在 service 實現類上。比如我們指定取用例 service 方法的數據源:

**   * 描述: 參數化自定義查詢實現類   *   * @author zuozewei   * @create 2019-11-21 16:04   */    @Service  public class TestDataServiceImpl implements TestDataService {        @Resource      private TestDataMapper testDataMapper;        @DS("mysql_1")      @Override      public List<LinkedHashMap<String, Object>> selectBysql(String sql) {          return testDataMapper.selectBysql(sql);      }    }

啟動工程

啟動的時候,我們可以看到各數據源已初始化成功。

業務持久層

業務持久層的框架,我們依舊選擇統一使用 Mybatis。在使用 Mybatis 的時候,Dao 介面,Entity 實體類,還有每個實體類對應的 xml 都得自己寫,這其實也是工作量很大的事情,維護起來也很費勁,使用我們這裡選用的是mybatis-generatormybatis-generator-gui 來快速生成 Mybatis 的Java POJO文件及資料庫 Mapping 文件。

初始化數據

這裡我們演示的業務資料庫是 H2,資料庫的表結構腳本 schema.sql:

drop table t_coffee if exists;  create table t_coffee (      id bigint not null auto_increment,      name varchar(255),      price bigint not null,      create_time timestamp,      update_time timestamp,      primary key (id)  );

初始化數據腳本 data.sql:

insert into t_coffee (name, price, create_time, update_time) values ('espresso', 2000, now(), now());  insert into t_coffee (name, price, create_time, update_time) values ('latte', 2500, now(), now());  insert into t_coffee (name, price, create_time, update_time) values ('capuccino', 2500, now(), now());  insert into t_coffee (name, price, create_time, update_time) values ('mocha', 3000, now(), now());  insert into t_coffee (name, price, create_time, update_time) values ('macchiato', 3000, now(), now());

這兩個SQL文件都放到 srcmainresourcesdb 下。

處理自定義類型

這裡的 price 我們擴展了自定義類型,所以我們需要使用 TypeHandler 解決自定義類型預處理。因為 price 是joda-money 類型,資料庫中卻是 bigint 類型。MyBatis 為我們提供的方法即是 TypeHandler 來應對 Java 和 jdbc 欄位類型不匹配的情況。MyBatis 中內置了不少的TypeHandler,如果我們想要自己自定義一個 TypeHandler 可以實現 TypeHandler 介面,也可以繼承 BaseTypeHandler 類。下面我們實現一個將 Java 中的 joda-money 類型利用我們自定義的 MoneyTypeHandler 來轉換為 JDBC 的 bigint 類型。

引包:

!--money類型-->  <dependency>      <groupId>org.joda</groupId>      <artifactId>joda-money</artifactId>      <version>LATEST</version>  </dependency>

新建一個 handler package,編寫 MoneyTypeHandler.java

/**   * 在 Money 與 Long 之間轉換的 TypeHandler,處理 CNY 人民幣   */    public class MoneyTypeHandler extends BaseTypeHandler<Money> {        /**       *  設置非空參數       * @param ps       * @param i       * @param parameter       * @param jdbcType       * @throws SQLException       */      @Override      public void setNonNullParameter(PreparedStatement ps, int i, Money parameter, JdbcType jdbcType) throws SQLException {          ps.setLong(i, parameter.getAmountMinorLong());      }        /**       * 根據列名,獲取可以為空的結果       * @param rs       * @param columnName       * @return       * @throws SQLException       */      @Override      public Money getNullableResult(ResultSet rs, String columnName) throws SQLException {          return parseMoney(rs.getLong(columnName));      }        /**       * 根據列索引,獲取可以為空的結果       * @param rs       * @param columnIndex       * @return       * @throws SQLException       */      @Override      public Money getNullableResult(ResultSet rs, int columnIndex) throws SQLException {          return parseMoney(rs.getLong(columnIndex));      }        /**       *       * @param cs       * @param columnIndex       * @return       * @throws SQLException       */      @Override      public Money getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {          return parseMoney(cs.getLong(columnIndex));      }        /**       *  處理 CNY 人民幣       * @param value       * @return       */      private Money parseMoney(Long value) {          return Money.of(CurrencyUnit.of("CNY"), value / 100.0);      }  }

使用 mybatis-generator

MyBatis Generator是 MyBatis 的程式碼生成器,支援為 MyBatis 的所有版本生成程式碼。非常容易及快速生成 Mybatis 的Java POJO文件及資料庫 Mapping 文件。

引包:

<!--mybatis-generator生成器-->  <dependency>      <groupId>org.mybatis.generator</groupId>      <artifactId>mybatis-generator-core</artifactId>      <version>1.3.7</version>  </dependency>

配置 generatorConfig.xml

<?xml version="1.0" encoding="UTF-8"?>  <!DOCTYPE generatorConfiguration          PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"          "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">    <generatorConfiguration>      <context id="H2Tables" targetRuntime="MyBatis3">          <!--支援流式 fluent 方法-->          <plugin type="org.mybatis.generator.plugins.FluentBuilderMethodsPlugin" />          <!-- 自動生成toString方法 -->          <plugin type="org.mybatis.generator.plugins.ToStringPlugin" />          <!-- 自動生成hashcode方法 -->          <plugin type="org.mybatis.generator.plugins.SerializablePlugin" />          <!-- 分頁插件 -->          <plugin type="org.mybatis.generator.plugins.RowBoundsPlugin" />            <!--資料庫連接資訊-->          <jdbcConnection driverClass="org.h2.Driver"                          connectionURL="jdbc:h2:mem:test"                          userId="sa"                          password="">          </jdbcConnection>            <!--模型生成器、Mapper生成器-->          <javaModelGenerator targetPackage="com.zuozewei.springbootdatadrivendemo.db.auto.model"                              targetProject="./src/main/java">              <property name="enableSubPackages" value="true" />              <property name="trimStrings" value="true" />          </javaModelGenerator>          <sqlMapGenerator targetPackage="com.zuozewei.springbootdatadrivendemo.db.auto.mapper"                           targetProject="./src/main/resources/mapper">              <property name="enableSubPackages" value="true" />          </sqlMapGenerator>          <javaClientGenerator type="MIXEDMAPPER"                               targetPackage="com.zuozewei.springbootdatadrivendemo.db.auto.mapper"                               targetProject="./src/main/java">              <property name="enableSubPackages" value="true" />          </javaClientGenerator>            <!--表映射-->          <table tableName="t_coffee" domainObjectName="Coffee" >              <generatedKey column="id" sqlStatement="CALL IDENTITY()" identity="true" />              <columnOverride column="price" javaType="org.joda.money.Money" jdbcType="BIGINT"                              typeHandler="com.zuozewei.springbootdatadrivendemo.handler.MoneyTypeHandler"/>          </table>      </context>  </generatorConfiguration>

注意:

  • id 是自增的;
  • price 欄位需要映射到 MoneyTypeHandler。

啟動方法

在工程啟動類編寫一個調用方法:

@SpringBootApplication(exclude = DruidDataSourceAutoConfigure.class)  @Slf4j  @MapperScan("com.zuozewei.springbootdatadrivendemo.db")  public class SpringbootDataDrivenDemoApplication  implements ApplicationRunner {        public static void main(String[] args) {          SpringApplication.run(SpringbootDataDrivenDemoApplication.class, args);          log.info("程式啟動!");      }        @Override      public void run(ApplicationArguments args) throws Exception {          generateArtifacts();          log.info("啟動generateArtifacts");      }        /**       * 執行MyBatisGenerator       * @throws Exception       */      private void generateArtifacts() throws Exception {          List<String> warnings = new ArrayList<>();          ConfigurationParser cp = new ConfigurationParser(warnings);          Configuration config = cp.parseConfiguration(                  this.getClass().getResourceAsStream("/generatorConfig.xml"));          DefaultShellCallback callback = new DefaultShellCallback(true);          MyBatisGenerator myBatisGenerator = new MyBatisGenerator(config, callback, warnings);          myBatisGenerator.generate(null);      }    }

啟動工程:

檢查配置文件指定路徑是否生成文件:

使用 mybatis-generator-gui

還有一種簡易的方法生成 Mybatis 的 Java POJO 文件及資料庫 Mapping 文件,即使用 mybatis-generator-gui 工具。

簡介

mybatis-generator-gui 是基於 mybatis generator 開發一款介面工具, 本工具可以使你非常容易及快速生成 Mybatis 的 Java POJO 文件及資料庫 Mapping 文件。 核心特性:

  • 按照介面步驟輕鬆生成程式碼,省去 XML 繁瑣的學習與配置過程
  • 保存資料庫連接與 Generator 配置,每次程式碼生成輕鬆搞定
  • 內置常用插件,比如分頁插件
  • 支援 OverSSH 方式,通過 SSH 隧道連接至公司內網訪問資料庫
  • 把資料庫中表列的注釋生成為 Java 實體的注釋,生成的實體清晰明了
  • 可選的去除掉對版本管理不友好的注釋,這樣新增或刪除欄位重新生成的文件比較過來清楚
  • 目前已經支援 Mysql、Mysql8、Oracle、PostgreSQL 與 SQL Server,暫不對其他非主流資料庫提供支援。(MySQL 支援的比較好)

要求:由於使用了 Java 8 的眾多特性,所以要求 JDK 1.8.0.60 以上版本,另外 JDK 1.9 暫時還不支援。

git clone https://github.com/zouzg/mybatis-generator-gui  cd mybatis-generator-gui  mvn jfx:jar  cd target/jfx/app/  java -jar mybatis-generator-gui.jar

使用示例

由於目前不支援 H2 資料庫,故使用示意圖代替:

實現Service方法

在 service package 下新建 Service 介面 CoffeeService.java

/**   * 描述: coffee Service   *   * @author zuozewei   * @create 2019-11-21 18:00   */    public interface CoffeeService {      // 插入    int addCoffee(Coffee coffee);      // 查詢    List selectCoffeeFromDs(CoffeeExample coffeeExample) throws InterruptedException;    }

實現 CoffeeService 介面,並使用註解指定 h2 數據源,新建 CoffeeServiceImpl.java

/**   * 描述: CoffeeService 實現類   *   * @author zuozewei   * @create 2019-11-21 18:00   */    @Service  @DS("h2")  public class CoffeeServiceImpl implements CoffeeService {          @Resource        private CoffeeMapper coffeeMapper;          @Override        public int addCoffee(Coffee coffee) {          return coffeeMapper.insert(coffee);        }          @Override        public List selectCoffeeFromDs(CoffeeExample coffeeExample) throws InterruptedException {          return coffeeMapper.selectByExample(coffeeExample);        }        }

配置mybatis

application.yml 中配置 mybatis

mybatis:    type-aliases-package: com.zuozewei.db # 自動掃描實體類所在的包    type-handlers-package: com.zuozewei.handler # 指定 TypeHandler 所在的包    configuration:      map-underscore-to-camel-case: true # 開啟駝峰功能      call-setters-on-nulls: true     # 調用setter null,返回空也必須設置到bean中  (直接執行sql專用)    mapper-locations: classpath*:/mapper/**/*.xml # 掃描類路徑下所有以xml文件結尾的文件

工程結構

最後,多數據源和業務持久層的工程結構大概是以下的樣子:

小結

在今天這篇文章中,主要和大家分享了實現註解式多數據源和業務持久層開發的過程。在實現過程中,你最需要關注的幾部分內容是:

  • 基於 MybatisGenerator 或者 MybatisGeneratorGUI 自動化生成持久層程式碼,節省大量重複開發工作;
  • 使用 TypeHandler 解決自定義類型預處理;
  • 實現註解式動態數據源,滿足 N 個業務數據源測試需求。

希望對你能有所啟發。