最近有个需求,记录框架spring+mybatis的项目的慢sql,想到了log4jdbc框架,log4jdbc只是对传统jdbc的一层封装,然后打印出sql执行日志和执行时间:
先简单介绍一下log4jdbc的使用:
第一步:引用log4jdbc的gradle配置,如果是maven的自己转换:
compile 'com.googlecode.log4jdbc:log4jdbc:1.2'
第二步:修改jdbc的driver和url:
mysql: datasource: driverClassName: net.sf.log4jdbc.DriverSpy url: jdbc:log4jdbc:mysql://localhost:3306/demo?useSSL=false&serverTimezone=UTC
就是这么简单,修改了这2个地方,就可以直接使用了,现在只要操作了数据库,日志便会打印出sql和sql的执行时间,以下面的这种格式:
然而具体的执行过程是怎么样的呢,我们有必要先阅读一下log4jdbc的源码,log4jdbc的源码包并不大,只有十几个类:
DriverSpy是对java.sql.Driver接口的实现,ConnectionSpy是对java.sql.Connection接口的实现,先来看看Class.forName(“net.sf.log4jdbc.DriverSpy”)时到底干了什么,先来看看DriverSpy的static静态代码块:
static { //log4jdbc初始化开始 log.debug("... log4jdbc initializing ..."); //从classpath下读取配置文件 InputStream propStream = DriverSpy.class.getResourceAsStream("/log4jdbc.properties"); Properties props = new Properties(System.getProperties()); if (propStream != null) { try { props.load(propStream); } catch (IOException e) { log.debug("ERROR! io exception loading " + "log4jdbc.properties from classpath: " + e.getMessage()); } finally { try { propStream.close(); } catch (IOException e) { log.debug("ERROR! io exception closing property file stream: " + e.getMessage()); } } log.debug(" log4jdbc.properties loaded from classpath"); } else { log.debug(" log4jdbc.properties not found on classpath"); } // look for additional driver specified in properties //加载dubug模式的前缀 DebugStackPrefix = getStringOption(props, "log4jdbc.debug.stack.prefix"); TraceFromApplication = DebugStackPrefix != null; //当SqlTimingWarnThresholdEnabled 为true时,SqlTimingWarnThresholdMsec 才有效,当sql执行时间大于 //SqlTimingWarnThresholdMsec 时间时,才打印warn日志 Long thresh = getLongOption(props, "log4jdbc.sqltiming.warn.threshold"); SqlTimingWarnThresholdEnabled = (thresh != null); if (SqlTimingWarnThresholdEnabled) { SqlTimingWarnThresholdMsec = thresh.longValue(); } thresh = getLongOption(props, "log4jdbc.sqltiming.error.threshold"); SqlTimingErrorThresholdEnabled = (thresh != null); if (SqlTimingErrorThresholdEnabled) { SqlTimingErrorThresholdMsec = thresh.longValue(); } DumpBooleanAsTrueFalse = getBooleanOption(props, "log4jdbc.dump.booleanastruefalse",false); DumpSqlMaxLineLength = getLongOption(props, "log4jdbc.dump.sql.maxlinelength", 90L).intValue(); DumpFullDebugStackTrace = getBooleanOption(props, "log4jdbc.dump.fulldebugstacktrace",false); StatementUsageWarn = getBooleanOption(props, "log4jdbc.statement.warn",false); //默认的打印所有类型的日志,也可以自己在配置文件中配置只打印某一种类型的日志,比如“select” DumpSqlSelect = getBooleanOption(props, "log4jdbc.dump.sql.select",true); DumpSqlInsert = getBooleanOption(props, "log4jdbc.dump.sql.insert",true); DumpSqlUpdate = getBooleanOption(props, "log4jdbc.dump.sql.update",true); DumpSqlDelete = getBooleanOption(props, "log4jdbc.dump.sql.delete",true); DumpSqlCreate = getBooleanOption(props, "log4jdbc.dump.sql.create",true); DumpSqlFilteringOn = !(DumpSqlSelect && DumpSqlInsert && DumpSqlUpdate && DumpSqlDelete && DumpSqlCreate); DumpSqlAddSemicolon = getBooleanOption(props, "log4jdbc.dump.sql.addsemicolon", false); //是否加载所有有名气的数据库驱动,默认是加载 AutoLoadPopularDrivers = getBooleanOption(props, "log4jdbc.auto.load.popular.drivers", true); TrimSql = getBooleanOption(props, "log4jdbc.trim.sql", true); TrimExtraBlankLinesInSql = getBooleanOption(props, "log4jdbc.trim.sql.extrablanklines", true); SuppressGetGeneratedKeysException = getBooleanOption(props, "log4jdbc.suppress.generated.keys.exception", false); // The Set of drivers that the log4jdbc driver will preload at instantiation // time. The driver can spy on any driver type, it's just a little bit // easier to configure log4jdbc if it's one of these types! Set subDrivers = new TreeSet(); if (AutoLoadPopularDrivers) { subDrivers.add("oracle.jdbc.driver.OracleDriver"); subDrivers.add("oracle.jdbc.OracleDriver"); subDrivers.add("com.sybase.jdbc2.jdbc.SybDriver"); subDrivers.add("net.sourceforge.jtds.jdbc.Driver"); // MS driver for Sql Server 2000 subDrivers.add("com.microsoft.jdbc.sqlserver.SQLServerDriver"); // MS driver for Sql Server 2005 subDrivers.add("com.microsoft.sqlserver.jdbc.SQLServerDriver"); subDrivers.add("weblogic.jdbc.sqlserver.SQLServerDriver"); subDrivers.add("com.informix.jdbc.IfxDriver"); subDrivers.add("org.apache.derby.jdbc.ClientDriver"); subDrivers.add("org.apache.derby.jdbc.EmbeddedDriver"); subDrivers.add("com.mysql.jdbc.Driver"); subDrivers.add("org.postgresql.Driver"); subDrivers.add("org.hsqldb.jdbcDriver"); subDrivers.add("org.h2.Driver"); } // look for additional driver specified in properties //也可以自己添加不常用的数据库驱动 String moreDrivers = getStringOption(props, "log4jdbc.drivers"); if (moreDrivers != null) { String[] moreDriversArr = moreDrivers.split(","); for (int i = 0; i < moreDriversArr.length; i++) { subDrivers.add(moreDriversArr[i]); log.debug (" will look for specific driver " + moreDriversArr[i]); } } try { DriverManager.registerDriver(new DriverSpy()); } catch (SQLException s) { // this exception should never be thrown, JDBC just defines it // for completeness throw (RuntimeException) new RuntimeException ("could not register log4jdbc driver!").initCause(s); } // instantiate all the supported drivers and remove // those not found String driverClass; for (Iterator i = subDrivers.iterator(); i.hasNext();) { driverClass = (String) i.next(); try { //加载所有驱动,我这里用的mysql,所有只会加载mysql的数据库驱动,所有说DriverSpy只是对传统的 //Driver的封装 Class.forName(driverClass); log.debug(" FOUND DRIVER " + driverClass); } catch (Throwable c) { i.remove(); } } if (subDrivers.size() == 0) { log.debug("WARNING! " + "log4jdbc couldn't find any underlying jdbc drivers."); } SqlServerRdbmsSpecifics sqlServer = new SqlServerRdbmsSpecifics(); OracleRdbmsSpecifics oracle = new OracleRdbmsSpecifics(); MySqlRdbmsSpecifics mySql = new MySqlRdbmsSpecifics(); /** create lookup Map for specific rdbms formatters */ rdbmsSpecifics = new HashMap(); rdbmsSpecifics.put("oracle.jdbc.driver.OracleDriver", oracle); rdbmsSpecifics.put("oracle.jdbc.OracleDriver", oracle); rdbmsSpecifics.put("net.sourceforge.jtds.jdbc.Driver", sqlServer); rdbmsSpecifics.put("com.microsoft.jdbc.sqlserver.SQLServerDriver", sqlServer); rdbmsSpecifics.put("weblogic.jdbc.sqlserver.SQLServerDriver", sqlServer); rdbmsSpecifics.put("com.mysql.jdbc.Driver", mySql); log.debug("... log4jdbc initialized! ..."); } 注册驱动的初始化基本结束了
接下来我们来看看项目运行的时候是怎么执行打印sql的:
项目使用的是mybatis,所以我们从SimpleExecutor类的doQuery方法作为入口来看程序的执行:
SimpleExecutor:
@Override public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException { Statement stmt = null; try { Configuration configuration = ms.getConfiguration(); //这里是一个RoutingStatementHandler,通过它的构造方法可知delegate=PreparedStatementHandler StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql); stmt = prepareStatement(handler, ms.getStatementLog()); return handler.<E>query(stmt, resultHandler); } finally { closeStatement(stmt); } } 我们先进去prepareStatement看看Connection是怎么得到的:
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException { Statement stmt; Connection connection = getConnection(statementLog); stmt = handler.prepare(connection, transaction.getTimeout()); handler.parameterize(stmt); return stmt; }
BaseExecutor protected Connection getConnection(Log statementLog) throws SQLException { Connection connection = transaction.getConnection(); if (statementLog.isDebugEnabled()) { return ConnectionLogger.newInstance(connection, statementLog, queryStack); } else { return connection; } } SpringManagedTransaction
@Override public Connection getConnection() throws SQLException { if (this.connection == null) { openConnection(); } return this.connection; }
private void openConnection() throws SQLException { //这里的datasource就是配置文件里面配置的dataSource this.connection = DataSourceUtils.getConnection(this.dataSource); this.autoCommit = this.connection.getAutoCommit(); this.isConnectionTransactional = DataSourceUtils.isConnectionTransactional(this.connection, this.dataSource); if (LOGGER.isDebugEnabled()) { LOGGER.debug( "JDBC Connection [" + this.connection + "] will" + (this.isConnectionTransactional ? " " : " not ") + "be managed by Spring"); } }
DataSourceUtils
public static Connection getConnection(DataSource dataSource)
throws CannotGetJdbcConnectionException {
try {
return
doGetConnection(dataSource); }
catch (SQLException ex) {
throw new CannotGetJdbcConnectionException(
"Failed to obtain JDBC Connection", ex); }
catch (IllegalStateException ex) {
throw new CannotGetJdbcConnectionException(
"Failed to obtain JDBC Connection: " + ex.getMessage()); }}
public static Connection doGetConnection(DataSource dataSource) throws SQLException { Assert.notNull(dataSource, "No DataSource specified"); ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource); if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) { conHolder.requested(); if (!conHolder.hasConnection()) { logger.debug("Fetching resumed JDBC Connection from DataSource"); conHolder.setConnection(fetchConnection(dataSource)); } return conHolder.getConnection(); } // Else we either got no holder or an empty thread-bound holder here. logger.debug("Fetching JDBC Connection from DataSource"); //获取Connection的入口 Connection con = fetchConnection(dataSource); if (TransactionSynchronizationManager.isSynchronizationActive()) { logger.debug("Registering transaction synchronization for JDBC Connection"); // Use same Connection for further JDBC actions within the transaction. // Thread-bound object will get removed by synchronization at transaction completion. ConnectionHolder holderToUse = conHolder; if (holderToUse == null) { holderToUse = new ConnectionHolder(con); } else { holderToUse.setConnection(con); } holderToUse.requested(); TransactionSynchronizationManager.registerSynchronization( new ConnectionSynchronization(holderToUse, dataSource)); holderToUse.setSynchronizedWithTransaction(true); if (holderToUse != conHolder) { TransactionSynchronizationManager.bindResource(dataSource, holderToUse); } } return con; }
private static Connection fetchConnection(DataSource dataSource) throws SQLException { Connection con = dataSource.getConnection(); if (con == null) { throw new IllegalStateException("DataSource returned null from getConnection(): " + dataSource); } return con; }
AbstractDriverBasedDataSource
public Connection getConnection()
throws SQLException {
return getConnectionFromDriver(getUsername(), getPassword());}
protected Connection getConnectionFromDriver(@Nullable String username, @Nullable String password) throws SQLException { Properties mergedProps = new Properties(); Properties connProps = getConnectionProperties(); if (connProps != null) { mergedProps.putAll(connProps); } if (username != null) { mergedProps.setProperty("user", username); } if (password != null) { mergedProps.setProperty("password", password); } //获取Connection Connection con = getConnectionFromDriver(mergedProps); if (this.catalog != null) { con.setCatalog(this.catalog); } if (this.schema != null) { con.setSchema(this.schema); } return con; }
DriverManagerDataSource
@Override protected Connection getConnectionFromDriver(Properties props) throws SQLException { String url = getUrl(); Assert. state(url != null, "'url' not set"); if ( logger.isDebugEnabled()) { logger.debug( "Creating new JDBC DriverManager Connection to [" + url + "]"); } return getConnectionFromDriverManager(url, props);}
DriverManager
@CallerSensitive public static Connection getConnection(String url, java.util.Properties info) throws SQLException { return (getConnection(url, info, Reflection.getCallerClass())); }
private static Connection getConnection( String url, java.util.Properties info, Class<?> caller) throws SQLException { /* * When callerCl is null, we should check the application's * (which is invoking this class indirectly) * classloader, so that the JDBC driver class outside rt.jar * can be loaded from here. */ ClassLoader callerCL = caller != null ? caller.getClassLoader() : null; synchronized(DriverManager.class) { // synchronize loading of the correct classloader. if (callerCL == null) { callerCL = Thread.currentThread().getContextClassLoader(); } } if(url == null) { throw new SQLException("The url cannot be null", "08001"); } println("DriverManager.getConnection(\"" + url + "\")"); // Walk through the loaded registeredDrivers attempting to make a connection. // Remember the first exception that gets raised so we can reraise it. SQLException reason = null; //还记得这个参数吗,就是加载数据库驱动的时候初始化的 for(DriverInfo aDriver : registeredDrivers) { // If the caller does not have permission to load the driver then // skip it. if(isDriverAllowed(aDriver.driver, callerCL)) { try { println(" trying " + aDriver.driver.getClass().getName()); //这才是真正得到Connection的方法,这里调用的就是初始化set进去的DriverSpy的connect方法 Connection con = aDriver.driver.connect(url, info); if (con != null) { // Success! println("getConnection returning " + aDriver.driver.getClass().getName()); return (con); } } catch (SQLException ex) { if (reason == null) { reason = ex; } } } else { println(" skipping: " + aDriver.getClass().getName()); } } // if we got here nobody could connect. if (reason != null) { println("getConnection failed: " + reason); throw reason; } println("getConnection: no suitable driver found for "+ url); throw new SQLException("No suitable driver found for "+ url, "08001"); }
DriverSpy //现在我们知道了最终返回的就是ConnectionSpy对象,但是里面有一个真正的connection public Connection connect(String url, Properties info) throws SQLException { Driver d = getUnderlyingDriver(url); if (d == null) { return null; } // get actual URL that the real driver expects // (strip off "jdbc:log4" from url) url = url.substring(9); lastUnderlyingDriverRequested = d; Connection c = d.connect(url, info); if (c == null) { throw new SQLException("invalid or unknown driver url: " + url); } if (log.isJdbcLoggingEnabled()) { ConnectionSpy cspy = new ConnectionSpy(c); RdbmsSpecifics r = null; String dclass = d.getClass().getName(); if (dclass != null && dclass.length() > 0) { r = (RdbmsSpecifics) rdbmsSpecifics.get(dclass); } if (r == null) { r = defaultRdbmsSpecifics; } cspy.setRdbmsSpecifics(r); return cspy; } else { return c; } } 现在我们再回到得到Connection的入口方法:SimpleExecutor
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException { Statement stmt; Connection connection = getConnection(statementLog); stmt = handler.prepare(connection, transaction.getTimeout()); handler.parameterize(stmt); return stmt; }现在我们再来看看如何得到PreparedStatementSpy对象的:
BaseStatementHandler
public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException { ErrorContext.instance().sql(boundSql.getSql()); Statement statement = null; try { //初始化statement statement = instantiateStatement(connection); setStatementTimeout(statement, transactionTimeout); setFetchSize(statement); return statement; } catch (SQLException e) { closeStatement(statement); throw e; } catch (Exception e) { closeStatement(statement); throw new ExecutorException("Error preparing statement. Cause: " + e, e); } }
PreparedStatementHandler
@Override protected Statement instantiateStatement(Connection connection) throws SQLException { String sql = boundSql.getSql(); if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) { String[] keyColumnNames = mappedStatement.getKeyColumns(); if (keyColumnNames == null) { return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS); } else { return connection.prepareStatement(sql, keyColumnNames); } } else if (mappedStatement.getResultSetType() != null) { return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY); } else { //找到了,就在这里,这里的connection就是前面的ConnectionSpy对象,调用ConnectionSpy对象的
//PreparedStatementHandler方法:
return connection.prepareStatement(sql); }}
ConnectionSpy
public PreparedStatement prepareStatement(String sql) throws SQLException { String methodCall = "prepareStatement(" + sql + ")"; try { //这是真是的PreparedStatement 对象 PreparedStatement statement = realConnection.prepareStatement(sql); return (PreparedStatement) reportReturn(methodCall, new PreparedStatementSpy(sql, this, statement)); } catch (SQLException s) { reportException(methodCall, s, sql); throw s; } }这里返回的是PreparedStatementSpy对象,但是同样的里面有一个真实的PreparedStatement ;
在回到SimpleExecutor的doQuery方法:
@Override public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException { Statement stmt = null; try { Configuration configuration = ms.getConfiguration(); StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql); stmt = prepareStatement(handler, ms.getStatementLog()); //prepareStatement真正的执行 return handler.<E>query(stmt, resultHandler); } finally { closeStatement(stmt); } }
PreparedStatementHandler
@Override public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException { PreparedStatement ps = (PreparedStatement) statement; ps.execute(); return resultSetHandler.<E> handleResultSets(ps); }
PreparedStatementSpy
public boolean execute() throws SQLException { String methodCall = "execute()"; //拼装sql,就是将?去掉,换上对应的参数 String dumpedSql = dumpedSql(); //日志打印sql reportSql(dumpedSql, methodCall); long tstart = System.currentTimeMillis(); try { //真实的执行sql boolean result = realPreparedStatement.execute(); //打印sql以及sql执行时间 reportSqlTiming(System.currentTimeMillis() - tstart, dumpedSql, methodCall); return reportReturn(methodCall, result); } catch (SQLException s) { reportException(methodCall, s, dumpedSql, System.currentTimeMillis() - tstart); throw s; } }
一直跟下去,跟到Slf4jSpyLogDelegator,这个类就是根据配置文件来打印sql以及sql执行时间的日志:
public void sqlTimingOccured(Spy spy, long execTime, String methodCall, String sql) { if (sqlTimingLogger.isErrorEnabled() && (!DriverSpy.DumpSqlFilteringOn || shouldSqlBeLogged(sql))) { if (DriverSpy.SqlTimingErrorThresholdEnabled && execTime >= DriverSpy.SqlTimingErrorThresholdMsec) { sqlTimingLogger.error( buildSqlTimingDump(spy, execTime, methodCall, sql, sqlTimingLogger.isDebugEnabled())); } else if (sqlTimingLogger.isWarnEnabled()) { if (DriverSpy.SqlTimingWarnThresholdEnabled && execTime >= DriverSpy.SqlTimingWarnThresholdMsec) { sqlTimingLogger.warn( buildSqlTimingDump(spy, execTime, methodCall, sql, sqlTimingLogger.isDebugEnabled())); } else if (sqlTimingLogger.isDebugEnabled()) { sqlTimingLogger.debug( buildSqlTimingDump(spy, execTime, methodCall, sql, true)); } else if (sqlTimingLogger.isInfoEnabled()) { sqlTimingLogger.info( buildSqlTimingDump(spy, execTime, methodCall, sql, false)); } } } }
private String buildSqlTimingDump(Spy spy, long execTime, String methodCall, String sql, boolean debugInfo) { StringBuffer out = new StringBuffer(); if (debugInfo) { out.append(getDebugInfo()); out.append(nl); out.append(spy.getConnectionNumber()); out.append(". "); } // NOTE: if both sql dump and sql timing dump are on, the processSql // algorithm will run TWICE once at the beginning and once at the end // this is not very efficient but usually // only one or the other dump should be on and not both. sql = processSql(sql); out.append(sql); out.append(" {executed in "); out.append(execTime); out.append(" msec}");
return out.toString();
}
到这里,一切真相大白!