摘要:最開始我想做的是通過攔截器攔截SQL執行,但是經過測試發現,過濾器至少可以監聽每一個SQL的執行與返回結果。因此,將這一次探索過程記錄下來。
本文分享自華為雲社群《jfinal中使用過濾器監控Druid的SQL執行【五月07】》,作者:KevinQ 。
最開始我想做的是通過攔截器攔截SQL執行,比如類似與PageHelper這種外掛,通過攔截器或過濾器,手動修改SQL語句,以實現某些業務需求,比如執行分頁,或者限制存取的資料許可權等等。但是查到資料說過濾器不是幹這個的,幹這個的是資料庫中介軟體乾的事情,比如MyCat等。
但是經過測試發現,過濾器至少可以監聽每一個SQL的執行與返回結果。因此,將這一次探索過程記錄下來。
在jfinal的啟動設定類中,有一個函數configPlugin(Plugins me)函數來設定外掛,這個函數會在jfinal啟動時呼叫,這個函數的引數是Plugins me,這個引數是一個外掛管理器,可以通過這個外掛管理器來新增外掛。
資料庫外掛Druid就是在該函數內新增的。
public void configPlugin(Plugins me) { DruidPlugin druidPlugin = createDruidPlugin_holdoa(); druidPlugin.setPublicKey(p.get("publicKeydebug").trim()); wallFilter = new WallFilter(); wallFilter.setDbType("mysql"); druidPlugin_oa.addFilter(wallFilter); druidPlugin_oa.addFilter(new StatFilter()); me.add(druidPlugin); }
我們參考WallFilter以及StatFilter也建立一個過濾器類:
import com.alibaba.druid.filter.FilterEventAdapter; public class DataScopeFilter extends FilterEventAdapter { }
我們發現FilterEventAdapter中的方法大概有這幾個:
public boolean statement_execute(FilterChain chain, StatementProxy statement, String sql) throws SQLException {...} protected void statementExecuteUpdateBefore(StatementProxy statement, String sql) {...} protected void statementExecuteUpdateAfter(StatementProxy statement, String sql, int updateCount) {...} protected void statementExecuteQueryBefore(StatementProxy statement, String sql) {...} protected void statementExecuteQueryAfter(StatementProxy statement, String sql, ResultSetProxy resultSet) {...} protected void statementExecuteBefore(StatementProxy statement, String sql) {...} protected void statementExecuteAfter(StatementProxy statement, String sql, boolean result) {...}
我們複寫這幾個方法來看一下(排除Update方法,因為我們更關心查詢語句)
package xxxx.xxxx; import com.alibaba.druid.filter.FilterChain; import com.alibaba.druid.filter.FilterEventAdapter; import com.alibaba.druid.proxy.jdbc.ResultSetProxy; import com.alibaba.druid.proxy.jdbc.StatementProxy; import com.jfinal.kit.LogKit; import java.sql.SQLException; public class DataScopeFilter extends FilterEventAdapter { @Override public boolean statement_execute(FilterChain chain, StatementProxy statement, String sql) throws SQLException { LogKit.info("statement_execute"); return super.statement_execute(chain, statement, sql); } @Override protected void statementExecuteQueryBefore(StatementProxy statement, String sql) { LogKit.info("statementExecuteQueryBefore"); super.statementExecuteQueryBefore(statement, sql); } @Override protected void statementExecuteQueryAfter(StatementProxy statement, String sql, ResultSetProxy resultSet) { LogKit.info("statementExecuteQueryAfter"); super.statementExecuteQueryAfter(statement, sql, resultSet); } @Override protected void statementExecuteBefore(StatementProxy statement, String sql) { LogKit.info("statementExecuteBefore"); super.statementExecuteBefore(statement, sql); } @Override protected void statementExecuteAfter(StatementProxy statement, String sql, boolean result) { LogKit.info("statementExecuteAfter"); super.statementExecuteAfter(statement, sql, result); } @Override public ResultSetProxy statement_executeQuery(FilterChain chain, StatementProxy statement, String sql) throws SQLException { LogKit.info("statement_executeQuery"); return super.statement_executeQuery(chain, statement, sql); } }
然後再config設定類中新增過濾器:
druidPlugin.addFilter(new DataScopeFilter());
發起其執行順序為:
statement_executeQuery
statementExecuteQueryBefore
statementExecuteQueryAfter
檢視父級程式碼,發現其執行邏輯是,首先執行statement_executeQuery,然後因為呼叫父級的方法,而父級方法體為:
@Override public ResultSetProxy statement_executeQuery(FilterChain chain, StatementProxy statement, String sql) throws SQLException { statementExecuteQueryBefore(statement, sql); try { ResultSetProxy resultSet = super.statement_executeQuery(chain, statement, sql); if (resultSet != null) { statementExecuteQueryAfter(statement, sql, resultSet); resultSetOpenAfter(resultSet); } return resultSet; } catch (SQLException error) { statement_executeErrorAfter(statement, sql, error); throw error; } catch (RuntimeException error) { statement_executeErrorAfter(statement, sql, error); throw error; } catch (Error error) { statement_executeErrorAfter(statement, sql, error); throw error; } }
從而進一步觸發statementExecuteQueryBefore方法與statementExecuteQueryAfter方法。
因此我們,修改statement_executeQuery方法:
@Override public ResultSetProxy statement_executeQuery(FilterChain chain, StatementProxy statement, String sql) throws SQLException { statementExecuteQueryBefore(statement, sql); ResultSetProxy result = chain.statement_executeQuery(statement, sql); statementExecuteQueryAfter(statement, sql, result); return result; }
如此,便讓輸出結果為:
statementExecuteQueryBefore
statement_executeQuery
statementExecuteQueryAfter
我們可以在Before或者After方法中新增一些邏輯,比如:記錄SQL的實際執行人,操作時間,請求執行SQL的介面。
發現執行的SQL在Druid中對應的類是:DruidPooledPreparedStatement,其類結構為:
public class DruidPooledPreparedStatement extends DruidPooledStatement implements PreparedStatement { private final static Log LOG = LogFactory.getLog(DruidPooledPreparedStatement.class); private final PreparedStatementHolder holder; private final PreparedStatement stmt; private final String sql; .... }
這也就以為著,該類一旦建立,SQL設定後就不允許再修改了,因此,我們需要修改SQL的話,就需要在prepared物件生成之前就修改到對應的執行SQL。
在偵錯過程中,發現需要覆蓋下面這個方法:
@Override public PreparedStatementProxy connection_prepareStatement(FilterChain chain, ConnectionProxy connection, String sql) throws SQLException { // 可以達到修改SQL的目的 sql += " LIMIT 1"; PreparedStatementProxy statement = super.connection_prepareStatement(chain, connection, sql); statementPrepareAfter(statement); return statement; }
我們可以在這裡新增自定義的SQL修改邏輯,比如新增資料許可權等等。