1 - 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, 这里需要做两部分事情:
- 获取到日志配置文件
- 解析日志配置文件里的变量值 这两部分在多模块里都可能存在问题,先看下普通应用过程是如何完成这两步的
获取日志配置文件
可以看到是通过 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。
预期多模块合并下的日志
基座与模块都能使用独立的日志配置、配置值,完全独立。但由于上述分析中,存在两处可能导致模块无法正常初始化的逻辑,故这里需要多 log4j2 进行适配。
多模块适配点
getLoggerContext() 能拿到模块自身的 LoggerContext
需要调整成使用 ContextMapLookup,从而模块日志能获取到模块应用名,日志能打印到模块目录里
a. 模块启动时将 application.properties 的值设置到 ThreadContext 中 b. 日志配置时,只能使用 ctx:xxx:xxx 的配置方式
模块改造方式
2 - ehcache 的多模块化最佳实践
为什么需要最佳实践
CacheManager 初始化的时候存在共用 static 变量,多应用使用相同的 ehcache name,导致缓存互相覆盖。
最佳实践的几个要求
- 基座里必须引入 ehcache,模块里复用基座
在 springboot 里 ehcache 的初始化需要通过 Spring 里定义的 EhCacheCacheConfiguration 来创建,由于 EhCacheCacheConfiguration 是属于 Spring, Spring 统一放在基座里。
这里在初始化的时候,在做 Bean 初始化的条件判断时会走到类的检验, 如果 net.sf.ehcache.CacheManager 是。这里会走到 java native 方法上做判断,从当前类所在的 ClassLoader 里查找 net.sf.ehcache.CacheManager 类,所以基座里必须引入这个依赖,否则会报 ClassNotFound 的错误。
- 模块里将引入的 ehcache 排包掉(scope设置成 provide,或者使用自动瘦身能力)
模块使用自己 引入的 ehcache,照理可以避免共用基座 CacheManager 类里的 static 变量,而导致报错的问题。但是实际测试发现,模块安装的时候,在初始化 enCacheCacheManager 时, 这里在 new 对象时,需要先获得对象所属类的 CacheManager 是基座的 CacheManager。这里也不能讲 CacheManager 由模块 compile 引入,否则会出现一个类由多个不同 ClassLoader 引入导致的问题。
所以结论是,这里需要全部委托给基座加载。
最佳实践的方式
- 模块 ehcache 排包瘦身委托给基座加载
- 如果多个模块里有多个相同的 cacheName,需要修改 cacheName 为不同值。
- 如果不想改代码的方式修改 cache name,可以通过打包插件的方式动态替换 cacheName
<plugin>
<groupId>com.google.code.maven-replacer-plugin</groupId>
<artifactId>replacer</artifactId>
<version>1.5.3</version>
<executions>
<!-- 打包前进行替换 -->
<execution>
<phase>prepare-package</phase>
<goals>
<goal>replace</goal>
</goals>
</execution>
</executions>
<configuration>
<!-- 自动识别到项目target文件夹 -->
<basedir>${build.directory}</basedir>
<!-- 替换的文件所在目录规则 -->
<includes>
<include>classes/j2cache/*.properties</include>
</includes>
<replacements>
<replacement>
<token>ehcache.ehcache.name=f6-cache</token>
<value>ehcache.ehcache.name=f6-${parent.artifactId}-cache</value>
</replacement>
</replacements>
</configuration>
</plugin>
- 需要把 FactoryBean 的 shared 设置成 false
@Bean
public EhCacheManagerFactoryBean ehCacheManagerFactoryBean() {
EhCacheManagerFactoryBean factoryBean = new EhCacheManagerFactoryBean();
// 需要把 factoryBean 的 share 属性设置成 false
factoryBean.setShared(true);
// factoryBean.setShared(false);
factoryBean.setCacheManagerName("biz1EhcacheCacheManager");
factoryBean.setConfigLocation(new ClassPathResource("ehcache.xml"));
return factoryBean;
}
否则会进入这段逻辑,初始化 CacheManager 的static 变量 instance. 该变量如果有值,且如果模块里 shared 也是ture 的化,就会重新复用 CacheManager 的 instance,从而拿到基座的 CacheManager, 从而报错。
最佳实践的样例
样例工程请参考这里