博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
0104 代码方式动态刷新logback日志配置
阅读量:4209 次
发布时间:2019-05-26

本文共 19555 字,大约阅读时间需要 65 分钟。

0104 代码方式刷新logback日志配置

背景

日志是一个系统或者说一个产品技术架构中重要组成部分。

常见的日志框架如下:

日志框架 说明 跟slf4j集成所需依赖
slf4j 日志门面,具体实现由程序决定
jcl commons-logging
jcl-over-slf4j
jul jdk-logging slf4j-api
jul-to-slf4j
slf4j-jdk14
log4j log4j slf4j-api
log4j-over-slf4j
slf4j-log4j12
log4j2 log4j-api,log4j-core slf4j-api
log4j-slf4j-impl
logback logback-core,logback-classic slf4j-api

slf4j-logback的启动过程

一般使用slf4j来操作日志:

private static final Logger LOGGER =        LoggerFactory.getLogger(LogbackAppenderExample.class); public static void main(String[] args) {        LOGGER.trace("trace log");        LOGGER.debug("debug log");        LOGGER.info("info log");        LOGGER.warn("warn log");        LOGGER.error("error log");        LOGGER.error("error log  xxx");        LOGGER.error("error log   yyy");        LOGGER.error("error log zzz");        LOGGER.error("error log  aaa");    }

通过这个来跟踪Logger的初始过程;

1 LoggerFactory.getLogger

代码如下:

public static Logger getLogger(Class
clazz) { Logger logger = getLogger(clazz.getName()); if (DETECT_LOGGER_NAME_MISMATCH) { Class
autoComputedCallingClass = Util.getCallingClass(); if (autoComputedCallingClass != null && nonMatchingClasses(clazz, autoComputedCallingClass)) { Util.report(String.format("Detected logger name mismatch. Given name: \"%s\"; computed name: \"%s\".", logger.getName(), autoComputedCallingClass.getName())); Util.report("See " LOGGER_NAME_MISMATCH_URL " for an explanation"); } } return logger; }

过程:

步骤 说明
1 获取得到Logger对象
2 如果有设置系统属性 slf4j.detectLoggerNameMismatch=true
则找到调用getLogger方法的类名
如果跟传入的类名不一致,则给出警告,给的类和调用方法的类不一致,并给出文档地址
3 返回Logger对象

2 getLogger(clazz.getName())

通过类名得到Logger

代码如下:

public static Logger getLogger(String name) {        ILoggerFactory iLoggerFactory = getILoggerFactory();        return iLoggerFactory.getLogger(name); }

核心步骤

序号 步骤
1 得到ILggerFactory对象
2 通过工厂,传入名字,得到Logger对象

3 getILoggerFactory()

得到日志工厂

代码如下:

