最近的一個項目是將J2EE環境打包安裝在客戶端(使用 nwjs + NSIS 製做安裝包)運行, 全部的業務操做在客戶端完成, 數據存儲在客戶端數據庫中. 服務器端數據庫彙總各客戶端的數據進行分析. 其中客戶端ORM使用Mybatis. 經過Mybatis攔截器獲取全部在執行的SQL語句, 按期同步至服務器.mysql
本文經過在客戶端攔截SQL的操做介紹Mybatis攔截器的使用方法.sql
客戶分店較多且比較分散, 部分店內網絡不穩定, 客戶要求每一個分店在無網絡的狀況下也能正常使用系統, 同時全部店面數據須要進行彙總分析. 綜合客戶的需求, 項目架構以下:數據庫
將WEB項目及其運行環境經過NSIS製做安裝包在各分店進行安裝, 每一個分店是一個獨立的WEB服務, 這樣就保證店內在無網絡(有局域網,沒法訪問互聯網)的狀況下也能夠正常使用系統. 此時每一個分店的數據庫保存本身店內的運營數據, 各店之間的數據相互隔離.apache
但運營方沒法分析全部店面的彙總數據(如商品總體銷售狀況等), 所以須要將每一個店面的數據按期同步至服務器的數據庫中.數組
因爲店內可能無網絡(無網時不能受數據同步影響,系統需正常運行), 實時同步方案被排除. 爲保證數據庫安全性, 服務器數據庫不能對外暴露, 使用數據庫的同步機制方案被排除. 部分業務須要記錄數據變化日誌(數據從1到0又到1, 需記錄過程), 增量同步方案被排除. 最終採用了將客戶端全部更新(增,刪,改)的SQL按照執行順序保存至數據庫中, 按期同步並在服務器的數據庫按照順序執行SQL, 以此來保證服務器數據庫的數據是各客戶端數據的彙總.安全
項目採用Mybatis, Mapper 中定義SQL時可使用Mybatis的標籤及參數標識符, Mybatis會解析標籤替換參數生成最終的SQL在數據庫中執行, 而咱們須要的是最終在數據庫中執行的SQL.服務器
Mybatis中SQL的寫法:網絡
INSERT INTO atd681_mybatis_test ( dv ) VALUES ( #{dv} ) 複製代碼 須要同步至服務器執行的SQL:INSERT INTO atd681_mybatis_test ( dv ) VALUES ( 'aaa' ) 複製代碼 3. 攔截器mybatis
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配置文件中, 聲明攔截器並將其配置到 SqlSessionFactoryBean 中 plugins 屬性中
// 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 建立 DAO 和 Mapper , 建立增長, 刪除, 修改的方法及SQL
// 數據DAO @Repository public interface DataDAO {
// 添加數據
void insert(String dv);
// 更新數據
void update(String dv);
// 刪除數據
void delete();
複製代碼
} 複製代碼
<!-- 添加數據,內容爲參數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>
複製代碼
複製代碼 控制器中添加方法, 依次調用刪除, 添加, 更新. 保證三個操做在同一個事務中.
@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.
執行DELETE操做 保存1中DELETE操做的SQL 執行INSERT SQL 保存3中INSERT操做的SQL 執行UPDATE SQL 保存5中UPDATE操做的SQL 上述6次數據庫操做必須在同一事務中, 不然一旦出現業務操做成功但保存SQL失敗的狀況. 服務器端同步的數據就會與客戶端本地不一致.