[MyBatis源碼分析 - 日志模塊]

一、日志概述

??衡量軟件產(chǎn)品的質(zhì)量時(shí)蠕趁,是否具備完善的日志是個(gè)非常重要的因素薛闪。開發(fā)測(cè)試階段,需要日志幫助我們完善功能和發(fā)現(xiàn)bug俺陋;生產(chǎn)上豁延,當(dāng)出現(xiàn)生產(chǎn)問題時(shí),又需要日志幫助我們定位問題發(fā)生現(xiàn)場(chǎng)的情況腊状。同時(shí)诱咏,日志還是開發(fā)與運(yùn)維之間的橋梁,有助于運(yùn)維管理人員快速查找系統(tǒng)的故障和瓶頸缴挖,良好的日志在一個(gè)軟件中占了非常重要的地位袋狞。

??日志對(duì)于排查問題很有幫助,但不是越多越好,過多冗余的日志硕并,不管是日志輸出還是保存日志到文件法焰,都會(huì)消耗服務(wù)器的資源,我們希望某些日志在開發(fā)測(cè)試階段打出來倔毙,在版本穩(wěn)定投產(chǎn)之后只打印關(guān)鍵日志埃仪,還有想根據(jù)日志的不同類型分門別類歸納到日志文件中,日志文件達(dá)到一定大小或保存一定時(shí)間就刪除陕赃,這些日志需求的場(chǎng)景都是很常見的卵蛉,在 Java 的世界中已經(jīng)有多種成熟開源的日志框架,常用的有 Log4j么库、Log4j2傻丝、Apache Commons Logjava.util.logging诉儒、slf4j 等葡缰,它們的框架 API 略有差異,不過使用上整體大同小異忱反。

??MyBatis 聚合了上述多種優(yōu)秀的日志框架泛释,提供框架內(nèi)部輸出詳細(xì)日志的能力,并且為了屏蔽不同日志框架 API 的差異温算,提供了一個(gè)統(tǒng)一的接口怜校,并且基于該接口定制了針對(duì)不同日志框架的適配器,使得用戶可以根據(jù)自身喜好和使用習(xí)慣方便地選擇要使用的日志框架注竿。

??MyBatis 的日志模塊位于 org.apache.ibatis.logging 包中茄茁,如下所示:



二、整體設(shè)計(jì)

??日志模塊的整體設(shè)計(jì)如下:


??日志模塊使用了適配器模式巩割,對(duì)外提供的統(tǒng)一日志接口是 Log裙顽,每種日志框架都有對(duì)應(yīng)的適配器來適配接口的定義,LogFactory 是日志工廠類宣谈,MyBatis 中最重要的日志就是執(zhí)行 SQL 的時(shí)候打印調(diào)試日志愈犹,為了適應(yīng)格式化日志的要求,定義了 BaseJdbcLogger 及其幾個(gè)子類蒲祈,通過動(dòng)態(tài)代理的方式甘萧,在執(zhí)行 JDBC 相關(guān)方法時(shí)攔截萝嘁,并輸出響應(yīng)的日志梆掸,常見的有 SQL 語句、傳入?yún)?shù)牙言、執(zhí)行影響行數(shù)等酸钦。

三、Log

??不同的日志框架對(duì)日志級(jí)別的定義略有差異咱枉,比如 Log4j 的日志級(jí)別有:trace卑硫、debug徒恋、info、warn欢伏、error入挣、fatal;java.util.logging 的日志級(jí)別有:ALL硝拧、FINEST径筏、FINER、FINE障陶、CONFIG滋恬、INFO、WARNING抱究、SEVERE恢氯、OFF。MyBatis 統(tǒng)一提供了 trace鼓寺、debug勋拟、warn、error 四個(gè)級(jí)別侄刽,這基本與主流日志框架的日志級(jí)別類似指黎,可以滿足絕大多數(shù)場(chǎng)景的日志需求。

??org.apache.ibatis.logging.Log 定義了日志模塊的功能州丹,所有的日志適配器都要實(shí)現(xiàn)該接口醋安,接口定義如下:

public interface Log {
  boolean isDebugEnabled();

  boolean isTraceEnabled();

  void error(String s, Throwable e);

  void error(String s);

  void debug(String s);

  void trace(String s);

  void warn(String s);
}

??下面以 Log4jImplJdk14LoggingImpl 為例說明日志框架適配器的實(shí)現(xiàn),其他適配器類似墓毒。

1吓揪、Log4jImpl

??Log4jImpl 是 Log4j 日志框架對(duì)接口的適配器,其源碼如下:

public class Log4jImpl implements Log {
  // 顯示調(diào)用者位置的參數(shù)
  private static final String FQCN = Log4jImpl.class.getName();

  private Logger log;

  public Log4jImpl(String clazz) {
    log = Logger.getLogger(clazz);
  }

  @Override
  public boolean isDebugEnabled() {
    return log.isDebugEnabled();
  }

  @Override
  public boolean isTraceEnabled() {
    return log.isTraceEnabled();
  }

