一、日志概述
??衡量軟件產(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 Log
、java.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);
}
??下面以 Log4jImpl
和 Jdk14LoggingImpl
為例說明日志框架適配器的實(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_METHODS
、EXECUTE_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)用返回的 PreparedStatement、Statement 對(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í)行方法的名稱话原,分別為:execute
、executeUpdate
诲锹、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ì)被 PreparedStatementLogger 和 StatementLogger 所調(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
類似,不贅述漆羔。