public static ILoggerFactory getILoggerFactory() {        if (INITIALIZATION_STATE == UNINITIALIZED) {            synchronized (LoggerFactory.class) {                if (INITIALIZATION_STATE == UNINITIALIZED) {                    INITIALIZATION_STATE = ONGOING_INITIALIZATION;                    performInitialization();                }            }        }        switch (INITIALIZATION_STATE) {        case SUCCESSFUL_INITIALIZATION:            return StaticLoggerBinder.getSingleton().getLoggerFactory();        case NOP_FALLBACK_INITIALIZATION:            return NOP_FALLBACK_FACTORY;        case FAILED_INITIALIZATION:            throw new IllegalStateException(UNSUCCESSFUL_INIT_MSG);        case ONGOING_INITIALIZATION:            // support re-entrant behavior.            // See also http://jira.qos.ch/browse/SLF4J-97            return SUBST_FACTORY;        }        throw new IllegalStateException("Unreachable code");    }

核心步骤:

序号 步骤
1 如果初始化状态值为 未初始化
同步加锁 synchronized(LoggerFactory.class)
再次判断 初始化状态值为 未初始化,如果是:
设置初始化状态值为 正在初始化
然后 执行初始化 performInitialization()
2 然后根据初始化状态的条件做不同的处理
如果 初始化失败,抛出异常,并提示哪里失败了
如果 正在初始化, 返回替代工厂SubstituteLoggerFactory,日志一般也是委托给NOPLogger
如果 空回退初始化 返回空的工厂 NOPLoggerFactory,不输出日志的空实现
如果 成功初始化,调用StaticLoggerBinder.getLoggerFactory返回工厂
如果不在以上的状态,直接抛出异常,无法抵达的code;

performInitialization()

执行初始化

代码:

private final static void performInitialization() {        bind();        if (INITIALIZATION_STATE == SUCCESSFUL_INITIALIZATION) {            versionSanityCheck();        } }

核心步骤

序号 步骤说明
1 绑定
2 如果初始化成功,则进行版本明智检查

5 bind()

绑定

代码:

private final static void bind() {        try {            Set
staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet(); reportMultipleBindingAmbiguity(staticLoggerBinderPathSet); // the next line does the binding StaticLoggerBinder.getSingleton(); INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION; reportActualBinding(staticLoggerBinderPathSet); fixSubstitutedLoggers(); playRecordedEvents(); SUBST_FACTORY.clear(); } catch (NoClassDefFoundError ncde) { String msg = ncde.getMessage(); if (messageContainsOrgSlf4jImplStaticLoggerBinder(msg)) { INITIALIZATION_STATE = NOP_FALLBACK_INITIALIZATION; Util.report("Failed to load class \"org.slf4j.impl.StaticLoggerBinder\"."); Util.report("Defaulting to no-operation (NOP) logger implementation"); Util.report("See " NO_STATICLOGGERBINDER_URL " for further details."); } else { failedBinding(ncde); throw ncde; } } catch (java.lang.NoSuchMethodError nsme) { String msg = nsme.getMessage(); if (msg != null && msg.contains("org.slf4j.impl.StaticLoggerBinder.getSingleton()")) { INITIALIZATION_STATE = FAILED_INITIALIZATION; Util.report("slf4j-api 1.6.x (or later) is incompatible with this binding."); Util.report("Your binding is version 1.5.5 or earlier."); Util.report("Upgrade your binding to version 1.6.x."); } throw nsme; } catch (Exception e) { failedBinding(e); throw new IllegalStateException("Unexpected initialization failure", e); } }

关键步骤

序号 步骤
1 找到可能的静态日志绑定器的路径集合findPossibleStaticLoggerBinderPathSet()
2 如果日志有多个绑定器,打印到控制台
如果是android平台,忽略
依次打印出多个日志绑定器,并给出文档提示
3 获得唯一的静态日志绑定器StaticLoggerBinder.getSingleton()
绑定器内部持有LoggerContext和ContextSelectorStaticBinder
4 设置初始化状态为成功
5 打印出实际的日志绑定器 ContextSelectorStaticBinder
6 设置SubstitutedLogger的委托为实际的Logger; fixSubstitutedLoggers()
7 播放记录的事件 playRecordedEvents()
8 清空委托工厂 SubstituteLoggerFactory

6 findPossibleStaticLoggerBinderPathSet()

找到可能的静态日志绑定器的路径

代码:

**

static Set
findPossibleStaticLoggerBinderPathSet() { // use Set instead of list in order to deal with bug #138 // LinkedHashSet appropriate here because it preserves insertion order during iteration Set
staticLoggerBinderPathSet = new LinkedHashSet
(); try { ClassLoader loggerFactoryClassLoader = LoggerFactory.class.getClassLoader(); Enumeration
paths; if (loggerFactoryClassLoader == null) { paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH); } else { paths = loggerFactoryClassLoader.getResources(STATIC_LOGGER_BINDER_PATH); } while (paths.hasMoreElements()) { URL path = paths.nextElement(); staticLoggerBinderPathSet.add(path); } } catch (IOException ioe) { Util.report("Error getting resources from path", ioe); } return staticLoggerBinderPathSet; }

关键步骤:

序号 步骤
1 如果LoggerFactory的类加载器为空,系统类加载器得到
org/slf4j/impl/StaticLoggerBinder.class 这个文件
分布在不同的jar中,可能有多个;
2 如果不为空,则通过LoggerFactoryLoader找到
org/slf4j/impl/StaticLoggerBinder.class 这个文件
3 把这些class对应的url汇总到结合中返回

image.png

image.png

7 playRecordedEvents()

放映记录的事件

代码:

private static void playRecordedEvents() {        List
events = SUBST_FACTORY.getEventList(); if (events.isEmpty()) { return; } for (int i = 0; i < events.size(); i ) { SubstituteLoggingEvent event = events.get(i); SubstituteLogger substLogger = event.getLogger(); if( substLogger.isDelegateNOP()) { break; } else if (substLogger.isDelegateEventAware()) { if (i == 0) emitReplayWarning(events.size()); substLogger.log(event); } else { if(i == 0) emitSubstitutionWarning(); Util.report(substLogger.getName()); } } }

关键步骤:

序号 步骤
1 得到委托日志工厂的事件,如果为空,则结束
2 如果事件不为空,取出来,
如果委托的日志有空日志,中断
如果委托的日志是委托事件, 打印日志,并打印出播放的警告
否则,警告委托的日志不可用,并打印出日志的名称

8 versionSanityCheck()

得到StaticLoggerBinder的版本,并进行判断是否合适。

LoggerFactory放了允许使用的StaticLoggerBinder的版本,如果不合适,会答应出警告。

源码:

private final static void versionSanityCheck() {        try {            String requested = StaticLoggerBinder.REQUESTED_API_VERSION;            boolean match = false;            for (String aAPI_COMPATIBILITY_LIST : API_COMPATIBILITY_LIST) {                if (requested.startsWith(aAPI_COMPATIBILITY_LIST)) {                    match = true;                }            }            if (!match) {                Util.report("The requested version "   requested   " by your slf4j binding is not compatible with "                                  Arrays.asList(API_COMPATIBILITY_LIST).toString());                Util.report("See "   VERSION_MISMATCH   " for further details.");            }        } catch (java.lang.NoSuchFieldError nsfe) {            // given our large user base and SLF4J's commitment to backward            // compatibility, we cannot cry here. Only for implementations            // which willingly declare a REQUESTED_API_VERSION field do we            // emit compatibility warnings.        } catch (Throwable e) {            // we should never reach here            Util.report("Unexpected problem occured during version sanity check", e);        }    }

9 StaticLoggerBinder.init()

静态日志绑定器的初始化

代码:

void init() {        try {            try {                new ContextInitializer(defaultLoggerContext).autoConfig();            } catch (JoranException je) {                Util.report("Failed to auto configure default logger context", je);            }            // logback-292            if (!StatusUtil.contextHasStatusListener(defaultLoggerContext)) {                StatusPrinter.printInCaseOfErrorsOrWarnings(defaultLoggerContext);            }            contextSelectorBinder.init(defaultLoggerContext, KEY);            initialized = true;        } catch (Exception t) { // see LOGBACK-1159            Util.report("Failed to instantiate ["   LoggerContext.class.getName()   "]", t);        }    }

核心过程

序号 步骤
1 新建上下文初始化器,然后自动配置;
new ContextInitializer(defaultLoggerContext).autoConfig();
2 如果没有配置状态监听器,则打印出警告
3 上下文选择绑定器初始化

10 ContextInitializer.autoConfig();

自动配置上下文

代码:

public void autoConfig() throws JoranException {        StatusListenerConfigHelper.installIfAsked(loggerContext);        URL url = findURLOfDefaultConfigurationFile(true);        if (url != null) {            configureByResource(url);        } else {            Configurator c = EnvUtil.loadFromServiceLoader(Configurator.class);            if (c != null) {                try {                    c.setContext(loggerContext);                    c.configure(loggerContext);                } catch (Exception e) {                    throw new LogbackException(String.format("Failed to initialize Configurator: %s using ServiceLoader", c != null ? c.getClass()                                    .getCanonicalName() : "null"), e);                }            } else {                BasicConfigurator basicConfigurator = new BasicConfigurator();                basicConfigurator.setContext(loggerContext);                basicConfigurator.configure(loggerContext);            }        }    }

核心步骤

序号 说明
1 如果没有,安装状态监听器
2 找到默认的配置文件或者URL,一次按照系统属性
logback.configurationFile查找
按照logback-test.xml
按照logback.groovy
按照logback.xml
得到配置文件
3 如果找到了,configureByResource(url);
4 否则,按照spi的方式找到Configurator的实现类,设置上下文,进行配置
如果spi方式拿不到,则使用缺省的BasicConfigurator(里面只配置了一个控制台)
设置上下文,进行配置

11 StaticLoggerBinder.getLoggerFactory

通过静态日志绑定器得到日志工厂,实现类是 LoggerContext;

源码:

public ILoggerFactory getLoggerFactory() {        if (!initialized) {            return defaultLoggerContext;        }        if (contextSelectorBinder.getContextSelector() == null) {            throw new IllegalStateException("contextSelector cannot be null. See also "   NULL_CS_URL);        }        return contextSelectorBinder.getContextSelector().getLoggerContext();    }

核心流程:

序号 步骤
1 如果没有初始化,返回默认的LoggerContext
2 如果ContextSelectBinder不为空,得到ContextSeleter
3 通过ContextSelector得到LoggerContext;

12 iLoggerFactory.getLogger(name)

这是一个接口,直接得到一个Logger实例;

从上面的代码之后,这里的实例应该是一个LoggerContext对象

这个对象是核心,所有的日志动作都在里面;

logback-aliyun-appender

直接把日志接入到阿里云

对于初创企业来说,直接使用阿里云的日志服务非常方便,减少了自己搭建ELK的运维成本,直接按量付费,非常方便,我贴一下我的接入过程;

引入依赖:

com.aliyun.openservices
aliyun-log-logback-appender
org.slf4j
jcl-over-slf4j
org.slf4j
log4j-over-slf4j

然后按照 代码刷新logback日志配置的方法,把日志配置放到apollo,启动的时候就可以接入到阿里云日志了。

贴一下配置:

cn-xxx.log.aliyuncs.com
xxxxx
xxxxx
ts-app-xxx
ts-app-xxx
topic2
source2
104857600
60
2
524288
4096
2000
3
100
100
%d %-5level [%thread] %logger{0}: %msg
yyyy-MM-dd'T'HH:mmZ
Asia/Shanghai
INFO
%d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg %X{THREAD_ID} %n

代码刷新logback日志配置

主要是模仿LogbackLister的实现细节来模仿:

简单的贴一下我的实现代码:

package com.lifesense.opensource.spring;import ch.qos.logback.classic.BasicConfigurator;import ch.qos.logback.classic.LoggerContext;import ch.qos.logback.classic.joran.JoranConfigurator;import ch.qos.logback.core.joran.spi.JoranException;import ch.qos.logback.core.util.StatusPrinter;import org.apache.commons.lang3.StringUtils;import org.slf4j.LoggerFactory;import org.springframework.util.Assert;import org.springframework.util.ClassUtils;import org.springframework.util.ReflectionUtils;import javax.servlet.ServletContext;import java.io.ByteArrayInputStream;import java.io.InputStream;import java.lang.reflect.Method;/** * @author carter */public class LogbackLoader {    private static final String DEFAULT_LOG_BACK_XML = "
" "
" "
" "
%d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg %X{THREAD_ID} %n
" "
" "
" "
" "
" "
"; /** * 初始化日志配置 */ public static void initLogbackWithoutConfigFile(ServletContext servletContext) { initLogbackConfigFromXmlString(servletContext, DEFAULT_LOG_BACK_XML); } public static void initLogbackConfigFromXmlString(ServletContext servletContext, String xmlStr) { System.out.println("Initializing Logback from [\n" xmlStr "\n]"); LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory(); Assert.notNull(loggerContext, "获取不到LoggerContext"); loggerContext.getStatusManager().clear(); loggerContext.reset(); //安装默认的日志配置 if (StringUtils.isBlank(xmlStr)) { BasicConfigurator basicConfigurator = new BasicConfigurator(); basicConfigurator.setContext(loggerContext); basicConfigurator.configure(loggerContext); return; } //按照传入的配置文件来配置 JoranConfigurator configurator = new JoranConfigurator(); configurator.setContext(loggerContext); InputStream in = new ByteArrayInputStream(xmlStr.getBytes()); try { configurator.doConfigure(in); } catch (JoranException e) { System.out.println("初始化配置logback发生错误"); e.printStackTrace(); } //If SLF4J's java.util.logging bridge is available in the classpath, install it. This will direct any messages //from the Java Logging framework into SLF4J. When logging is terminated, the bridge will need to be uninstalled try { Class
julBridge = ClassUtils.forName("org.slf4j.bridge.SLF4JBridgeHandler", ClassUtils.getDefaultClassLoader()); Method removeHandlers = ReflectionUtils.findMethod(julBridge, "removeHandlersForRootLogger"); if (removeHandlers != null) { servletContext.log("Removing all previous handlers for JUL to SLF4J bridge"); ReflectionUtils.invokeMethod(removeHandlers, null); } Method install = ReflectionUtils.findMethod(julBridge, "install"); if (install != null) { servletContext.log("Installing JUL to SLF4J bridge"); ReflectionUtils.invokeMethod(install, null); } } catch (ClassNotFoundException ignored) { //Indicates the java.util.logging bridge is not in the classpath. This is not an indication of a problem. servletContext.log("JUL to SLF4J bridge is not available on the classpath"); } StatusPrinter.print(loggerContext); }}

在springmvc上下文启动的时候,可以使用代码的方式加载默认的日志配置;

启动完成之后,加上apollo的配置监听器,这样就可以在apollo中实时的修改日志的配置文件,代码实时生效。

package com.lifesense.opensource.spring;import com.ctrip.framework.apollo.Config;import com.ctrip.framework.apollo.ConfigService;import com.ctrip.framework.apollo.model.ConfigChange;import com.google.common.base.Strings;import com.lifesense.opensource.commons.utils.WebResourceUtils;import lombok.extern.slf4j.Slf4j;import org.springframework.util.CollectionUtils;import org.springframework.web.context.WebApplicationContext;import org.springframework.web.context.support.WebApplicationContextUtils;import javax.servlet.ServletContext;import javax.servlet.ServletContextEvent;import java.util.Objects;import java.util.Set;/** * @author carter */@Slf4jpublic class ContextLoaderListener extends org.springframework.web.context.ContextLoaderListener {    private static final String APOLLO_LOG_BACK_CONFIG_KEY = "log4j2.xml";    @Override    public void contextInitialized(ServletContextEvent event) {        final ServletContext servletContext = event.getServletContext();        final Config configFile = ConfigService.getAppConfig();        String xmlContent = configFile.getProperty(APOLLO_LOG_BACK_CONFIG_KEY, "");        if (!Strings.isNullOrEmpty(xmlContent)) {            LogbackLoader.initLogbackConfigFromXmlString(servletContext, xmlContent);            configFile.addChangeListener(configFileChangeEvent -> {                final Set
newValue = configFileChangeEvent.changedKeys(); if (!CollectionUtils.isEmpty(newValue) && newValue.contains(APOLLO_LOG_BACK_CONFIG_KEY)) { final ConfigChange change = configFileChangeEvent.getChange(APOLLO_LOG_BACK_CONFIG_KEY); System.out.println(String.format("log4j2.ml changed:old:\n %s , new : \n %s ", change.getOldValue(), change.getNewValue())); LogbackLoader.initLogbackConfigFromXmlString(servletContext, change.getNewValue()); } }); } }}

小结

今天学会了:

1. slf4j的日志装配过程,分析了源码;

1. 学会了使用代码的方式动态刷新logback的日志配置;

1. 一种接入阿里云日志的实现方式。

1. 常见的slf4j的日志组合方式的使用;

原创不易,转载请注明出处,欢迎多沟通交流

你可能感兴趣的文章
LoadRunner如何在脚本运行时修改log设置选项?
查看>>
QC数据库表结构
查看>>
自动化测试工具的3个关键部分
查看>>
测试工具厂商的编程语言什么时候“退休”?
查看>>
资源监控工具 - Hyperic HQ
查看>>
LoadRunner中Concurrent与Simultaneous的区别
查看>>
SiteScope - Agentless监控
查看>>
QTP测试.NET控件CheckedListBox
查看>>
使用QTP的.NET插件扩展技术测试ComponentOne的ToolBar控件
查看>>
用上帝之眼进行自动化测试
查看>>
为LoadRunner写一个lr_save_float函数
查看>>
PrefTest工作室全新力作-《性能测试与调优实战》课程视频即将上线
查看>>
质量度量分析与测试技术 培训大纲
查看>>
欢迎加入【亿能测试快讯】邮件列表!
查看>>
为什么我们的自动化测试“要”这么难
查看>>
LoadRunner性能脚本开发实战训练
查看>>
测试之途,前途?钱途?图何?
查看>>
adb常用命令
查看>>
通过LR监控Linux服务器性能
查看>>
通过FTP服务的winsockes录制脚本
查看>>