  @Override
  public void error(String s, Throwable e) {
    log.log(FQCN, Level.ERROR, s, e);
  }

  @Override
  public void error(String s) {
    log.log(FQCN, Level.ERROR, s, null);
  }

  @Override
  public void debug(String s) {
    log.log(FQCN, Level.DEBUG, s, null);
  }

  @Override
  public void trace(String s) {
    log.log(FQCN, Level.TRACE, s, null);
  }

  @Override
  public void warn(String s) {
    log.log(FQCN, Level.WARN, s, null);
  }

}

??這里有一個(gè)特別的地方所计,FQCN 參數(shù)柠辞,該參數(shù)傳入的 #log() 方法的第一個(gè)參數(shù)傳入,在 Log4j 中參數(shù)名叫 callFQCN主胧,log4j 把傳遞進(jìn)來的 callerFQCN 在調(diào)用堆棧中一一比較叭首,相等后,再往上一層即認(rèn)為是用戶的調(diào)用類踪栋。

2焙格、Jdk14LoggingImpl

??Jdk14LoggingImpl 是 JDK 內(nèi)置日志框架對(duì)接口的適配器,其源碼如下:

public class Jdk14LoggingImpl implements Log {

  private Logger log;

  public Jdk14LoggingImpl(String clazz) {
    log = Logger.getLogger(clazz);
  }

  @Override
  public boolean isDebugEnabled() {
    return log.isLoggable(Level.FINE);
  }

  @Override
  public boolean isTraceEnabled() {
    return log.isLoggable(Level.FINER);
  }

  @Override
  public void error(String s, Throwable e) {
    log.log(Level.SEVERE, s, e);
  }

  @Override
  public void error(String s) {
    log.log(Level.SEVERE, s);
  }

  @Override
  public void debug(String s) {
    log.log(Level.FINE, s);
  }

  @Override
  public void trace(String s) {
    log.log(Level.FINER, s);
  }

  @Override
  public void warn(String s) {
    log.log(Level.WARNING, s);
  }

}


四夷都、LogFactory

  public static final String MARKER = "MYBATIS";
  // 記錄當(dāng)前使用的第三方日志組件所對(duì)應(yīng)的適配器的構(gòu)造方法
  private static Constructor<? extends Log> logConstructor;

  // 針對(duì)每種日志組件調(diào)用 tryImplementation() 方法進(jìn)行嘗試加載眷唉,具體調(diào)用順序是:
  // useSlf4jLogging() --> useCommonsLogging() --> useLog4J2Logging() -->
  // useLog4JLogging() --> useJdkLogging() --> useNoLogging()
  static {
    tryImplementation(new Runnable() {
      @Override
      public void run() {
        useSlf4jLogging();
      }
    });
    tryImplementation(new Runnable() {
      @Override
      public void run() {
        useCommonsLogging();
      }
    });
    tryImplementation(new Runnable() {
      @Override
      public void run() {
        useLog4J2Logging();
      }
    });
    tryImplementation(new Runnable() {
      @Override
      public void run() {
        useLog4JLogging();
      }
    });
    tryImplementation(new Runnable() {
      @Override
      public void run() {
        useJdkLogging();
      }
    });
    tryImplementation(new Runnable() {
      @Override
      public void run() {
        useNoLogging();
      }
    });
  }
  // 工廠類對(duì)外只提供靜態(tài)方法,構(gòu)造函數(shù)私有
  private LogFactory() {
    // disable construction
  }

??LogFactory 中定義了 logConstructor 成員來保存當(dāng)前使用的第三方日志所對(duì)應(yīng)的適配器的構(gòu)造方法,第一次加載工廠類時(shí)冬阳,會(huì)調(diào)用靜態(tài)代碼塊初始化嘗試加載各種日志組件蛤虐,優(yōu)先級(jí)從代碼中可以看出,最先加載的適配器會(huì)賦值給 logConstructor肝陪,代碼如下:

  private static void tryImplementation(Runnable runnable) {
    // 先判斷是否已加載到適配器驳庭,是則放棄加載其他適配器
    if (logConstructor == null) {
      try {
        runnable.run();
      } catch (Throwable t) {
        // ignore
      }
    }
  }

??雖然將方法調(diào)用包裝成了一個(gè) runnable,但這里注意不是用 new Thread(runnable).start() 去啟動(dòng)一個(gè)新線程氯窍,只是簡單的調(diào)用 runnable.run() 所以對(duì)各個(gè)適配器的加載是串行的嚷掠,優(yōu)先級(jí)靠前的適配器加載不到才會(huì)真正嘗試去加載后面的適配器,各個(gè)加載適配器的方法里荞驴,都會(huì)去調(diào)用 #setImplementation() 完成加載動(dòng)作不皆,傳參是適配器對(duì)應(yīng)的 Class 對(duì)象,以 Log4j 為例:

  public static synchronized void useLog4JLogging() {
    setImplementation(org.apache.ibatis.logging.log4j.Log4jImpl.class);
  }

  private static void setImplementation(Class<? extends Log> implClass) {
    try {
      // 獲取構(gòu)造參數(shù)為 String 的構(gòu)造器
      Constructor<? extends Log> candidate = implClass.getConstructor(String.class);
      // 用構(gòu)造器實(shí)例化 Log 對(duì)象
      Log log = candidate.newInstance(LogFactory.class.getName());
      if (log.isDebugEnabled()) {
        log.debug("Logging initialized using '" + implClass + "' adapter.");
      }
      // 賦值
      logConstructor = candidate;
    } catch (Throwable t) {
      throw new LogException("Error setting Log implementation.  Cause: " + t, t);
    }
  }

