栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

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

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

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;
4 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汇总到结合中返回

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;


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;



@Slf4j
public 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的日志装配过程,分析了源码;
  2. 学会了使用代码的方式动态刷新logback的日志配置;
  3. 一种接入阿里云日志的实现方式。
  4. 常见的slf4j的日志组合方式的使用;

本文由博客一文多发平台 OpenWrite 发布!

转载请注明:文章转载自 www.mshxw.com
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号