Mybatis之攔截器--獲取執行SQL實現多客戶端數據同步

0. 前言

最近的一個項目是將J2EE環境打包安裝在客戶端(使用nwjs+NSIS製做安裝包)運行, 全部的業務操做在客戶端完成, 數據存儲在客戶端數據庫中. 服務器端數據庫彙總各客戶端的數據進行分析. 其中客戶端ORM使用Mybatis. 經過Mybatis攔截器獲取全部在執行的SQL語句, 按期同步至服務器.mysql

本文經過在客戶端攔截SQL的操做介紹Mybatis攔截器的使用方法.git

1. 項目需求

客戶分店較多且比較分散, 部分店內網絡不穩定, 客戶要求每一個分店在無網絡的狀況下也能正常使用系統, 同時全部店面數據須要進行彙總分析. 綜合客戶的需求, 項目架構以下:github

將WEB項目及其運行環境經過NSIS製做安裝包在各分店進行安裝, 每一個分店是一個獨立的WEB服務, 這樣就保證店內在無網絡(有局域網,沒法訪問互聯網)的狀況下也能夠正常使用系統. 此時每一個分店的數據庫保存本身店內的運營數據, 各店之間的數據相互隔離.sql

但運營方沒法分析全部店面的彙總數據(如商品總體銷售狀況等), 所以須要將每一個店面的數據按期同步至服務器的數據庫中.數據庫

  • 因爲店內可能無網絡(無網時不能受數據同步影響,系統需正常運行), 實時同步方案被排除.
  • 爲保證數據庫安全性, 服務器數據庫不能對外暴露, 使用數據庫的同步機制方案被排除.
  • 部分業務須要記錄數據變化日誌(數據從1到0又到1, 需記錄過程), 增量同步方案被排除.

最終採用了將客戶端全部更新(增,刪,改)的SQL按照執行順序保存至數據庫中, 按期同步並在服務器的數據庫按照順序執行SQL, 以此來保證服務器數據庫的數據是各客戶端數據的彙總.apache

2. 解決方案

項目採用Mybatis, Mapper中定義SQL時可使用Mybatis的標籤及參數標識符, Mybatis會解析標籤替換參數生成最終的SQL在數據庫中執行, 而咱們須要的是最終在數據庫中執行的SQL.數組

Mybatis中SQL的寫法:安全

<insert id="insert">
    INSERT INTO atd681_mybatis_test ( dv ) VALUES ( #{dv} )
</insert>
複製代碼

須要同步至服務器執行的SQL:bash

INSERT INTO atd681_mybatis_test ( dv ) VALUES ( 'aaa' )
複製代碼

3. 攔截器

3.1 什麼是攔截器

想這樣一個場景, 你作飯的時候可能須要如下步驟:服務器

買菜 >> 洗菜 >> 切菜 >> 作菜 >> 上菜 >> 洗碗

  • 開始洗菜前, 買菜操做已經完成, 能夠知道買了什麼菜.
  • 洗菜時還未開始作菜, 所以不知道菜是什麼口味的.
  • 在上菜前(此時作菜已經完成), 能夠知道菜的口味.
  • 在上菜時不知道有沒有剩菜
  • 在洗碗時咱們能夠知道有沒有剩菜.

上面的作飯流程是按照步驟一步一步的進行, 咱們既能夠在其中的某個步驟中獲取前幾步的成果, 也能夠在某個步驟開始以前作些額外的事情, 好比: 切菜前對菜稱重等.

Mybatis提供了這樣一個組件: 他能夠在某個步驟執行以前先執行自定義的操做. 這個組件叫作攔截器. 所謂攔截器, 顧名思義: 須要定義攔截哪一個操做步驟及攔截後作什麼事情.

3.2 定義攔截器

攔截器須要實現org.apache.ibatis.plugin.Interceptor接口並指定攔截的方法.

// 攔截器
@Intercepts(@Signature(type = StatementHandler.class, 
                       method = "update", 
                       args = Statement.class)
            )
public class SQLInterceptor implements Interceptor {

    // 攔截方法後執行的邏輯
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 繼續執行Mybatis原有的邏輯
        // proceed中經過反射執行被攔截的方法
        return invocation.proceed();
    }

    // 返回當前攔截的對象(StatementHandler)的動態代理
    // 當攔截對象的方法被執行時, 動態代理中執行攔截器intercept方法.
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    // 設置屬性
    @Override
    public void setProperties(Properties properties) {
    }

}

複製代碼
  • @Intercepts爲Mybatis提供的攔截器註解, @Signature指定攔截的方法.
  • 若是一個攔截器攔截多個方法時, 在@Intercepts中配置多個@Signature(數組)便可.
  • 因爲JAVA的方法能夠重載, 肯定惟一方法須要指定類(type), 方法(method), 參數(args).
  • 攔截器可攔截Executor,ParameterHandler,ResultSetHandler,StatementHandler下的方法.

3.3 配置攔截器

在Spring配置文件中, 聲明攔截器並將其配置到SqlSessionFactoryBeanplugins屬性中

// Mybatis攔截器
sqlInterceptor(SQLInterceptor)

// Mybatis配置
sqlSessionFactory(SqlSessionFactoryBean) {
    dataSource = ref("dataSource")
    mapperLocations = "classpath*:/com/atd681/mybatis/interceptor/*_mapper.xml"
    
    // 配置Mybatis攔截器
    plugins = [
        sqlInterceptor
    ] 
}
複製代碼

4. 獲取並保存SQL

Mybatis處理SQL的大體流程以下:

加載SQL >> 解析SQL >> 替換SQL參數 >> 執行SQL >> 獲取返回結果