??這里的 #useLog4JLogging() 方法定義用了 synchronized 關(guān)鍵字熊楼,這里有點(diǎn)疑惑霹娄,如果是加載各個(gè)適配器都啟動(dòng)一個(gè)新線程還可以理解,但問題是鲫骗,并沒有啟新線程犬耻,所以我猜測(cè)這里跟初始化加載無關(guān),跟調(diào)用線程有關(guān)执泰,因?yàn)?logConstructor 必須是線程安全的枕磁,因?yàn)楸┞督o外界的 #getLog 方法用了該變量去創(chuàng)建具體的 Log 對(duì)象,如下:

  public static Log getLog(Class<?> aClass) {
    return getLog(aClass.getName());
  }

  public static Log getLog(String logger) {
    try {
      return logConstructor.newInstance(logger);
    } catch (Throwable t) {
      throw new LogException("Error creating logger for logger " + logger + ".  Cause: " + t, t);
    }
  }


五术吝、JDBC 調(diào)試

??業(yè)務(wù)代碼在打印日志時(shí)计济,往往是先創(chuàng)建一個(gè) Logger,然后在一些關(guān)鍵的地方排苍,比如接口調(diào)用沦寂、錯(cuò)誤異常處理等地方打上日志,但有些時(shí)候淘衙,出于保持代碼清晰或其他原因传藏,在某些地方不想摻雜日志打印的代碼,這時(shí)候就可以使用動(dòng)態(tài)代理彤守,動(dòng)態(tài)代理常用的有 JDK 動(dòng)態(tài)代理毯侦、CGLIB 等,MyBatis 打印 SQL 執(zhí)行日志方便 JDBC 調(diào)試的功能具垫,就是基于 JDK 動(dòng)態(tài)代理實(shí)現(xiàn)的侈离。

??動(dòng)態(tài)代理,一般無非是在某個(gè)方法調(diào)用的前后添加某些處理做修,對(duì)于使用者來說霍狰,只要拿到代理對(duì)象,就可以將它當(dāng)成普通對(duì)象來使用饰及,代理的邏輯對(duì)使用者來說是透明蔗坯。

1、JDK 動(dòng)態(tài)代理的使用

??使用 JDK 動(dòng)態(tài)代理燎含,要滿足幾個(gè)條件:

  • (1)定義一個(gè)接口宾濒。
  • (2)被代理的類要實(shí)現(xiàn)(1)中接口。
  • (3)定義一個(gè)實(shí)現(xiàn)了 InvocationHandler 接口的類屏箍,該類中會(huì)封裝被代理的對(duì)象绘梦,并且該類實(shí)例會(huì)被傳遞給 Proxy.newProxyInstance 方法創(chuàng)建代理對(duì)象。

??下面舉例說明:

1.1 定義接口

public interface HelloWorld {
    public void sayHelloWorld();
}


1.2 代理類

public class HelloWorldImpl implements HelloWorld {
    @Override
    public void sayHelloWorld() {
        System.out.println("Hello World");      
    }
}


1.3 InvocationHandler 實(shí)現(xiàn)類

/**
 * 使用JDK實(shí)現(xiàn)動(dòng)態(tài)代理赴魁,要求被代理的對(duì)象必須實(shí)現(xiàn)一個(gè)接口
 */
public class JdkProxyExample implements InvocationHandler {

    // 真實(shí)對(duì)象
    private Object target = null;
    
    // 綁定真實(shí)對(duì)象并返回代理對(duì)象
    public Object bind(Object target) {
        this.target = target;
        // 第三個(gè)參數(shù)類型是InvocationHandler卸奉,代理對(duì)象被調(diào)用時(shí),會(huì)先執(zhí)行Handler接口的invoke方法
        return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
    }
    
