之前做項目,一般會有一張,用戶操作記錄的數據表,里面主要包括一些,用戶請求的URL和請求參數,用以記錄用戶做過哪些事情。並沒有以文件的形式來做記錄,當然只適合於一些用戶量特別少的系統。
而Mybatis打印SQL這個就比較常見了,但是還要保存SQL到數據庫就不那么常見了,最近我遇到了一個這樣的需求(當然我是為了操作方便,具體業務就不敘說了),主要實現的就是一個把打印的sql給保存起來
其中保存的sql是最終的sql,也就是說,這個sql拿出來是可以直接在數據庫客戶端執行的!目前這種方式只適合 使用Druid數據庫連接池配置的打印SQL的方式
首先上配置
上圖 是一個簡單的Druid連接池的配置,終點看logFilter 里面的屬性的值表示打印sql,我們隨便操作一下就會打印許多sql
如圖
可以發現一條數據庫操作會打印多條sql,但其中只有青色的框是我們想要的保存的SQL,同時后面還跟着Log4jFilter.java:137,然后找到Log4jFilter.java ,使用IDE可以直接在Druid配置文件中,點進Log4jFilter class文件中,
然后你會發現,這個類並沒有137行,總共才130行代碼
同時,發現這個類繼承了LogFilter 類 且實現了Log4jFilterMBean的接口
我們先進LogFilter 類看一看:
通過之前打印的SQL 我們可以看到 在我們想要的SQL前面,打印了如下字符
應該可以想到其中executed. 是硬編碼進去的,所以我們在這個類中搜索這個字符串,找到了好幾個,
但是只有兩個是比較相符的,
那么究竟是第一個還是第二個呢,實際上這個時候從字面上就可以看出來了,第一個是沒有參數的打印SQL語句,第二個是有參數的打印SQL語句
所以當是有條件的查詢,走第二個,沒有條件的查詢走第一個,那么,很清楚的知道了,沒有參數的查詢sql,就是該方法的 入參sql,那么有條件的查詢的查詢語句是什么?
很明顯,倒數第二行的變量var8 即是,倒數第二行就是把參數和sql進行了格式化(即把參數放進了sql中),這樣var8就是一條完整的sql了,那么找到了sql,如何實現自定義保存這個sql了
簡單的方法即是:復制Log4jFilter LogFilter 這兩個文件,分別重命名 MyLog4jFilter MyLogFilter ,並且讓 MyLog4jFilter 繼承 MyLogFilter
同時更改配置文件(將logFilter的實現類,更改成自己的):
然后找到 MyLogFilter 找到變量var8
添加一段代碼:
接着重啟服務!
發現,sql都可以打印出來了,保存數據庫的代碼,我就不寫了
以上的記錄SQL是一種方式相對比較簡單,但代碼侵入性就比較強,有沒有比較好的方式呢,當然有那就是Mybatis攔截器
利用mybatis攔截器,我們可以攔截相關的SQL語句,進行相應的處理,比如修改參數等,這樣我們可以做出mybatis的分頁插件等等
但我們需要做的是記錄SQL
package com.darkBlue.web; import com.alibaba.druid.pool.DruidDataSource; import com.alibaba.fastjson.JSON; import com.darkBlue.web.mobile.OperateLog; import org.apache.ibatis.cache.CacheKey; import org.apache.ibatis.executor.Executor; import org.apache.ibatis.executor.parameter.ParameterHandler; import org.apache.ibatis.executor.statement.StatementHandler; import org.apache.ibatis.jdbc.SQL; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.mapping.ParameterMapping; import org.apache.ibatis.plugin.*; import org.apache.ibatis.reflection.MetaObject; import org.apache.ibatis.reflection.SystemMetaObject; import org.apache.ibatis.scripting.defaults.DefaultParameterHandler; import org.apache.ibatis.scripting.xmltags.SqlNode; import org.apache.ibatis.session.Configuration; import org.apache.ibatis.session.ResultHandler; import org.apache.ibatis.session.RowBounds; import java.lang.reflect.Method; import java.sql.Connection; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Properties; import com.alibaba.druid.sql.SQLUtils; import org.apache.velocity.util.ArrayListWrapper; import javax.sql.DataSource; import static org.apache.ibatis.reflection.SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY; /** * 參考文獻:http://www.yangxuwang.com/jingyan/1533818219451005 * <p> * 定義自己的Interceptor最重要的是要實現plugin方法和intercept方法,在plugin方法中我們可以決定是否要進行攔截進而決定要返回 * 一個什么樣的目標對象。而intercept方法就是要進行攔截的時候要執行的方法。 * <p> * 對於實現自己的Interceptor而言有兩個很重要的注解,一個是@Intercepts,其值是一個@Signature數組。@Intercepts用於表明當前的對象是一個Interceptor, * 而@Signature則表明要攔截的接口、方法以及對應的參數類型 * Mybatis支持對Executor、StatementHandler、PameterHandler和ResultSetHandler進行攔截,也就是說會對這4種對象進行代理 */ /** * method:表示攔截的方法,mybatis支持的方法有 update, query, flushStatements, commit, rollback, getTransaction, close, isClosed * 方法,其中,update包括新增、修改、刪除等方法,query用於查詢,其它的基本用不到。 * args:表示攔截的參數類型,有MappedStatement、Object、RowBounds和ResultHandler等等. * type:表示攔截的類,有Executor、StatementHandler、ParameterHandler和ResultSetHandler。 */ @Intercepts({@org.apache.ibatis.plugin.Signature( type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}), @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class})}) public class MybatisInterceptor implements Interceptor { /** * intercept方法就是要進行攔截的時候要執行的方法。 */ @Override public Object intercept(Invocation invocation) throws Throwable { Object[] args = invocation.getArgs(); MappedStatement ms = (MappedStatement) args[0]; ms.getStatementType(); // 當前SQL使用的是哪個Mapper,即哪個Mapper類 String mapper = ms.getResource(); Configuration configuration = ms.getConfiguration(); // 執行當前SQL的Mapper id,其組成 [ 類型.方法 ] String mapperID = ms.getId(); // 獲取當前執行的SQL使用哪個數據源,我這里的數據源組件使用的是Druid,如果使用c3p0或者其他,則需要查看相關API,一般來降一個項目可能會配多個數據源,但是數據源組件都會使用一個 DruidDataSource dataSource = (DruidDataSource) configuration.getEnvironment().getDataSource(); // 獲取數據庫的類型[即mysql,或者oracle等等] String dbType = dataSource.getDataSourceStat().getDbType(); // 存放的是SQL的參數[它是一個實例對象] Object parameterObject = args[1]; Object target = invocation.getTarget(); StatementHandler handler = configuration.newStatementHandler((Executor) target, ms, parameterObject, RowBounds.DEFAULT, null, null); /** * commandName.startsWith(增/刪/改/查),可以得到crud的具體類型[得到的是大寫的INSERT UPDATE] * method.getName()得到的name可能為update, query, flushStatements, commit, rollback, getTransaction, close, isClosed */ String commandName = ms.getSqlCommandType().name(); Method method = invocation.getMethod(); String methodName = method.getName(); BoundSql boundSql = ms.getBoundSql(parameterObject); // 這個ParameterMapping表示當前SQL綁定的是哪些參數,及參數類型,但並不是參數本身 List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); // 將參數值轉成json字符串 String parameterObjects = JSON.toJSONString(boundSql.getParameterObject()); // 要攔截的SQL,通過攔截器的SQL 其不帶參數 String srcSQL = boundSql.getSql(); // 返回拼裝好參數的SQL String retSQL = formatSQL(srcSQL, dbType, parameterObjects); // 先執行當前的SQL方法,即通過當前攔截器的CRUD操作,因為我們要返回這個結果 Object result = invocation.proceed(); // 組裝自己的SQL記錄類 OperateLog log = new OperateLog(); // 記錄SQL // log.setStatement(); //記錄影響行數 // log.setResult(Integer.valueOf(Integer.parseInt(result.toString()))); // 記錄時間 // log.setOperateDate(new Date()); //TODO 還可以記錄參數,或者單表id操作時,記錄數據操作前的狀態 //獲取insertSqlLog方法 // ms = ms.getConfiguration().getMappedStatement("insertSqlLog"); //替換當前的參數為新的ms // args[0] = ms; //insertSqlLog 方法的參數為 log args[1] = log; //執行insertSqlLog方法 // invocation.proceed(); // 返回攔截器攔截的執行結果 return result; } /** * plugin方法是攔截器用於封裝目標對象的,通過該方法我們可以返回目標對象本身,也可以返回一個它的代理。 * 當返回的是代理的時候我們可以對其中的方法進行攔截來調用intercept方法,當然也可以調用其他方法 * 對於plugin方法而言,其實Mybatis已經為我們提供了一個實現。Mybatis中有一個叫做Plugin的類, * 里面有一個靜態方法wrap(Object target,Interceptor interceptor),通過該方法可以決定要返回的對象是目標對象還是對應的代理。 */ @Override public Object plugin(Object o) { // 只攔截Executor對象,減少目標被代理的次數 if (o instanceof Executor) { return Plugin.wrap(o, this); } return o; } /** * setProperties方法是用於在Mybatis配置文件中指定一些屬性的 * 這個方法在Configuration初始化當前的Interceptor時就會執行 */ @Override public void setProperties(Properties properties) { } /** * @describe: 組裝SQL * @params: * @Author: Kanyun * @Date: 2018/8/22 10:53 */ public String formatSQL(String src, String dbType, String params) { // 要傳入的SQLUtils的參數集合,實際上雖然泛型是Object,但其實都是基本數據類型 List<Object> paramList = new ArrayList(); // 有了JSON字符串我們就可以通過正則表達式得到參數了 System.out.println(params); // 需要注意的是這個SQLUtils是Druid數據源中的一個工具類,因為有現成的拼sql的工具,所以我就不再重復造輪子了,如果你的項目並沒有使用Druid, // 則需要將這個工具類加入到你的項目中 String retSQL = SQLUtils.format(src, dbType, paramList); return retSQL; } }