走進Java介面測試之從0到1搭建數據驅動框架(多數據源和業務持久層)
- 2019 年 12 月 11 日
- 筆記

前言
在前三篇文章中,我們分別介紹了需求、設計、以及測試管理的實現功能,本篇我們一起來實現多數據源和業務持久層開發。
全部程式碼骨架結構
├─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-generator 或 mybatis-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 個業務數據源測試需求。
希望對你能有所啟發。