    /**
     * 代理方法邏輯
     * @param proxy 代理對(duì)象
     * @param method 當(dāng)前調(diào)度方法
     * @param args 當(dāng)前方法參數(shù)
     * @return 代理結(jié)果返回
     * @throws Throwable 異常
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("進(jìn)入代理邏輯方法");
        System.out.println("在調(diào)度真實(shí)對(duì)象之前的服務(wù)");
        Object obj = method.invoke(target, args);
        System.out.println("在調(diào)度真實(shí)對(duì)象之后的服務(wù)");
        return obj;
    }
    
    public static void main(String[] args) {
        JdkProxyExample jdk = new JdkProxyExample();
        HelloWorld proxy = (HelloWorld) jdk.bind(new HelloWorldImpl());
        proxy.sayHelloWorld();
    }
}

??執(zhí)行結(jié)果如下:

進(jìn)入代理邏輯方法
在調(diào)度真實(shí)對(duì)象之前的服務(wù)
Hello World
在調(diào)度真實(shí)對(duì)象之后的服務(wù)


2颖御、BaseJdbcLogger 及其子類

??BaseJdbcLogger 及其子類通過使用動(dòng)態(tài)代理榄棵,實(shí)現(xiàn)了執(zhí)行 SQL 操作時(shí),無感輸出 JDBC 調(diào)試日志的效果潘拱。

2.1 BaseJdbcLogger

2.1.1 數(shù)據(jù)結(jié)構(gòu)

  protected static final Set<String> SET_METHODS = new HashSet<String>();       // 記錄了PreparedStatement接口中定義的常用的set*()方法
  protected static final Set<String> EXECUTE_METHODS = new HashSet<String>();   // 記錄了Statement接口和PreparedStatement接口中與執(zhí)行SQL語句相關(guān)的方法

  private Map<Object, Object> columnMap = new HashMap<Object, Object>();                // 記錄了PreparedStatement.set*()方法設(shè)置的鍵值對(duì)

  private List<Object> columnNames = new ArrayList<Object>();                   // 記錄了PreparedStatement.set*()方法設(shè)置的key值
  private List<Object> columnValues = new ArrayList<Object>();                  // 記錄了PreparedStatement.set*()方法設(shè)置的value值

  protected Log statementLog;   // 用于輸出日志的Log對(duì)象
  protected int queryStack;     // 記錄了SQL的層數(shù)疹鳄,用于格式化輸出SQL
  • SET_METHODS:記錄了PreparedStatement接口中定義的常用的set*()方法。
  • EXECUTE_METHODS:記錄了Statement接口和PreparedStatement接口中與執(zhí)行SQL語句相關(guān)的方法芦岂。
  • columnMap:記錄了PreparedStatement.set*()方法設(shè)置的鍵值對(duì)瘪弓。
  • columnNames:記錄了PreparedStatement.set*()方法設(shè)置的key值。
  • columnValues:記錄了PreparedStatement.set*()方法設(shè)置的value值禽最。
  • statementLog:用于輸出日志的Log對(duì)象腺怯。
  • queryStack: 記錄了SQL的層數(shù),用于格式化輸出SQL川无。

??在類加載時(shí)瓢喉,會(huì)對(duì) SET_METHODSEXECUTE_METHODS 進(jìn)行初始化舀透,源碼如下:

    static {
        SET_METHODS.add("setString");
        SET_METHODS.add("setNString");
        SET_METHODS.add("setInt");
        SET_METHODS.add("setByte");
        SET_METHODS.add("setShort");
        SET_METHODS.add("setLong");
        SET_METHODS.add("setDouble");
        SET_METHODS.add("setFloat");
        SET_METHODS.add("setTimestamp");
        SET_METHODS.add("setDate");
        SET_METHODS.add("setTime");
        SET_METHODS.add("setArray");
        SET_METHODS.add("setBigDecimal");
        SET_METHODS.add("setAsciiStream");
        SET_METHODS.add("setBinaryStream");
        SET_METHODS.add("setBlob");
        SET_METHODS.add("setBoolean");
        SET_METHODS.add("setBytes");
        SET_METHODS.add("setCharacterStream");
        SET_METHODS.add("setNCharacterStream");
        SET_METHODS.add("setClob");
        SET_METHODS.add("setNClob");
        SET_METHODS.add("setObject");
        SET_METHODS.add("setNull");

        EXECUTE_METHODS.add("execute");
        EXECUTE_METHODS.add("executeUpdate");
        EXECUTE_METHODS.add("executeQuery");
        EXECUTE_METHODS.add("addBatch");
    }


2.1.2 構(gòu)造函數(shù)

    public BaseJdbcLogger(Log log, int queryStack) {
        this.statementLog = log;
        if (queryStack == 0) {
            this.queryStack = 1;
        } else {
            this.queryStack = queryStack;
        }
    }

??初始化 Log 對(duì)象栓票,指定 SQL 層次。

2.1.3 方法功能

??BaseJdbcLogger 里大部分方法比較簡單愕够,要特別關(guān)注的是 #getParameterValueString()#prefix() 方法走贪。

String getParameterValueString()

【功能】獲取參數(shù)值字符串,其形式為 value1(type1), value2(type2), ..., null, ...惑芭,用于輸出實(shí)際執(zhí)行 SQL 時(shí)綁定參數(shù)的信息坠狡。
【源碼與注解】

    protected String getParameterValueString() {        
        List<Object> typeLists = new ArrayList<Object>(columnValues.size());
        // 遍歷所有綁定傳入的參數(shù)值,若為空則字符串為 "null"遂跟,否則為 "value(type)"
        for (Object value : columnValues) {
            if (value == null) {
                typeLists.add("null");
            } else {
                typeLists.add(value + "(" + value.getClass().getSimpleName() + ")");
            }           
        }
        // eg: [null, 10(Integer), test(String)]
        // 將 List 中的值轉(zhuǎn)化為字符串
        final String parameters = typeLists.toString();
        // 去掉前后中括號(hào)
        return parameters.substring(1, parameters.length() - 1);
    }


String prefix(boolean isInput)

【功能】根據(jù)判斷輸入還是輸出階段輸出的日志逃沿,決定日志前綴是形如 "==> " 還是 "<== "婴渡。
【源碼與注解】

    protected void debug(String text, boolean input) {
        if (statementLog.isDebugEnabled()) {
            statementLog.debug(prefix(input) + text);
        }
    }
    
    protected void trace(String text, boolean input) {
        if (statementLog.isTraceEnabled()) {
            statementLog.trace(prefix(input) + text);
        }
    }
    
    private String prefix(boolean isInput) {
        char[] buffer = new char[queryStack * 2 + 2];
        Arrays.fill(buffer, '=');
        buffer[queryStack * 2 + 1] = ' ';    // 前綴最后一個(gè)字符為空格,與實(shí)際的日志內(nèi)容隔開
        if (isInput) {
            buffer[queryStack * 2] = '>';    // 根據(jù)輸入還是輸出決定前綴的第一個(gè)字符為 ‘<’ 還是倒數(shù)第二個(gè)字符為 ‘>’
        } else {
            buffer[0] = '<';
        }
        return new String(buffer);
    }


String removeBreakingWhitespace(String original)

【功能】將多行字符串整合成一行凯亮,并且取出多余的空格边臼,主要是將 SQL 格式化,方便日志輸出假消。
【源碼與注解】

  protected String removeBreakingWhitespace(String original) {
    // 根據(jù)字符串中的 \t\n\r\f 解析處理柠并,分成多段
    StringTokenizer whitespaceStripper = new StringTokenizer(original);
    StringBuilder builder = new StringBuilder();
    // 將每一段拼接起來,并用空格分隔
    while (whitespaceStripper.hasMoreTokens()) {
      builder.append(whitespaceStripper.nextToken());
      builder.append(" ");
    }
    return builder.toString();
  }

【測(cè)試案例】

public class Test extends BaseJdbcLogger {

    public Test(Log log, int queryStack) {
        super(log, queryStack);
    }

    public static void main(String[] args) {
        String sql = "select id, role_name as roleName, note " +
                     "from t_role\r\n" + 
                     "where role_name like concat('%', #{roleName}, '%')";
        Test test = new Test(null, 0); 
        System.out.println(test.removeBreakingWhitespace(sql));
    }

}

執(zhí)行結(jié)果如下:



2.2 ConnectionLogger

【功能】ConnectionLogger 繼承了 BaseJdbcLogger 并實(shí)現(xiàn)了 InvocationHandler 接口富拗,封裝了真正的 Connection 對(duì)象臼予,并為其生成代理對(duì)象。
【構(gòu)造方法和成員】

public final class ConnectionLogger extends BaseJdbcLogger implements InvocationHandler {

  private Connection connection;

  private ConnectionLogger(Connection conn, Log statementLog, int queryStack) {
    super(statementLog, queryStack);
    this.connection = conn;
  }

  // other code
}

【生成代理對(duì)象方法】

  // 創(chuàng)建一個(gè)內(nèi)嵌Log對(duì)象的代理Connection對(duì)象
  public static Connection newInstance(Connection conn, Log statementLog, int queryStack) {
    InvocationHandler handler = new ConnectionLogger(conn, statementLog, queryStack);
    ClassLoader cl = Connection.class.getClassLoader();
    return (Connection) Proxy.newProxyInstance(cl, new Class[]{Connection.class}, handler);
  }

【代理邏輯】

  @Override
  public Object invoke(Object proxy, Method method, Object[] params)
      throws Throwable {
    try {
      // 如果調(diào)用的是從Object繼承的方法啃沪,則直接調(diào)用粘拾,不做任何其他處理
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, params);
      }
      // 如果調(diào)用的是preparedStatement()方法、prepareCall()方法或createStatement()方法创千,
      // 則在創(chuàng)建相應(yīng)的statement對(duì)象后半哟,為其創(chuàng)建代理對(duì)象并返回該代理對(duì)象
      if ("prepareStatement".equals(method.getName())) {
        if (isDebugEnabled()) {
          debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true);
        }
        // 調(diào)用底層封裝的Connection對(duì)象的prepareStatement()方法,得到PreparedStatement對(duì)象
        PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);
        stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack);
        return stmt;
      } else if ("prepareCall".equals(method.getName())) {
        if (isDebugEnabled()) {
          debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true);
        }        
        PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);
        stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack);
        return stmt;
      } else if ("createStatement".equals(method.getName())) {
        Statement stmt = (Statement) method.invoke(connection, params);
        stmt = StatementLogger.newInstance(stmt, statementLog, queryStack);
        return stmt;
      } else {
        // 其他方法則直接調(diào)用底層Connection對(duì)象的相應(yīng)方法
        return method.invoke(connection, params);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
  }
  // 返回真實(shí)的對(duì)象
  public Connection getConnection() {
    return connection;
  }
}

【解析】

  • (1)ConnectionLogger 封裝了 Connection 對(duì)象签餐,在構(gòu)造函數(shù)中寓涨,調(diào)用父類構(gòu)造器用傳入的Log 對(duì)象和 SQL 層次進(jìn)行初始化。
  • (2)#newInstance() 方法負(fù)責(zé)暴露給調(diào)用者生成代理對(duì)象氯檐。
  • (3)當(dāng)執(zhí)行生成的代理 Connection 對(duì)象的方法時(shí)戒良,會(huì)調(diào)用 #invoke() 方法,在該方法中冠摄,對(duì)執(zhí)行的方法進(jìn)行攔截糯崎,若是 Object 類中聲明的方法,直接反射調(diào)用河泳,不做特殊處理沃呢,如果是 prepareStatement、prepareCall拆挥、createStatement 方法薄霜,為方法調(diào)用返回的 PreparedStatementStatement 對(duì)象生成響應(yīng)的代理對(duì)象再返回纸兔,因?yàn)?prepareStatement惰瓜、prepareStatement 方法的入?yún)⑹?SQL,所以這里打印 SQL 的日志汉矿,開發(fā)者測(cè)試時(shí)就可以從日志中看到實(shí)際執(zhí)行的 SQL 是什么崎坊,特別是使用動(dòng)態(tài) SQL 時(shí),很難看出實(shí)際執(zhí)行的 SQL 是什么洲拇。

【測(cè)試案例】

??調(diào)用代碼(部分)

        SqlSession sqlSession = null;
        try {
            sqlSession = SqlSessionFactoryUtil.openSqlSession("mybatis-config-properties.xml");
            RoleMapper roleMapper = sqlSession.getMapper(RoleMapper.class);
            Role role = roleMapper.getRole(1L);
            System.out.println("Role = " + role);
        } finally {
            if (sqlSession != null) {
                sqlSession.close();
            }
        }

??mapper 配置(部分)

    <select id="getRole" parameterType="long" resultType="role">
        select id, role_name as roleName, note 
        from t_role 
        where id = #{id}
    </select>

??在 ConnectionLogger 里的 #invoke() 方法里打上斷點(diǎn)奈揍,運(yùn)行觀察


可以看到曲尸,執(zhí)行 SQL 時(shí)會(huì)調(diào)用 Connection.prepareStatement() 創(chuàng)建 PreparedStatement 對(duì)象,并打印一行日志輸出取出多余換行空格后的 SQL男翰。

2.3 PreparedStatementLogger

【功能】PreparedStatementLogger 繼承了 BaseJdbcLogger 并實(shí)現(xiàn)了 InvocationHandler 接口另患,封裝了真正的 PreparedStatement 對(duì)象,并提供了 #newInstance() 方法生成代理對(duì)象,會(huì)被 ConnectionLogger 所調(diào)用。
【代理邏輯】

  @Override
  public Object invoke(Object proxy, Method method, Object[] params) throws Throwable {
    try {
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, params);
      }
      // (1) 如果調(diào)用 PreparedStatement 的執(zhí)行方法
      if (EXECUTE_METHODS.contains(method.getName())) {
        // (1.1) 輸出綁定參數(shù)信息
        if (isDebugEnabled()) {
          debug("Parameters: " + getParameterValueString(), true);
        }
        clearColumnInfo();
        // (1.2 )執(zhí)行查詢SQL叽粹,返回值是一個(gè)ResultSet對(duì)象铺浇,封裝生成代理對(duì)象ResultSetLogger返回
        if ("executeQuery".equals(method.getName())) {
          ResultSet rs = (ResultSet) method.invoke(statement, params);
          return rs == null ? null : ResultSetLogger.newInstance(rs, statementLog, queryStack);
        } else {
          // (1.3) 執(zhí)行其他方法不用封裝代理對(duì)象返回
          return method.invoke(statement, params);
        }
      // (2) 如果執(zhí)行的是設(shè)置參數(shù)的方法,將傳入?yún)?shù)保存到父類成員中敛熬,方便執(zhí)行時(shí)輸出參數(shù)日志
      } else if (SET_METHODS.contains(method.getName())) {
        if ("setNull".equals(method.getName())) {
          setColumn(params[0], null);
        } else {
          setColumn(params[0], params[1]);
        }
        return method.invoke(statement, params);
      // (3) getResultSet() 一般跟 execute() 方法配合使用肺稀,用來獲取SQL執(zhí)行的結(jié)果集
      } else if ("getResultSet".equals(method.getName())) {
        ResultSet rs = (ResultSet) method.invoke(statement, params);
        return rs == null ? null : ResultSetLogger.newInstance(rs, statementLog, queryStack);
      // (4) getUpdateCount() 一般跟 execute() 方法配合使用,用來獲取SQL執(zhí)行的更新計(jì)數(shù)
      } else if ("getUpdateCount".equals(method.getName())) {
        int updateCount = (Integer) method.invoke(statement, params);
        if (updateCount != -1) {
          debug("   Updates: " + updateCount, false);
        }
        return updateCount;
      } else {
        return method.invoke(statement, params);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
  }

【解析】
(1) 如果調(diào)用 PreparedStatement 的執(zhí)行方法
??在 BaseJdbcLogger 中应民,存儲(chǔ)了四個(gè)執(zhí)行方法的名稱话原,分別為:executeexecuteUpdate诲锹、executeQuery繁仁、addBatch

  • execute 是一個(gè)通用的執(zhí)行 SQL 的方法归园,返回值為布爾值黄虱,如果為 true,表示返回一個(gè)結(jié)果集(常見于查詢語句)庸诱,如果為 false捻浦,則返回表示沒有結(jié)果或者是更新計(jì)數(shù);
  • executeUpdate 可以執(zhí)行 INSERT桥爽、UPDATE 或 DELETE 語句朱灿,返回值是一個(gè)整數(shù)表示影響行數(shù);
  • executeQuery 通常用來執(zhí)行 SELECT 語句钠四,返回值是一個(gè)結(jié)果集盗扒;
  • addBatch 是往 Statement 對(duì)象中添加要批量執(zhí)行的 SQL;

?(1.1)在執(zhí)行上述這些方法時(shí)缀去,通常都是會(huì)綁定參數(shù)的环疼,所以如果檢查到執(zhí)行上述的方法,會(huì)輸出綁定參數(shù)的日志朵耕。
?(1.2)如果執(zhí)行 executeQuery 肯定返回一個(gè) ResultSet炫隶,封裝成代理對(duì)象返回。
?(1.3)其他執(zhí)行方法直接反射調(diào)用阎曹。

(2)如果執(zhí)行的是設(shè)置參數(shù)的方法伪阶,將傳入?yún)?shù)保存到父類成員中煞檩,方便執(zhí)行時(shí)輸出參數(shù)日志。
(3)getResultSet() 一般跟 execute() 方法配合使用栅贴,用來獲取SQL執(zhí)行的結(jié)果集斟湃,這里生成結(jié)果集的代理對(duì)象返回。
(4)getUpdateCount() 一般跟 execute() 方法配合使用檐薯,用來獲取SQL執(zhí)行的更新計(jì)數(shù)凝赛。

【測(cè)試案例】
??還是上面的代碼,在 PreparedStatementLogger 中打上斷點(diǎn)坛缕,執(zhí)行情況如下圖所示:


??可以看到執(zhí)行的方法是 execute()墓猎,并輸出了 SQL 綁定參數(shù)的日志。

2.4 ResultLogger

【功能】ResultLogger 繼承了 BaseJdbcLogger 并實(shí)現(xiàn)了 InvocationHandler 接口赚楚,封裝了真正的 ResultSet 對(duì)象毙沾,并提供了 #newInstance() 方法生成代理對(duì)象,會(huì)被 PreparedStatementLoggerStatementLogger 所調(diào)用宠页。
【數(shù)據(jù)結(jié)構(gòu)與構(gòu)造】

  private static Set<Integer> BLOB_TYPES = new HashSet<Integer>();    // 不輸出字段值類型左胞,主要是超大數(shù)據(jù)的類型
  private boolean first = true;                                 // 控制輸出結(jié)果集字段名的日志時(shí),只在第一次打印日志
  private int rows = 0;                                         // 統(tǒng)計(jì)結(jié)果集中有多少行
  private ResultSet rs;                                         // 封裝的真實(shí)ResultSet對(duì)象
  private Set<Integer> blobColumns = new HashSet<Integer>();          // 存儲(chǔ)屬于大數(shù)據(jù)類型的字段名

  static {
    BLOB_TYPES.add(Types.BINARY);
    BLOB_TYPES.add(Types.BLOB);
    BLOB_TYPES.add(Types.CLOB);
    BLOB_TYPES.add(Types.LONGNVARCHAR);
    BLOB_TYPES.add(Types.LONGVARBINARY);
    BLOB_TYPES.add(Types.LONGVARCHAR);
    BLOB_TYPES.add(Types.NCLOB);
    BLOB_TYPES.add(Types.VARBINARY);
  }
  
  private ResultSetLogger(ResultSet rs, Log statementLog, int queryStack) {
    super(statementLog, queryStack);
    this.rs = rs;
  }

??ResultLogger 具備打印結(jié)果集中每行數(shù)據(jù)信息日志的功能举户,需要將日志級(jí)別設(shè)定為 TRACE烤宙,需要在 log4j.properties 文件中添加以下配置:

log4j.logger.ssm=TRACE

??其中,后綴 ssm 是映射配置文件中 <mapper> 標(biāo)簽的 namespace 屬性的前綴即可俭嘁。
??ResultLogger 限制了一些可能攜帶大量數(shù)據(jù)的字段的數(shù)據(jù)的輸出门烂,BLOB_TYPES 就存儲(chǔ)了受限制的類型,并在加載類時(shí)靜態(tài)初始化兄淫,其他字段的含義看注釋即可屯远。

【代理邏輯】

  @Override
  public Object invoke(Object proxy, Method method, Object[] params) throws Throwable {
    try {
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, params);
      }    
      Object o = method.invoke(rs, params);
      // 調(diào)用 ResultSet.next(),遍歷結(jié)果及中的多行結(jié)果
      if ("next".equals(method.getName())) {
        if (((Boolean) o)) {
          rows++;     // 統(tǒng)計(jì)行數(shù)加一
          // 需要開啟TRACE的日志級(jí)別才能輸出每一行的字段信息和對(duì)應(yīng)的值信息
          if (isTraceEnabled()) {
            ResultSetMetaData rsmd = rs.getMetaData();
            final int columnCount = rsmd.getColumnCount();
            // 字段信息只需要輸出一次日志
            if (first) {
              first = false;
              printColumnHeaders(rsmd, columnCount);
            }
            // 輸出每行數(shù)據(jù)中的字段值
            printColumnValues(columnCount);
          }
        } else {
          debug("     Total: " + rows, false);
        }
      }
      clearColumnInfo();
      return o;
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
  }

??當(dāng) ResultSet.next() 被調(diào)用時(shí)捕虽,類似迭代器會(huì)遍歷結(jié)果集中的多行結(jié)果慨丐,假如返回 false,表示沒有行了泄私,則打印統(tǒng)計(jì)行數(shù)的日志房揭;假如返回 true,則獲取行信息晌端,打印其字段信息和字段值捅暴,對(duì)應(yīng) #printColumnHeaders()#printColumnValues() 方法,還是上面的測(cè)試案例咧纠,加上 TRACE 的配置之后蓬痒,輸出的日志如下:

2.5 StatementLogger

??跟 PreparementLogger 類似,不贅述漆羔。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末梧奢,一起剝皮案震驚了整個(gè)濱河市狱掂,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌亲轨,老刑警劉巖趋惨,帶你破解...
    沈念sama閱讀 217,657評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異惦蚊,居然都是意外死亡器虾,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,889評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門蹦锋,熙熙樓的掌柜王于貴愁眉苦臉地迎上來兆沙,“玉大人,你說我怎么就攤上這事晕粪〖废ぃ” “怎么了渐裸?”我有些...
    開封第一講書人閱讀 164,057評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵巫湘,是天一觀的道長。 經(jīng)常有香客問我昏鹃,道長尚氛,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,509評(píng)論 1 293
  • 正文 為了忘掉前任洞渤,我火速辦了婚禮阅嘶,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘载迄。我一直安慰自己讯柔,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,562評(píng)論 6 392
  • 文/花漫 我一把揭開白布护昧。 她就那樣靜靜地躺著魂迄,像睡著了一般。 火紅的嫁衣襯著肌膚如雪惋耙。 梳的紋絲不亂的頭發(fā)上捣炬,一...
    開封第一講書人閱讀 51,443評(píng)論 1 302
  • 那天,我揣著相機(jī)與錄音绽榛,去河邊找鬼湿酸。 笑死,一個(gè)胖子當(dāng)著我的面吹牛灭美,可吹牛的內(nèi)容都是我干的推溃。 我是一名探鬼主播,決...
    沈念sama閱讀 40,251評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼届腐,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼美莫!你這毒婦竟也來了页眯?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,129評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤厢呵,失蹤者是張志新(化名)和其女友劉穎窝撵,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體襟铭,經(jīng)...
    沈念sama閱讀 45,561評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡碌奉,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,779評(píng)論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了寒砖。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片赐劣。...
    茶點(diǎn)故事閱讀 39,902評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖哩都,靈堂內(nèi)的尸體忽然破棺而出魁兼,到底是詐尸還是另有隱情,我是刑警寧澤漠嵌,帶...
    沈念sama閱讀 35,621評(píng)論 5 345
  • 正文 年R本政府宣布咐汞,位于F島的核電站,受9級(jí)特大地震影響儒鹿,放射性物質(zhì)發(fā)生泄漏化撕。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,220評(píng)論 3 328
  • 文/蒙蒙 一约炎、第九天 我趴在偏房一處隱蔽的房頂上張望植阴。 院中可真熱鬧,春花似錦圾浅、人聲如沸掠手。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,838評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽喷鸽。三九已至,卻和暖如春府寒,著一層夾襖步出監(jiān)牢的瞬間魁衙,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,971評(píng)論 1 269
  • 我被黑心中介騙來泰國打工株搔, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留剖淀,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,025評(píng)論 2 370
  • 正文 我出身青樓纤房,卻偏偏與公主長得像纵隔,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,843評(píng)論 2 354