log4j2 的多模块化适配

Koupleless log4j2 的多模块化适配

为什么需要做适配

原生 log4j2 在多模块下,模块没有独立打印的日志目录,统一打印到基座目录里,导致日志和对应的监控无法隔离。这里做适配的目的就是要让模块能有独立的日志目录。

普通应用 log4j2 的初始化

在 Spring 启动前,log4j2 会使用默认值初始化一次各种 logContext 和 Configuration,然后在 Spring 启动过程中,监听 Spring 事件进行初始化 org.springframework.boot.context.logging.LoggingApplicationListener,这里会调用到 Log4j2LoggingSystem.initialize 方法

该方法会根据 loggerContext 来判断是否已经初始化过了

这里在多模块下会存在问题一

这里的 getLoggerContext 是根据 org.apache.logging.log4j.LogManager 所在 classLoader 来获取 LoggerContext。根据某个类所在 ClassLoader 来提取 LoggerContext 在多模块化里会存在不稳定,因为模块一些类可以设置为委托给基座加载,所以模块里启动的时候,可能拿到的 LoggerContext 是基座的,导致这里 isAlreadyInitialized 直接返回,导致模块的 log4j2 日志无法进一步根据用户配置文件配置。

如果没初始化过,则会进入 super.initialize, 这里需要做两部分事情:

  1. 获取到日志配置文件
  2. 解析日志配置文件里的变量值 这两部分在多模块里都可能存在问题,先看下普通应用过程是如何完成这两步的

获取日志配置文件

可以看到是通过 ResourceUtils.getURL 获取的 location 对应日志配置文件的 url,这里通过获取到当前线程上下文 ClassLoader 来获取 URL,这在多模块下没有问题(因为每个模块启动时线程上下文已经是 模块自身的 ClassLoader )。

解析日志配置值

配置文件里有一些变量,例如这些变量

这些变量的解析逻辑在 org.apache.logging.log4j.core.lookup.AbstractLookup 的具体实现里,包括

变量写法代码逻辑地址
${bundle:application:logging.file.path}org.apache.logging.log4j.core.lookup.ResourceBundleLookup根据 ResourceBundleLookup 所在 ClassLoader 提前到 application.properties, 读取里面的值
${ctx:logging.file.path}org.apache.logging.log4j.core.lookup.ContextMapLookup根据 LoggerContext 上下文 ThreadContex 存储的值来提起,这里需要提前把 applicaiton.properties 的值设置到 ThreadContext 中

根据上面判断通过 bundle 的方式配置在多模块里不可行,因为 ResourceBundleLookup 可能只存在于基座中,导致始终只能拿到基座的 application.properties,导致模块的日志配置路径与基座相同,模块日志都打到基座中。所以需要改造成使用 ContextMapLookup。

static final修饰的Logger导致三方组件下沉基座后日志打印不能正常隔离

如:

private static final Logger LOG = LoggerFactory.getLogger(CacheManager.class);
  1. static final修饰的变量只会在类加载的时候初始化话一次
  2. 组件依赖下沉基座后,类加载器使用的为基座的类加载器,初始化log实例时使用的是基座的log配置,所以会打印到基座文件中,不能正常隔离

具体获取log源码如下:

//org.apache.logging.log4j.spi.AbstractLoggerAdapter
@Override
public L getLogger(final String name) {
    //关键是LoggerContext获取是否正确,往下追
    final LoggerContext context = getContext();
    final ConcurrentMap<String, L> loggers = getLoggersInContext(context);
    final L logger = loggers.get(name);
    if (logger != null) {
        return logger;
    }
    loggers.putIfAbsent(name, newLogger(name, context));
    return loggers.get(name);
}

//获取LoggerContext
protected LoggerContext getContext() {
    Class<?> anchor = LogManager.getFactory().isClassLoaderDependent() ? StackLocatorUtil.getCallerClass(Log4jLoggerFactory.class, CALLER_PREDICATE) : null;
    LOGGER.trace("Log4jLoggerFactory.getContext() found anchor {}", anchor);
    return anchor == null ? LogManager.getContext(false) : this.getContext(anchor);
}

//获取LoggerContext,关键在这里
protected LoggerContext getContext(final Class<?> callerClass) {
    ClassLoader cl = null;
    if (callerClass != null) {
        //会优先使用当前类相关的类加载器,这里肯定是基座的类加载,所以返回的是基座的LoggerContext
        cl = callerClass.getClassLoader();
    }
    if (cl == null) {
        cl = LoaderUtil.getThreadContextClassLoader();
    }
    return LogManager.getContext(cl, false);
}

预期多模块合并下的日志

基座与模块都能使用独立的日志配置、配置值,完全独立。但由于上述分析中,存在两处可能导致模块无法正常初始化的逻辑,故这里需要多 log4j2 进行适配。 static修饰的log在三方组件下沉基座后也会导致相关日志不能正常隔离打印,所以这里也需要做 log4j2 进行适配。

多模块适配点

  1. getLoggerContext() 能拿到模块自身的 LoggerContext
  1. 需要调整成使用 ContextMapLookup,从而模块日志能获取到模块应用名,日志能打印到模块目录里

    a. 模块启动时将 application.properties 的值设置到 ThreadContext 中 b. 日志配置时,只能使用 ctx:xxx:xxx 的配置方式

  2. LoggerFactory.getLogger()获取的是org.apache.logging.slf4j.Log4jLogger实例,他是一个包装类,所以有一定操作空间, 针对Log4jLogger进行复写改造,根据当前线程上下文类加载器动态获取底层ExtendedLogger对象

public class Log4JLogger implements LocationAwareLogger, Serializable {
   private transient final Map<ClassLoader, ExtendedLogger> loggerMap = new ConcurrentHashMap<>();
   private static final Map<ClassLoader, LoggerContext> LOGGER_CONTEXT_MAP = new ConcurrentHashMap<>();

   pubblic void info(final String format, final Object o) {
       //每次调用都获取对应的ExtendedLogger
       getLogger().logIfEnabled(FQCN, Level.INFO, null, format, o);
   }
   
   //根据当前线程类加载器动态获取ExtendedLogger
   private ExtendedLogger getLogger() {
       ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
       ExtendedLogger extendedLogger = loggerMap.get(classLoader);
       if (extendedLogger == null) {
           LoggerContext loggerContext = LOGGER_CONTEXT_MAP.get(classLoader);
           if (loggerContext == null) {
               loggerContext = LogManager.getContext(classLoader, false);
               LOGGER_CONTEXT_MAP.put(classLoader, loggerContext);
           }
           extendedLogger = loggerContext.getLogger(this.name);
           loggerMap.put(classLoader, extendedLogger);
       }
       return extendedLogger;
  }
  } 

模块改造方式

详细查看源码