目录
前言
在实际开发中,所有的
sql
语句参数都是带占位符的,但在本地开发时,为了方便测试追踪问题,经常需要查看具体执行的SQL语句内容,因此每次只能靠自己进行二次处理,耗时耗力,尤其是访问量大的情况下,很容易混淆,所以需要一个方法能够直接输出可以直接使用的SQL语句(将占位符替换成实际的参数),而
Druid
就支持此功能(Druid的主要功能可不是这个),所以此文主要讲如何在项目中使用
Druid
输出可执行SQL。
项目环境
因为是自己写的测试项目所以用到的版本都比较高,低版本的可能略有不同。
本项目使用的环境是:
Spring Boot 3.1
JDK17
Druid 1.2.18
Postgresql 42.6.0
pom.xml文件如下:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.2.18</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><scope>provided</scope></dependency><dependency><groupId>org.postgresql</groupId><artifactId>postgresql</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-dependencies</artifactId><version>3.1.1</version><type>pom</type><scope>import</scope></dependency>
项目配置
- 使用
Spring Boot
默认的日志logback
,可以在classpath下新建application.yml
、application.properties
或自定义logback.xml文件告诉Spring Boot
我们哪些地方需要打印日志;
yml配置如下:
logging:level:druid.sql.Statement: debug
spring:datasource:type: com.alibaba.druid.pool.DruidDataSource
druid:url: jdbc:postgresql://127.0.0.1:5432/数据库名称
username: 用户名
password: 密码
driver-class-name: org.postgresql.Driver
filter:slf4j:enabled:true# 只有当 isStatementExecutableSqlLogEnable() isStatementLogEnabled() 都为ture的情况才打印 可执行sql# 在{# LogFilter#logExecutableSql }使用statement-executable-sql-log-enable:true# 在{# LogFilter#logExecutableSql }使用statement-log-enabled:truestatement-create-after-log-enabled:falsestatement-log-error-enabled:trueresult-set-log-enabled:false#statementPrepareAfterLogEnable# 准备好的sql语句打印(此时为执行前)未进行参数拼接statement-prepare-after-log-enabled:false#isStatementParameterSetLogEnabled#打印参数statement-parameter-set-log-enabled:false#statementExecuteAfterLogEnable#sql语句执行完成后打印(执行后)未进行参数拼接statement-execute-after-log-enabled:false#statementCloseAfterLogEnablestatement-close-after-log-enabled:false
properties配置如下:
logging.level.druid.sql.Statement=debug
spring.datasource.druid.filter.slf4j.enabled=true
#配置可执行sql语句日志打印
#只有当 isStatementExecutableSqlLogEnable() isStatementLogEnabled() 都为ture的情况才打印 可执行sql
#在{# LogFilter#logExecutableSql }使用
spring.datasource.druid.filter.slf4j.statement-executable-sql-log-enable=true
spring.datasource.druid.filter.slf4j.statement-log-enabled=true
spring.datasource.druid.filter.slf4j.statement-log-error-enabled=true
spring.datasource.druid.filter.slf4j.statement-create-after-log-enabled=false
spring.datasource.druid.filter.slf4j.result-set-log-enabled=false
spring.datasource.druid.filter.slf4j.statement-prepare-after-log-enabled=false
spring.datasource.druid.filter.slf4j.statement-parameter-set-log-enabled=false
spring.datasource.druid.filter.slf4j.statement-execute-after-log-enabled=false
spring.datasource.druid.filter.slf4j.statement-close-after-log-enabled=false
xml配置如下:
其它配置需要配置在yml、properties文件中
<loggername="druid.sql.Statement"level="DEBUG"additivity="false"><-换成自己的appender引用-><appender-refref="STDOUT"/></logger>
以上配置则可以输出可执行
SQL
,因为
Spring Boot
默认使用的是
Logback
,
druid
中没有logback的过滤器,
slf4j
是所有日志的门面,所以使用
slf4j
输出日志,如果需要使用用
log4j
只需要吧
slf4j
替换成
log4j
即可,不过还是推荐使用
slf4j
,至于为什么使用
slf4j
,百度很多人都已经说的很好了,另外
druid
的其他配置说明也请移步专业的
druid
文章,他们写的更加详细,另外还需要指定
druid
的日志登记为
debug
才能显示
- 默认情况下
Druid
是关闭输出可执行SQL的,需要将statement-executable-sql-log-enable
设置为true
,详细配置文件在:
publicabstractclassLogFilterextendsFilterEventAdapterimplementsLogFilterMBean{//druid准备了四种类型的日志输出//这些logger分别对应打印 datasource相关、connection相关、statement相关、resultSet相关的日志//这里我们只关注statementLoggerName是打印可执行sql的关键protectedString dataSourceLoggerName ="druid.sql.DataSource";protectedString connectionLoggerName ="druid.sql.Connection";protectedString statementLoggerName ="druid.sql.Statement";protectedString resultSetLoggerName ="druid.sql.ResultSet";//需设置为trueprivateboolean statementExecutableSqlLogEnable;}
总结
踩坑点:
- 一开始参考其它文章说是将
statement-log-enabled
设置为false导致半天没有看到打印的日志最后查看源码在LogFilter
#logExecutableSql
有如下代码片段:
//此方法就是打印可执行SQL的方法privatevoidlogExecutableSql(StatementProxy statement,String sql){//这里判断 StatementExecutableSqlLogEnable 和 StatementLogEnabled字段必须为tureif((!isStatementExecutableSqlLogEnable())||!isStatementLogEnabled()){return;}int parametersSize = statement.getParametersSize();if(parametersSize ==0){statementLog("{conn-"+ statement.getConnectionProxy().getId()+", "+stmtId(statement)+"} executed. "+ sql);return;}List<Object> parameters =newArrayList<Object>(parametersSize);for(int i =0; i < parametersSize;++i){JdbcParameter jdbcParam = statement.getParameter(i);
parameters.add(jdbcParam !=null? jdbcParam.getValue():null);}DbType dbType =DbType.of(statement.getConnectionProxy().getDirectDataSource().getDbType());String formattedSql =SQLUtils.format(sql, dbType, parameters,this.statementSqlFormatOption);//最终调用 statementLog 方法 打印可执行SQL语句statementLog("{conn-"+ statement.getConnectionProxy().getId()+", "+stmtId(statement)+"} executed. "+ formattedSql);}
Spring Boot
默认的日志框架为logback
,笔者在配置的时候一直没有找到logback
相关的配置信息,最后知道slf4j
是日志统一接口,logback
是slf4j
的实现,所以druid
的官方文档中没有logback
的配置。只有slf4j
的配置。
测试结果:
源码分析:
Druid的Filter提供了强大的扩展功能,如连接池监控(连接池配置信息、SQL执行、并发、慢查询、执行时间区间分布等,由StatFilter实现)、防止SQL注入(WallFilter)、连接池信息日志输出(LogFilter)等,这里我们只关注日志输出的Filter,其在Druid内置的实现如下:
可以看到在LogFilter的实现类有CommonsLogFilter、Log4j2Filter、Slf4jLogFilter、Log4jFilter。
介绍上面几个过滤器前需要说明:slf4j、log4j、log4j2、logback的区别,这不是本文的重点,请参考:带你深入Java Log框架,彻底搞懂Log4J、Log4J2、LogBack,SLF4J
CommonsLogFilter:由于本项目使用的是Logback,Log4j2相关jar包没有引全所以爆红,实际项目中如果需要使用这个过滤器,需要引入Log4j2相关依赖。
Log4j2Filter:如果项目使用的是Log4j2请引入相关依赖并修改配置文件为log4j2
Slf4jLogFilter:注意Druid中并没有类似LogbackFilter相关类,但Logback是Slf4j的实现所以后面追踪源码我们以这个进行展开说明。
Log4jFilter:由于2015年9月,Apache软件基金业宣布,Log4j不在维护,建议所有相关项目升级到Log4j2,因此这里不做太多说明。
- 过滤器初始化注册
- dataSource:这里可以看到实例的dataSource实现类为
DruidDataSourceWrapper
,其中filters属性就是存储我们所注册的过滤器,这里是上面配置的Slf4jLogFilterDruidDataSourceWrapper
的主要初始化代码如下,仅粘出核心代码
@ConfigurationProperties("spring.datasource.druid")publicclassDruidDataSourceWrapperextendsDruidDataSourceimplementsInitializingBean{@AutowiredprivateDataSourceProperties basicProperties;/**
* 这里可以看出
* spring.datasource.druid.username == spring.datasource.username
* password、url、deiverClassName等这两种方式都可以
**/@OverridepublicvoidafterPropertiesSet()throwsException{//if not found prefix 'spring.datasource.druid' jdbc properties ,'spring.datasource' prefix jdbc properties will be used.if(super.getUsername()==null){super.setUsername(basicProperties.determineUsername());}if(super.getPassword()==null){super.setPassword(basicProperties.determinePassword());}if(super.getUrl()==null){super.setUrl(basicProperties.determineUrl());}if(super.getDriverClassName()==null){super.setDriverClassName(basicProperties.getDriverClassName());}}//这里就是注册我们在配置文件中配置的过滤器了@Autowired(required =false)publicvoidautoAddFilters(List<Filter> filters){super.filters.addAll(filters);}}
- 调用过程
这里先贴出完整的调用过程:这里我们只关注Filter打印可执行SQL语句的出发时机(其它类似),我们以上图的查询为例,其它方法类似
com.alibaba.druid.pool.DruidDataSource#getConnection
com.alibaba.druid.filter.FilterChainImpl#dataSource_connect
com.alibaba.druid.pool.DruidPooledPreparedStatement#execute
com.alibaba.druid.proxy.jdbc.PreparedStatementProxyImpl#execute
com.alibaba.druid.filter.FilterChainImpl#preparedStatement_execute
com.alibaba.druid.filter.FilterEventAdapter#preparedStatement_execute
com.alibaba.druid.filter.logging.LogFilter#statementExecuteAfter
com.alibaba.druid.filter.logging.LogFilter#logExecutableSql
com.alibaba.druid.filter.logging.Slf4jLogFilter#statementLog
以上便是可执行SQL日志的调用链,我们debug进入
DruidDataSource#getConnection
protectedList<Filter> filters =newCopyOnWriteArrayList<Filter>();publicDruidPooledConnectiongetConnection(long maxWaitMillis)throwsSQLException{//防止connection对象为空,一般不会出现这种情况init();//我们注册的所有filter都在 filtersfinalint filtersSize = filters.size();if(filtersSize >0){//创建或者直接从缓存中获取 由druid提供了唯一实现的FilterChain过滤器链对象FilterChainImpl filterChain =createChain();try{//获取连接对象return filterChain.dataSource_connect(this, maxWaitMillis);}finally{recycleFilterChain(filterChain);}}else{returngetConnectionDirect(maxWaitMillis);}}
- druid首先判断是否初始化过connection对象,如果没有则进行初始化;
- 创建FilterChain对象,由FilterChain对象返回连接实际的连接对象。 接下来我们进入到
filterChain.dataSource_connect
publicDruidPooledConnectiondataSource_connect(DruidDataSource dataSource,long maxWaitMillis)throwsSQLException{if(this.pos < filterSize){//这里执行了获取连接的调用链(注意这里是迭代执行,有先后顺序) 如果connectionConnectAfterLogEnable为true且connectionLogEnabled为ture则会打印相关日志信息DruidPooledConnection conn =nextFilter().dataSource_getConnection(this, dataSource, maxWaitMillis);return conn;}return dataSource.getConnectionDirect(maxWaitMillis);}
上面就是在获取连接之前执行了一遍调用链,并不是我们关注的重点,接下来我们看
DruidPooledPreparedStatement#execute
publicbooleanexecute()throwsSQLException{//检测Statement是否打开checkOpen();//执行次数增量统计incrementExecuteCount();//事务相关处理transactionRecord(sql);//oracle特殊处理oracleSetRowPrefetch();
conn.beforeExecute();try{//真正的执行逻辑交给stmt对象return stmt.execute();}catch(Throwable t){errorCheck(t);throwcheckException(t);}finally{
conn.afterExecute();}}
接下来我们主要看stmt的实例
PreparedStatementProxyImpl#execute
做了什么
publicbooleanexecute()throwsSQLException{
updateCount =null;//最终的执行sql语句 带占位符的
lastExecuteSql = sql;//获取语句的执行类型 这里是:Execute// Execute, ExecuteBatch, ExecuteQuery, ExecuteUpdate
lastExecuteType =StatementExecuteType.Execute;
lastExecuteStartNano =-1L;
lastExecuteTimeNano =-1L;//前面已经创建过调用链了,这个直接从缓存中获取FilterChainImpl chain =createChain();//获取执行结果 我们重点分析
firstResultSet = chain.preparedStatement_execute(this);recycleFilterChain(chain);return firstResultSet;}
从上面可以看出,执行逻辑和前面获取连接类似,首先将当前实例交给FilterChain对象,FilterChain对象执行完调用链后再执行查询结果的逻辑,具体我们看
chain.preparedStatement_execute
publicbooleanpreparedStatement_execute(PreparedStatementProxy statement)throwsSQLException{if(this.pos < filterSize){//这里执行了获取连接的调用链(注意这里是迭代执行,有先后顺序)returnnextFilter().preparedStatement_execute(this, statement);}return statement.getRawObject().execute();}
上面执行逻辑和前面获取连接时候的逻辑基本一致,下面我们主要关注
nextFilter().preparedStatement_execute
,这个时候进去到Slf4jLogFilter的祖父类FilterEventAdapter对象的
preparedStatement_execute
方法
publicbooleanpreparedStatement_execute(FilterChain chain,PreparedStatementProxy statement)throwsSQLException{try{//statement 执行前操作statementExecuteBefore(statement, statement.getSql());//调用其它filter的preparedStatement_execute方法boolean firstResult = chain.preparedStatement_execute(statement);//statement 执行后操作 这里是打印可执行sql的方法this.statementExecuteAfter(statement, statement.getSql(), firstResult);return firstResult;}catch(SQLException error){statement_executeErrorAfter(statement, statement.getSql(), error);throw error;}catch(RuntimeException error){statement_executeErrorAfter(statement, statement.getSql(), error);throw error;}catch(Error error){statement_executeErrorAfter(statement, statement.getSql(), error);throw error;}}
接下来我们进入
FilterEventAdapter
对象的子类
LogFilter#statementExecuteAfter
,然后进入
logExecutableSql
方法
protectedvoidstatementExecuteAfter(StatementProxy statement,String sql,boolean firstResult){//这里是调用打印可执行sqllogExecutableSql(statement, sql);// 是否打印执行后的日志if(statementExecuteAfterLogEnable &&isStatementLogEnabled()){
statement.setLastExecuteTimeNano();double nanos = statement.getLastExecuteTimeNano();double millis = nanos /(1000*1000);statementLog("{conn-"+ statement.getConnectionProxy().getId()+", "+stmtId(statement)+"} executed. "+ millis +" millis. "+ sql);}}privatevoidlogExecutableSql(StatementProxy statement,String sql){//只有当 StatementExecutableSqlLogEnable 和 StatementLogEnabled 都为true才进行打印//这也就是前面的配置if((!isStatementExecutableSqlLogEnable())||!isStatementLogEnabled()){return;}int parametersSize = statement.getParametersSize();if(parametersSize ==0){statementLog("{conn-"+ statement.getConnectionProxy().getId()+", "+stmtId(statement)+"} executed. "+ sql);return;}List<Object> parameters =newArrayList<Object>(parametersSize);// 获取参数for(int i =0; i < parametersSize;++i){JdbcParameter jdbcParam = statement.getParameter(i);
parameters.add(jdbcParam !=null? jdbcParam.getValue():null);}DbType dbType =DbType.of(statement.getConnectionProxy().getDirectDataSource().getDbType());// 参数拼接String formattedSql =SQLUtils.format(sql, dbType, parameters,this.statementSqlFormatOption);// 真正的打印输出交给子类statementLog("{conn-"+ statement.getConnectionProxy().getId()+", "+stmtId(statement)+"} executed. "+ formattedSql);}
最后会调用子类
Slf4jLogFilter#statementLog
的方法
protectedString statementLoggerName ="druid.sql.Statement";privateLogger statementLogger =LoggerFactory.getLogger(statementLoggerName);protectedvoidstatementLog(String message){//注意这里是debug 且日志打印地址是 “druid.sql.Statement”
statementLogger.debug(message);}
至此可执行SQL的执行流程就全部完毕了。
最后打印的sql语句如下
有什么不对的地方请大家指正,谢谢。
附上druid相关链接:
常见问题解决(官方)
Spring boot 整合 druid 配置链接(官方)
版权归原作者 孙陆泉 所有, 如有侵权,请联系我们删除。