Cao topics for work - using mybatis students, come to see how to print it in full sql logs, perform the kind of in the database

Foreword

Today, the first day of the New Year, Happy New Year to you, I wish you all the new year, technology Dodo suddenly, long hair long!

We engage in technology, more directly, it would begin. I give you my demo to see the effect of the project (the code below will give everyone):

Technology stack is a mybatis/mybatis plus, spring boot, logs are logback.

In fact, this pain point of it, I have been there, testing or development, logging in each time you print are with? The sql, then have their own manual parameter of a parameter affixed to the past, this is really a manual labor. Although the physical work, or do so many years, this time, finally decided not bear up.

Before this get it, I know the idea has had a plug-in can achieve this function, mybatis-log-pluginbut I have been here idea can not afford, the specific reasons unknown, anyway, is not complete sql print out.

Then I just found the next, mybatis plus also supported, so add the following line to the configuration:

mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

But I noticed that this is printed to the console, I tried it, the effect is as follows:

I think this is very good, but there is room for optimization:

  1. console print, does not apply to the development and test environments; local debugging is not bad;
  2. When local debugging, I usually only suspends the current thread, if requested more, here's print will be a mess; I can not tell which log the request, rather than the other thread print

I have also used this project mybatis-plus, so I end up like this configuration is the following:

mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl

Use slf4jprint, rather than consoledirectly print. But this still does not solve: assemble complete sql, and print to the needs of the log.

General idea

Because of their own blind groping out of the program, not guaranteed to be the best, can only say: it works.

Look at, under normal circumstances, would print such an sql following (mybatis default support):

[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]

That is, the default print out: one line preparedStatementstatement with? ; Next line is the corresponding parameters.

My plan is that for loggerdynamic agents, when calling logger.info/debug/...time, interception.

After interception logic, as follows:

  1. When the statement is printed to ==> Preparing:at the beginning, the current statement is stored in the thread-local variable is assumed to be A;
  2. When the statement is printed to ==> Parameters:at the beginning of the current thread-local variables A out, and the current statement together, makes up a complete sql, then called the current method (remember, we have a dynamic proxy logger.info other methods) printing of.

Paint Solution:

The above logic diagram, we looked all right, in fact, the key question becomes, how to generate dynamic proxy this logger, and most importantly, you generate a dynamic proxy object to how effective.

Analysis of the specific implementation

To explain this part, we can only cut into the details, after all, we have to find a starting point, to use our dynamic proxy logger.

We should remember that, when we usually use slf4j, not the next generation logger is written (though now with the lombok, nature has not changed):

private static final Logger logger = LoggerFactory.getLogger(A.class);
public static Logger getLogger(String name) {
    ILoggerFactory iLoggerFactory = getILoggerFactory();
    return iLoggerFactory.getLogger(name);
}

This line, getILoggerFactorygoing to get the classpath bound log to achieve a specific process, I was in another one also has said:
Cao workers change bug-- This time, I met a formidable stack overflow bug, or log-related, really hard

Because logback we use, so there will enter into (the package name in the package logback how is slf4j of? Yes, this is how slf4j-api go to the core of the implementation class, SPI mechanism similar to java, and specifically see above Bowen ):

logback-classic包内的:
org.slf4j.impl.StaticLoggerBinder#getSingleton
 public static StaticLoggerBinder getSingleton() {
        return SINGLETON;
 }
 进入上面代码前,会先执行静态代码:
 private static StaticLoggerBinder SINGLETON = new StaticLoggerBinder();
 static {
    SINGLETON.init();
 }

The above static code block is initialized:

    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);
        }
    }

After my multi-party debugging, I found here contextSelectorand found it very critical. It is an interface, as follows:


/**
 * 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();
}

Note that the method of this class, LoggerContext getLoggerContext();return value LoggerContext, the return value type relatively fast hardware, because it is actually LoggerFactory.

public class LoggerContext extends ContextBase implements ILoggerFactory, LifeCycle 

We see this LoggerContextachieved ILoggerFactory:

public interface ILoggerFactory {
    // 这个东西,大家熟悉了噻,logger工厂啊
    public Logger getLogger(String name);
}

The foregoing analysis, we want to change Logger, may not be so easy, because Logger, is ILoggerFactorycalling getLoggerobtained.

那么,我们只能把原始的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的包名,大家觉得有帮助,请点个赞哈,大过年的,哈哈!

Guess you like

Origin www.cnblogs.com/grey-wolf/p/12130803.html