最近的一個項目是將J2EE環境打包安裝在客戶端(使用nwjs
+NSIS
製做安裝包)運行, 全部的業務操做在客戶端完成, 數據存儲在客戶端數據庫中. 服務器端數據庫彙總各客戶端的數據進行分析. 其中客戶端ORM使用Mybatis. 經過Mybatis攔截器獲取全部在執行的SQL語句, 按期同步至服務器.mysql
本文經過在客戶端攔截SQL的操做介紹Mybatis攔截器的使用方法.git
客戶分店較多且比較分散, 部分店內網絡不穩定, 客戶要求每一個分店在無網絡的狀況下也能正常使用系統, 同時全部店面數據須要進行彙總分析. 綜合客戶的需求, 項目架構以下:github
將WEB項目及其運行環境經過NSIS製做安裝包在各分店進行安裝, 每一個分店是一個獨立的WEB服務, 這樣就保證店內在無網絡(有局域網,沒法訪問互聯網)的狀況下也能夠正常使用系統. 此時每一個分店的數據庫保存本身店內的運營數據, 各店之間的數據相互隔離.sql
但運營方沒法分析全部店面的彙總數據(如商品總體銷售狀況等), 所以須要將每一個店面的數據按期同步至服務器的數據庫中.數據庫
最終採用了將客戶端全部更新(增,刪,改)的SQL按照執行順序保存至數據庫中, 按期同步並在服務器的數據庫按照順序執行SQL, 以此來保證服務器數據庫的數據是各客戶端數據的彙總.apache
項目採用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' )
複製代碼
想這樣一個場景, 你作飯的時候可能須要如下步驟:服務器
買菜 >> 洗菜 >> 切菜 >> 作菜 >> 上菜 >> 洗碗
上面的作飯流程是按照步驟一步一步的進行, 咱們既能夠在其中的某個步驟中獲取前幾步的成果, 也能夠在某個步驟開始以前作些額外的事情, 好比: 切菜前對菜稱重等.
Mybatis提供了這樣一個組件: 他能夠在某個步驟執行以前先執行自定義的操做. 這個組件叫作攔截器. 所謂攔截器, 顧名思義: 須要定義攔截哪一個操做步驟及攔截後作什麼事情.
攔截器須要實現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
(數組)便可.Executor
,ParameterHandler
,ResultSetHandler
,StatementHandler
下的方法.在Spring配置文件中, 聲明攔截器並將其配置到SqlSessionFactoryBean
中plugins
屬性中
// Mybatis攔截器
sqlInterceptor(SQLInterceptor)
// Mybatis配置
sqlSessionFactory(SqlSessionFactoryBean) {
dataSource = ref("dataSource")
mapperLocations = "classpath*:/com/atd681/mybatis/interceptor/*_mapper.xml"
// 配置Mybatis攔截器
plugins = [
sqlInterceptor
]
}
複製代碼
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();
}
}
複製代碼
在數據庫中建立兩張表:
atd681_mybatis_test
: 存儲業務測試數據atd681_mybatis_sql
: 存儲業務操做的SQL建立DAO
和Mapper
, 建立增長, 刪除, 修改的方法及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.
- 執行DELETE操做
- 保存1中DELETE操做的SQL
- 執行INSERT SQL
- 保存3中INSERT操做的SQL
- 執行UPDATE SQL
- 保存5中UPDATE操做的SQL
上述6次數據庫操做必須在同一事務中, 不然一旦出現業務操做成功但保存SQL失敗的狀況. 服務器端同步的數據就會與客戶端本地不一致.
雖然Mybatis沒有Spring的擴展性強, 可是攔截器的出現也能夠幫助咱們解決一些常見問題. 經過攔截器能夠實現分頁, 統一設置參數等常見的功能.
https://github.com/atd681/alldemo
atd681-mybatis-interceptor