mybatis统计每条SQL的执行时间的方法示例
背景
最近面试经常被问到关于数据库的事务的问题,可能平时我就知道加个注解@Transactional之后就一脸懵逼的。现在发现这一块真的是常常被忽略了,然而面试官就是最喜欢这种看是不常用,但是非常重要的问题,进而达到出其不意攻其不备。不吹水了,开始正文。
方案一:切面编程@Aspect
此方案主要是通过环绕切面的方式将mapper包下的接口方法,然后前后计算时间差即可。这就是典型的AOP知识,不过这种计算比较粗糙,但是也是个办法。具体方法如下:
?1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | @Aspect @Component @Slf4j public class MapperAspect { @AfterReturning ( "execution(* cn.xbmchina.mybatissqltime.mapper.*Mapper.*(..))" ) public void logServiceAccess(JoinPoint joinPoint) { log.info( "Completed: " + joinPoint); } /** * 监控cn.xbmchina.mybatissqltime.mapper..*Mapper包及其子包的所有public方法 */ @Pointcut ( "execution(* cn.xbmchina.mybatissqltime.mapper.*Mapper.*(..))" ) private void pointCutMethod() { } /** * 声明环绕通知 * * @param pjp * @return * @throws Throwable */ @Around ( "pointCutMethod()" ) public Object doAround(ProceedingJoinPoint pjp) throws Throwable { long begin = System.nanoTime(); Object obj = pjp.proceed(); long end = System.nanoTime(); log.info( "调用Mapper方法:{},参数:{},执行耗时:{}纳秒,耗时:{}毫秒" , pjp.getSignature().toString(), Arrays.toString(pjp.getArgs()), (end - begin), (end - begin) / 1000000 ); return obj; } } |
方案二:mybatis 的插件
MyBatis在四大对象的创建过程中,都会有插件进行介入。插件可以利用动态代理机制一层层的包装目标对象,而实现在目标对象执行目标方法之前进行拦截的效果。
MyBatis 允许在已映射语句执行过程中的某一点进行拦截调用。
默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:
①Executor(update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
②ParameterHandler(getParameterObject, setParameters)
③ResultSetHandler(handleResultSets, handleOutputParameters)
④StatementHandler(prepare, parameterize, batch, update, query)
下面是代码:
?1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 | import org.apache.ibatis.executor.statement.StatementHandler; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.mapping.ParameterMapping; import org.apache.ibatis.plugin.Interceptor; import org.apache.ibatis.plugin.Intercepts; import org.apache.ibatis.plugin.Invocation; import org.apache.ibatis.plugin.Plugin; import org.apache.ibatis.plugin.Signature; import org.apache.ibatis.session.ResultHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import java.sql.Statement; import java.util.List; import java.util.Properties; /** * Sql执行时间记录拦截器 * * @author zero * 2019年12月13日17:05:28 */ @Intercepts ({ @Signature (type = StatementHandler. class , method = "query" , args = {Statement. class , ResultHandler. class }), @Signature (type = StatementHandler. class , method = "update" , args = {Statement. class }), @Signature (type = StatementHandler. class , method = "batch" , args = {Statement. class })}) @Component public class SqlExecuteTimeCountInterceptor implements Interceptor { private static Logger logger = LoggerFactory.getLogger(SqlExecuteTimeCountInterceptor. class ); /** * 打印的参数字符串的最大长度 */ private final static int MAX_PARAM_LENGTH = 50 ; /** * 记录的最大SQL长度 */ private final static int MAX_SQL_LENGTH = 200 ; @Override public Object intercept(Invocation invocation) throws Throwable { Object target = invocation.getTarget(); long startTime = System.currentTimeMillis(); StatementHandler statementHandler = (StatementHandler) target; try { return invocation.proceed(); } finally { long endTime = System.currentTimeMillis(); long timeCount = endTime - startTime; BoundSql boundSql = statementHandler.getBoundSql(); String sql = boundSql.getSql(); Object parameterObject = boundSql.getParameterObject(); List<ParameterMapping> parameterMappingList = boundSql.getParameterMappings(); // 格式化Sql语句,去除换行符,替换参数 sql = formatSQL(sql, parameterObject, parameterMappingList); logger.info( "执行 SQL:[ , {} ]执行耗时[ {} ms]" , sql, timeCount); } } /** * 格式化/美化 SQL语句 * * @param sql sql 语句 * @param parameterObject 参数的Map * @param parameterMappingList 参数的List * @return 格式化之后的SQL */ private String formatSQL(String sql, Object parameterObject, List<ParameterMapping> parameterMappingList) { // 输入sql字符串空判断 if (sql == null || sql.length() == 0 ) { return "" ; } // 美化sql sql = beautifySql(sql); // 不传参数的场景,直接把sql美化一下返回出去 if (parameterObject == null || parameterMappingList == null || parameterMappingList.size() == 0 ) { return sql; } return LimitSQLLength(sql); } /** * 返回限制长度之后的SQL语句 * * * @param sql 原始SQL语句 */ private String LimitSQLLength(String sql) { if (sql == null || sql.length() == 0 ) { return "" ; } if (sql.length() > MAX_SQL_LENGTH) { return sql.substring( 0 , MAX_SQL_LENGTH); } else { return sql; } } @Override public Object plugin(Object target) { return Plugin.wrap(target, this ); } @Override public void setProperties(Properties properties) { } /** * 替换SQL 中? 所对应的值, 只保留前50个字符 * * @param sql sql语句 * @param valueOf ?对应的值 */ private String replaceValue(String sql, String valueOf) { //超过50个字符只取前50个 if (valueOf != null && valueOf.length() > MAX_PARAM_LENGTH) { valueOf = valueOf.substring( 0 , MAX_PARAM_LENGTH); } sql = sql.replaceFirst( "\\?" , valueOf); return sql; } /** * 美化sql * * @param sql sql语句 */ private String beautifySql(String sql) { sql = sql.replaceAll( "[\\s\n ]+" , " " ); return sql; } } |
方案三:直接用druid
这种就是我们平时用的最多的,但是面试的话说一下就得了,估计也没有怎么好问的了。
Springboot+druid的配置application.yml文件如下:
?1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | spring: datasource: url: jdbc:mysql://localhost:3306/testdb1?characterEncoding=utf-8&useUnicode=true&useSSL=false&serverTimezone=UTC driver-class-name: com.mysql.jdbc.Driver # mysql8.0以前使用com.mysql.jdbc.Driver username: root password: root platform: mysql #通过这句配置将druid连接池引入到我们的配置中,spring会尽可能判断类型是什么,然后根据情况去匹配驱动类。 type: com.alibaba.druid.pool.DruidDataSource druid: initial-size: 5 # 初始化大小 min-idle: 5 # 最小 max-active: 100 # 最大 max-wait: 60000 # 配置获取连接等待超时的时间 time-between-eviction-runs-millis: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 min-evictable-idle-time-millis: 300000 # 指定一个空闲连接最少空闲多久后可被清除,单位是毫秒 validationQuery: select 'x' test-while-idle: true # 当连接空闲时,是否执行连接测试 test-on-borrow: false # 当从连接池借用连接时,是否测试该连接 test-on-return: false # 在连接归还到连接池时是否测试该连接 filters: config,wall,stat # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙 poolPreparedStatements: true # 打开PSCache,并且指定每个连接上PSCache的大小 maxPoolPreparedStatementPerConnectionSize: 20 maxOpenPreparedStatements: 20 # 通过connectProperties属性来打开mergeSql功能;慢SQL记录 connectionProperties: druid.stat.slowSqlMillis=200;druid.stat.logSlowSql=true;config.decrypt=false # 合并多个DruidDataSource的监控数据 #use-global-data-source-stat: true #WebStatFilter配置,说明请参考Druid Wiki,配置_配置WebStatFilter web-stat-filter: enabled: true #是否启用StatFilter默认值true url-pattern: /* exclusions: /druid/*,*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico session-stat-enable: true session-stat-max-count: 10 #StatViewServlet配置,说明请参考Druid Wiki,配置_StatViewServlet配置 stat-view-servlet: enabled: true #是否启用StatViewServlet默认值true url-pattern: /druid/* reset-enable: true login-username: admin login-password: admin |
总结
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持服务器之家。
原文链接:https://segmentfault.com/a/1190000021456684
1.本站遵循行业规范,任何转载的稿件都会明确标注作者和来源;2.本站的原创文章,请转载时务必注明文章作者和来源,不尊重原创的行为我们将追究责任;3.作者投稿可能会经我们编辑修改或补充。