曹工杂谈--使用mybatis的同学,进来看看怎么在日志打印完整sql吧,在数据库可执行那种
前言
今天新年第一天,给大家拜个年,祝大家新的一年里,技术突突突,头发长长长!
咱们搞技术的,比较直接,那就开始吧。我给大家看看我demo工程的效果(代码下边会给大家的):
技术栈是mybatis/mybatis plus
,spring boot
,日志是logback
。
其实这个痛点吧,我是一直有的,测试或者开发时,日志里每次打印的都是带?的sql,然后还得自己手动一个参数一个参数地贴过去,这真是一个体力活。虽然是体力活,还是做了这么多年了,这次,终于决定不忍了。
在弄这个之前呢,我知道idea里有个插件可以实现这个功能,mybatis-log-plugin
,但我这边idea一直用不起,具体原因不明,反正就是完整sql打印不出来。
然后我刚搜了下,mybatis plus也支持,加下面这样一行配置即可:
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
但我注意到,这个是打印到控制台的,我试了下,效果如下:
我觉得,这样挺好的,但是有优化空间:
- console打印,不适用于开发环境和测试环境;本地调试还不错;
- 本地调试时,一般我只挂起当前线程,如果请求多了,这里的打印会很乱;分不清哪个日志是我这个请求的,而不是其他线程打印的
我自己这个项目也用的mybatis-plus
,因此,我最终配置是下面这样:
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl
使用slf4j
打印,而不是console
直接print
。但这依然没有解决:拼装完整sql,并打印到日志的需求。
大体思路
因为是自己瞎摸索出来的方案,不保证是最优的,只能说:it works。
大家再看看,正常情况下,是会打印下面这样的sql的(mybatis默认支持):
[http-nio-8083-exec-1] DEBUG c.e.w.mapper.AppealDisposalOnOffMapper.selectList
- ==> Preparing: SELECT appeal_disposal_on_off_id,disposal_on_off_status,appeal_on_off_status,user_id FROM appeal_disposal_on_off WHERE (disposal_on_off_status = ?) [BaseJdbcLogger.java:143]
[http-nio-8083-exec-1] DEBUG c.e.w.mapper.AppealDisposalOnOffMapper.selectList
- ==> Parameters: 0(Integer) [BaseJdbcLogger.java:143]
即,默认打印出:一行preparedStatement
的语句,带?;下一行就是对应的参数。
我的方案是,对logger
进行动态代理,当调用logger.info/debug/...
的时候,拦截之。
拦截后的逻辑,如下:
- 当打印的语句,以
==> Preparing:
开头时,将当前语句存放到线程局部变量中,假设为A; - 当打印的语句,以
==> Parameters:
开头时,将当前线程局部变量中的A拿出来,和当前语句一起,拼成一个完整的sql,然后调用当前方法(记住,我们动态代理了logger.info等方法)打印之。
画图解决:
上面的逻辑图,大家看着没问题吧,其实问题的关键变成了,怎么去生成这个logger的动态代理,且最重要的是,你生成的动态代理对象要怎么生效。
具体实现分析
要讲解这部分,我们只能切入细节了,毕竟我们得找到一个切入点,去使用我们的动态代理logger。
大家应该记得,我们平时使用slf4j时,生成logger是不是下面这样写(现在虽然用lombok了,本质没变):
private static final Logger logger = LoggerFactory.getLogger(A.class);
public static Logger getLogger(String name) {
ILoggerFactory iLoggerFactory = getILoggerFactory();
return iLoggerFactory.getLogger(name);
}
这一行里,getILoggerFactory
就要去获取classpath中绑定的日志实现了,具体的过程,我在另一篇里也有讲:
曹工改bug--这次,我遇到了一个难缠的栈溢出bug,还是日志相关的,真的难
因为我们用的logback,所以这里会进入到logback包内的(包名怎么是slf4j的?没错,这就是slf4j-api怎么去找实现类的核心,类似java的SPI机制,具体看上面的博文):
logback-classic包内的:
org.slf4j.impl.StaticLoggerBinder#getSingleton
public static StaticLoggerBinder getSingleton() {
return SINGLETON;
}
进入上面代码前,会先执行静态代码:
private static StaticLoggerBinder SINGLETON = new StaticLoggerBinder();
static {
SINGLETON.init();
}
上面的静态代码块中,进行初始化:
void init() {
try {
new ContextInitializer(defaultLoggerContext).autoConfig();
} catch (JoranException je) {
Util.report("Failed to auto configure default logger context", je);
}
//核心代码
contextSelectorBinder.init(defaultLoggerContext, KEY);
initialized = true;
}
ch.qos.logback.classic.util.ContextSelectorStaticBinder#init
public void init(LoggerContext defaultLoggerContext, Object key) {
if (this.key == null) {
this.key = key;
}
// 这个contextSelector很重要,loggerFactory就是调用它的方法来生成
String contextSelectorStr = OptionHelper.getSystemProperty(ClassicConstants.LOGBACK_CONTEXT_SELECTOR);
if (contextSelectorStr == null) {
contextSelector = new DefaultContextSelector(defaultLoggerContext);
} else if (contextSelectorStr.equals("JNDI")) {
contextSelector = new ContextJNDISelector(defaultLoggerContext);
} else {
contextSelector = dynamicalContextSelector(defaultLoggerContext, contextSelectorStr);
}
}
经过我多方调试,发现这里的contextSelector
,发现它很关键。它是个接口,方法如下:
/**
* An interface that provides access to different contexts.
*
* It is used by the LoggerFactory to access the context
* it will use to retrieve loggers.
*
* @author Ceki Gülcü
* @author Sébastien Pennec
*/
public interface ContextSelector {
// 获取LoggerContext,这个LoggerContext其实就是LoggerFactory
LoggerContext getLoggerContext();
LoggerContext getLoggerContext(String name);
LoggerContext getDefaultLoggerContext();
LoggerContext detachLoggerContext(String loggerContextName);
List<String> getContextNames();
}
大家注意,这个类的方法,LoggerContext getLoggerContext();
,返回值是LoggerContext
,这个返回值类型比较牛逼,因为它其实就是LoggerFactory
。
public class LoggerContext extends ContextBase implements ILoggerFactory, LifeCycle
大家看到了,这个LoggerContext
实现了ILoggerFactory
:
public interface ILoggerFactory {
// 这个东西,大家熟悉了噻,logger工厂啊
public Logger getLogger(String name);
}
综上分析,我们要换Logger,可能没那么容易,因为Logger,是ILoggerFactory
调用getLogger
获得的。
那么,我们只能把原始的ILoggerFactory
(假设为A)给它换了,生成一个ILoggerFactory
的动态代理(假A),保证每次调用A的getLogger
时,就会被假A拦截。然后我们在拦截的逻辑中,先使用A获取到原始logger,然后生成对原始logger进行动态代理的logger。
所以,现在完整的逻辑是这样:
问题,现在就变成了,怎么去生成org.slf4j.ILoggerFactory
的动态代理,因为我们需要这个原始的factory,不然我们作为动态代理,自己也不知道怎么去生成Logger。
前面大家也看到了,
LoggerContext
满足要求,那我们只要在能拿到LoggerContext
的地方,处理下就行了。
能拿到LoggerContext
的地方,就是ContextSelector
。
大家回头再看看之前那段代码:
public void init(LoggerContext defaultLoggerContext, Object key) throws ClassNotFoundException, NoSuchMethodException, InstantiationException,
IllegalAccessException, InvocationTargetException {
if (this.key == null) {
this.key = key;
}
//扩展点就在这里了,这里会去取环境变量,如果取不到,就用默认的,取到了,就用环境变量里的类
String contextSelectorStr = OptionHelper.getSystemProperty(ClassicConstants.LOGBACK_CONTEXT_SELECTOR);
if (contextSelectorStr == null) {
A: contextSelector = new DefaultContextSelector(defaultLoggerContext);
} else if (contextSelectorStr.equals("JNDI")) {
B: contextSelector = new ContextJNDISelector(defaultLoggerContext);
} else {
C: contextSelector = dynamicalContextSelector(defaultLoggerContext, contextSelectorStr);
}
}
这里就是扩展点,我们自己设置一个环境变量ClassicConstants.LOGBACK_CONTEXT_SELECTOR
,就不会走A逻辑,而是走上面的C逻辑。具体的里面很简单,就是根据环境变量的值,去new一个对应的contextSelector
。
具体实现步骤1--指定环境变量
@SpringBootApplication
@MapperScan("com.example.webdemo.mapper")
public class WebDemoApplicationUsingMybatisPlus {
private static Logger log= null;
static {
// 这里设置环境变量,指向我们自定义的class System.setProperty(ClassicConstants.LOGBACK_CONTEXT_SELECTOR,"com.example.webdemo.util.CustomDefaultContextSelector");
log = LoggerFactory.getLogger(WebDemoApplicationUsingMybatisPlus.class);
}
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(WebDemoApplicationUsingMybatisPlus.class, args);
}
}
具体实现步骤2--实现自定义的context-selector
package com.example.webdemo.util;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.selector.ContextSelector;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
public class CustomDefaultContextSelector implements ContextSelector, MethodInterceptor {
private LoggerContext defaultLoggerContext;
private LoggerContext proxyedDefaultLoggerContext;
private static ConcurrentHashMap<String, org.slf4j.Logger> cachedLogger = new ConcurrentHashMap<>(1000);
public CustomDefaultContextSelector(LoggerContext context) {
//1:原始的LoggerContext,框架会传进来
this.defaultLoggerContext = context;
}
@Override
public LoggerContext getLoggerContext() {
return getDefaultLoggerContext();
}
@Override
public LoggerContext getDefaultLoggerContext() {
if (proxyedDefaultLoggerContext == null) {
//2:我们这里,将原始的LogegrContext进行代理,这里返回代理过的对象,完成偷天换日的效果,callback就设为自己
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(defaultLoggerContext.getClass());
enhancer.setCallback(this);
proxyedDefaultLoggerContext = (LoggerContext) enhancer.create();
}
return proxyedDefaultLoggerContext;
}
@Override
public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
Object result;
result = methodProxy.invokeSuper(o,args);
//3:当原始的LoggerContext的getLogger被调用时,生成一个动态代理的Logger,会组装sql日志那种
if (Objects.equals(method.getReturnType().getName(), org.slf4j.Logger.class.getName()) && Objects.equals(method.getName(), "getLogger")) {
org.slf4j.Logger logger = (org.slf4j.Logger) result;
String loggerName = logger.getName();
/**
* 只关心mybatis层的logger,mybatis层的logger的包名,我们这边是固定的包下面
* 如果不是这个包下的,直接返回
*/
if (!loggerName.startsWith("com.example.webdemo.mapper")) {
return result;
}
/**
* 对mybatis mapper的log,需要进行代理;代理后的对象,我们暂存一下,免得每次都创建代理对象
* 从缓存获取代理logger
*/
if (cachedLogger.get(loggerName) != null) {
return cachedLogger.get(loggerName);
}
CustomLoggerInterceptor customLoggerInterceptor = new CustomLoggerInterceptor();
customLoggerInterceptor.setLogger((Logger) result);
Object newProxyInstance = Proxy.newProxyInstance(result.getClass().getClassLoader(), result.getClass().getInterfaces(), customLoggerInterceptor);
cachedLogger.put(loggerName, (org.slf4j.Logger) newProxyInstance);
return newProxyInstance;
}
return result;
}
}
这里做了一点优化,将代理Logger进行了缓存,同名的logger只会有一个。
具体实现步骤3--logger的动态代理的逻辑
//摘录了一部分,因为处理字符串比较麻烦,所以代码多一点,这里就不贴出来了,大家自己去clone哈
private String assemblyCompleteMybatisQueryLog(Object[] args) {
if (args != null && args.length > 1) {
if (!(args[0] instanceof BasicMarker)) {
return null;
}
/**
* marker不匹配,直接返回
*/
BasicMarker arg = (BasicMarker) args[0];
if (!Objects.equals(arg.getName(), "MYBATIS")) {
return null;
}
String message = null;
for (int i = (args.length - 1); i >= 0 ; i--) {
if (args[i] != null && args[i] instanceof String) {
message = (String) args[i];
break;
}
}
if (message == null) {
return null;
}
// 这里就是判断当前打印的sql是啥,进行对应的处理
if (message.startsWith("==> Preparing:")) {
String newMessage = message.substring("==> Preparing:".length()).trim();
SQL_LOG_VO_THREAD_LOCAL.get().setPrepareSqlStr(newMessage);
} else if (message.startsWith("==> Parameters:")) {
try {
return populateSqlWithParams(message);
} catch (Exception e) {
logger.error("{}",e);
}finally {
SQL_LOG_VO_THREAD_LOCAL.remove();
}
}
}
return null;
}
总结
源码地址奉上,大家deug一下,马上就明白了。
针对mybatis
的:
https://gitee.com/ckl111/all-simple-demo-in-work/tree/master/log-complete-sql-demo-mybatis
针对mybatis-plus
的:
https://gitee.com/ckl111/all-simple-demo-in-work/tree/master/log-complete-sql-demo-mybatis-plus
具体就这么多吧,大家把3个工具类拷过去基本就能用了,然后改为自己mapper的包名,大家觉得有帮助,请点个赞哈,大过年的,哈哈!
曹工杂谈--使用mybatis的同学,进来看看怎么在日志打印完整sql吧,在数据库可执行那种的更多相关文章
- 【曹工杂谈】Maven源码调试工程搭建
Maven源码调试工程搭建 思路 我们前面的文章<[曹工杂谈]Maven和Tomcat能有啥联系呢,都穿打补丁的衣服吗>分析了Maven大体的执行阶段,主要包括三个阶段: 启动类阶段,负责 ...
- 【曹工杂谈】Mysql-Connector-Java时区问题的一点理解--写入数据库的时间总是晚13小时问题
背景 去年写了一篇"[曹工杂谈]Mysql客户端上,时间为啥和本地差了整整13个小时,就离谱",结果最近还真就用上了. 不是我用上,是组内一位同事,他也是这样:有个服务往数据库in ...
- 曹工说Redis源码(3)-- redis server 启动过程完整解析(中)
文章导航 Redis源码系列的初衷,是帮助我们更好地理解Redis,更懂Redis,而怎么才能懂,光看是不够的,建议跟着下面的这一篇,把环境搭建起来,后续可以自己阅读源码,或者跟着我这边一起阅读.由于 ...
- 【曹工杂谈】Mysql客户端上,时间为啥和本地差了整整13个小时,就离谱
瞎扯一点非技术 本来今天上午就打算写的,结果中途被别的事吸引了注意力,公司和某保险公司合作推了一个医疗保险,让我们给父母买,然后我研究了半天条款:又想起来之前买的支付宝那个好医保,也买了两年多了,但是 ...
- 曹工杂谈:Linux服务器上,Spring Boot 原地修改 jar 包配置文件/替换class文件,免去重复上传的麻烦
一.前言 相信很多同学有这样的需求,现在很多公司都有多地的研发中心,经常需要跨地区部署,比如,博主人在成都,但是服务器是北京的.一般城市间网络都不怎么好,上传一个几十兆的jar包那是真的慢,别说现在微 ...
- 曹工杂谈:Java 类加载还会死锁?这是什么情况?
一.前言 今天事不是很多,正好在Java交流群里,看到一个比较有意思的问题,于是花了点时间研究了一下,这里做个简单的分享. 先贴一份测试代码,大家可以先猜测一下,执行结果会是怎样的: import j ...
- 曹工杂谈:Spring boot应用,自己动手用Netty替换底层Tomcat容器
前言 问:标题说的什么意思? 答:简单说,一个spring boot应用(我这里,版本升到2.1.7.Release了,没什么问题),默认使用了tomcat作为底层容器来接收和处理连接. 我这里,在依 ...
- 曹工杂谈:花了两天时间,写了一个netty实现的http客户端,支持同步转异步和连接池(1)--核心逻辑讲解
背景 先说下写这个的目的,其实是好奇,dubbo是怎么实现同步转异步的,然后了解到,其依赖了请求中携带的请求id来完成这个连接复用:然后我又发现,redisson这个redis客户端,底层也是用的ne ...
- 【曹工杂谈】详解Maven插件调试方法
前言 今年的更新频率简直是降至冰点了,一方面平时加班相对多一些了,下班只想玩手机:另一方面,好像进了大厂后,学习动力也很低了,总之就,很懒散,博客的话,今年都才只更新了不到5篇. 现在慢慢有一点状态, ...
随机推荐
- 屏蔽指定地区IP访问
<?php if ($HTTP_SERVER_VARS["HTTP_X_FORWARDED_FOR"]) { $ip = $HTTP_SERVER_VARS["HT ...
- Android教程 -09 数据的持久化存储
视频为本篇播客知识点讲解,建议采用超清模式观看, 欢迎点击订阅我的优酷 任何一个程序其实说白了就是在不停地和数据打交道,数据持久化就是指将那些内存中的瞬时数据保存到存储设备中,保证及时手机关机的情况下 ...
- uva 11754 Code Feat (中国剩余定理)
UVA 11754 一道中国剩余定理加上搜索的题目.分两种情况来考虑,当组合总数比较大的时候,就选择枚举的方式,组合总数的时候比较小时就选择搜索然后用中国剩余定理求出得数. 代码如下: #includ ...
- javascript和jquery 移除事件 和 改变样式
javascript移除事件: document.getElementById("word").onmouseover = null; javascript改变样式: docume ...
- zoj 3652 Maze
Maze Time Limit: 2 Seconds Memory Limit: 65536 KB Celica is a brave person and believer of a Go ...
- Codeforces Round #181 (Div. 2)
A. Array 模拟. B. Coach 模拟. C. Beautiful Numbers good number的位和最大不超过\(10^7\),那么只要枚举a或b的个数,然后最多循环7次判断位和 ...
- caffe学习(1):多平台下安装配置caffe
如何在 centos 7.3 上安装 caffe 深度学习工具 有好多朋友在安装 caffe 时遇到不少问题.(看文章的朋友希望关心一下我的创业项目趣智思成) 今天测试并整理一下安装过程.我是在阿 ...
- nginx+tomcat实现负载均衡(windows环境)
一.准备工作 nginx1.14 nginx1.14下载链接 tomcat8 tomcat8下载链接 windows系统 二.实现目标 访问http://localhost地址时, 将请求轮询到tom ...
- QuartusII 13.0的完美破解
网络上破解QuartusII 13.0软件的方法都不行,最后经过本人总结测试(独创),最终实现了QuartusII 13.0的破解,破解方法如下: 网上常规操作之后,会得到一个“license.dat ...
- ASP.NET MVC 实现页落网资源分享网站+充值管理+后台管理(13)之会员登录注册
源码下载地址:http://www.yealuo.com/Sccnn/Detail?KeyValue=c891ffae-7441-4afb-9a75-c5fe000e3d1c 会员中心,是我们与用户交 ...