攔截[執行SQL]操做, 此時Mybatis已經完成SQL解析及替換參數, 所得的SQL即爲發送數據庫執行的SQL. 咱們只須要獲取該SQL並保存至數據庫便可.

// Mybatis攔截器:攔截全部的增刪改SQL,將SQL保持至數據庫
// 攔截StatementHandler.update方法
@Intercepts(@Signature(type = StatementHandler.class, 
                       method = "update", 
                       args = Statement.class)
           )
public class SQLInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {

        // invocation.getArgs()能夠獲取到被攔截方法的參數
        // StatementHandler.update(Statement s)的參數爲Statement
        Statement s = (Statement) invocation.getArgs()[0];

        // 數據源爲DRUID, Statement爲DRUID的Statement
        Statement stmt = ((DruidPooledPreparedStatement) s).getStatement();

        // 配置druid鏈接時使用filters: stat配置
        if (stmt instanceof PreparedStatementProxyImpl) {
            stmt = ((PreparedStatementProxyImpl) stmt).getRawObject();
        }

        // 數據庫提供的Statement可獲取參數替換後的SQL(JDBC和DRUID獲取的是帶?的)
        // 數據庫爲MySQL,能夠直接強制轉換爲MySQL的PreparedStatement獲取SQL
        // SQL在書寫時爲了格式容器閱讀會有換行符(多個空格)存在
        // 爲了保存和查看方便去除SQL中的換行及多個空格
        String sql = ((com.mysql.jdbc.PreparedStatement) stmt).asSql().replaceAll("\\s+", " ");

        // 保存SQL的操做必須和當前執行的SQL在同一事務中
        // 使用當前SQL所在的數據庫鏈接執行保存操做便可
        // 目標sql成功時保存sql的方法也同步成功
        Connection conn = stmt.getConnection();

        // 將SQL保存至數據庫中
        PreparedStatement ps = null;

        try {
            ps = conn.prepareStatement("INSERT INTO atd681_mybatis_sql (v_sql) VALUES (?)");
            ps.setString(1, sql);

            // 由於和Mybatis的操做在同一事務中
            // 若是本次操做若是失敗, 全部操做都回滾
            ps.execute();
        }
        finally {
            if (ps != null) {
                ps.close();
            }
        }

        // 繼續執行StatementHandler.update方法
        return invocation.proceed();

    }

}

複製代碼
  • 只有MySQL提供的PreparedStatement對象中能夠獲取到最終的SQL.
  • 保存SQL操做須要和Mybatis的操做在同一事務中, 必須同時成功或失敗.

5. 測試

在數據庫中建立兩張表:

  • atd681_mybatis_test: 存儲業務測試數據
  • atd681_mybatis_sql: 存儲業務操做的SQL

建立DAOMapper, 建立增長, 刪除, 修改的方法及SQL

// 數據DAO
@Repository
public interface DataDAO {

    // 添加數據
    void insert(String dv);

    // 更新數據
    void update(String dv);

    // 刪除數據
    void delete();

}
複製代碼
<mapper namespace="com.atd681.mybatis.interceptor.DataDAO">
	
	<!-- 添加數據,內容爲參數i的值 -->
	<insert id="insert">
		INSERT INTO atd681_mybatis_test ( dv ) VALUES ( #{dv} )
	</insert>
	
	<!-- 更新數據,更新爲參數u的值 -->
	<update id="update">
		UPDATE atd681_mybatis_test1 SET dv = #{dv}
	</update>
	
	<!-- 刪除數據 -->
	<delete id="delete">
		DELETE FROM atd681_mybatis_test
	</delete>
	
</mapper> 
複製代碼

控制器中添加方法, 依次調用刪除, 添加, 更新. 保證三個操做在同一個事務中.

@RestController
public class DataController {

    // 注入DAO
    @Autowired
    private DataDAO dao;

    // 分別執行刪除,插入,更新操做
    // 參數i: 插入時的字符串
    // 參數u: 更新時的字符串
    @GetMapping("/mybatis/test")
    @Transactional
    public String excuteSql(String i, String u) {

        // 刪除數據後將參數i的內容插件數據庫,將數據更新成參數u的內容
        // 該方法添加了事務,3次數據庫操做會在同一個事務中執行.
        // Mybatis攔截器會捕獲三次數據庫SQL插入至數據庫中(詳見攔截器)
        dao.delete();
        dao.insert(i);
        dao.update(u);

        return "success";
    }

}
複製代碼

啓動服務, 訪問http://localhost:3456/mybatis/test?i=insert&u=update

程序依次執行刪除、添加(內容爲"insert")、更新(內容爲"update")三個操做, 執行完成後數據庫中有一條記錄(內容爲"update"). 因爲配置了攔截器, 在每一個操做執行前將SQL保持至數據庫中, 所以三條SQL也被保存至數據庫中.

上述過程當中除了3次業務操做, 還有3次保持SQL的操做, 所以數據庫總共會執行6條SQL.

  1. 執行DELETE操做
  2. 保存1中DELETE操做的SQL
  3. 執行INSERT SQL
  4. 保存3中INSERT操做的SQL
  5. 執行UPDATE SQL
  6. 保存5中UPDATE操做的SQL

上述6次數據庫操做必須在同一事務中, 不然一旦出現業務操做成功但保存SQL失敗的狀況. 服務器端同步的數據就會與客戶端本地不一致.

6. 示例代碼

雖然Mybatis沒有Spring的擴展性強, 可是攔截器的出現也能夠幫助咱們解決一些常見問題. 經過攔截器能夠實現分頁, 統一設置參數等常見的功能.

  • 示例代碼地址: https://github.com/atd681/alldemo
  • 示例項目名稱: atd681-mybatis-interceptor
相關文章
相關標籤/搜索