保存 Mybatis打印的SQL日志到數據庫


 

 

之前做項目,一般會有一張,用戶操作記錄的數據表,里面主要包括一些,用戶請求的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;
    }

}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM