背景

Spring Boot的默认日志框架始终是 Logback,反对的很好。而且针对Logback,Spring Boot还提供了一个扩大性能 - <springProfile>,这个标签能够在Logback的XML配置文件中应用,用于配合Spring的profile来辨别环境,十分不便。

比方你能够像上面这样,只配置一个logback-spring.xml配置文件,而后用<springProfile>来辨别环境,开发环境只输入到控制台,而其余环境输入到文件

<Root level="INFO">  <!-- 开发环境应用Console Appender,生产环境应用File Appender -->  <springProfile name="dev">    <AppenderRef ref="Console"/>  </springProfile>  <SpringProfile name="!dev">    <AppenderRef ref="File"/>  </SpringProfile></Root>

这样做的益处是,我只须要一个logback.xml配置文件,就能够解决多环境的问题,而不是每个环境一个logback-spring.xml,切实太香了

这个Profile还能够有一些更灵便的语法(具体参考Spring Boot的官网文档):

  • ! - 逻辑“非”
  • & - 逻辑“与”
  • | - 逻辑“或”
<springProfile name="staging">    <!-- configuration to be enabled when the "staging" profile is active --></springProfile><springProfile name="dev | staging">    <!-- configuration to be enabled when the "dev" or "staging" profiles are active --></springProfile><springProfile name="!production">    <!-- configuration to be enabled when the "production" profile is not active --></springProfile>

然而有时候为了性能或其余起因,咱们会抉择log4j2作为Spring Boot的日志框架。Spirng Boot当然也是反对log4j2(其实没啥支不反对的,只有你不必spring boot的日志配置入口,jboss-logging都能反对)的,只须要引入spring-boot-starter-log4j2,再排除默认的logback依赖即可

<dependency>  <groupId>org.springframework.boot</groupId>  <artifactId>spring-boot-starter-web</artifactId>  <exclusions>    <!--排除默认的spring-boot-starter-logging,这个依赖传递依赖了logback相干包-->    <exclusion>      <groupId>org.springframework.boot</groupId>      <artifactId>spring-boot-starter-logging</artifactId>    </exclusion>  </exclusions></dependency><!--引入log4j2的依赖,同时传递依赖log4j2的相干适配包--><dependency>  <groupId>org.springframework.boot</groupId>  <artifactId>spring-boot-starter-log4j2</artifactId>  <version>2.4.4</version></dependency>

切换到 log4j2 尽管很简略,然而Spring Boot并没有对 log4j2进行扩大!log4j2的xml配置形式,并不反对<SpringProfile>标签,不能欢快的配置多环境!搜寻了一下,StackOverflow上也有人有雷同的困惑,而且这个性能目前并没有任何人提供
**
于是,我萌发了一个大胆的想法 :本人开发一个Spring Boot - Log4j2 XML的扩大,让 log4j2 的XML也反对<SpringProfile>标签,而后奉献给Spring Boot,万一被驳回了岂不妙哉。

而且这可不是改个正文,改个标点符号,改个变量名之类的PR;这可是一个新 feature,一旦被驳回,Spring Boot的文档上就会有我的一份力了!

性能开发

说干就干,先剖析Log4j2 XML解析的源码,看看好不好下手

Log4j2 XML解析源码剖析

通过一阵剖析,找到了 Log4j2 的 XML 文件解析代码在 org.apache.logging.log4j.core.config.xml.XmlConfiguration,仔细阅读+DEBUG这个类之后,发现这个XML解析类各种解析办法不是static就是private,设计之初就没有思考过提供扩大,定制标签的性能。比方这个递归解析标签的办法,间接就是private的:

private void constructHierarchy(final Node node, final Element element) {        processAttributes(node, element);        final StringBuilder buffer = new StringBuilder();        final NodeList list = element.getChildNodes();        final List<Node> children = node.getChildren();        for (int i = 0; i < list.getLength(); i++) {            final org.w3c.dom.Node w3cNode = list.item(i);            if (w3cNode instanceof Element) {                final Element child = (Element) w3cNode;                final String name = getType(child);                final PluginType<?> type = pluginManager.getPluginType(name);                final Node childNode = new Node(node, name, type);                constructHierarchy(childNode, child);                if (type == null) {                    final String value = childNode.getValue();                    if (!childNode.hasChildren() && value != null) {                        node.getAttributes().put(name, value);                    } else {                        status.add(new Status(name, element, ErrorType.CLASS_NOT_FOUND));                    }                } else {                    children.add(childNode);                }            } else if (w3cNode instanceof Text) {                final Text data = (Text) w3cNode;                buffer.append(data.getData());            }        }        final String text = buffer.toString().trim();        if (text.length() > 0 || (!node.hasChildren() && !node.isRoot())) {            node.setValue(text);        }    }

连解析后的数据,也是private

private Element rootElement;

想通过继承的形式,只重写局部办法来实现基本不可能,除非重写整个类能力扩大自定义的标签……

危险 & 兼容性的思考

这下就难堪了,重写整个类尽管也能够,但兼容性就得不到保障了。因为一旦Log4j2 的 XML配置有更新,我这套扩大就废了,不论是大更新还是小更新,但但凡这个类有变动我这个扩大就得跟着重写,切实不稳当。

但我在查看了XmlConfiguration这个类的提交历史后发现,它最近一次更新的工夫在2019年6月

而整个Log4j2框架 ,在2019年6月到2021年3月之间,公布了9次Release版本

整个我的项目更新了两年,快十个版本中,XmlConfiguration 只更新过一次,阐明更新频率很低。而且比照变更记录发现,这个类近几次的更新内容也很少。

这么一想,我就算重写XmlConfiguration又怎么样,这么低的更新频率,这么少的更新内容,重写的危险也很低啊。而且我也不是全副重写,只是拷贝原有的代码,加上一点自定义标签的反对而已,改变量并不大。就算须要跟着Log4j2 更新的话,比照一下代码,从新调整一遍也不是难事。

就这样我压服了本人,开始拉代码……

fork/clone 代码,本地环境搭建

spring-boot 仓库地址:https://github.com/spring-projects/spring-boot

  1. Fork一份 Spring Boot的代码
  2. clone 这个fork的仓库
  3. 基于master,新建一个log4j2_enhancement分支用于开发

这里也能够间接通过IDEA clone,不过前提是你有个“牢靠又稳固”的网络

因为Spring/Spring Boot曾经将构建工具从Maven迁徙到了Gradle,所以IDEA版本最好不要太老,太老的版本可能对Gradle反对的不够好。

如果你的网络足够“牢靠和稳固”,那么只须要在IDEA中关上Spring Boot的源码,就能够自定构建好开发环境,间接运行测试了。否则可能会遇到Gradle和相干包下载失败,Maven仓库包下载失败等各种问题……

Spring Boot对Logback的反对扩大

既然Spring Boot对Logback(XML)进行了加强,那么先来看看它是怎么加强的,待会我反对Log4j2的话能省很多事。

通过一阵剖析,找到了这个Logback的扩大点:

class SpringBootJoranConfigurator extends JoranConfigurator {    private LoggingInitializationContext initializationContext;    SpringBootJoranConfigurator(LoggingInitializationContext initializationContext) {        this.initializationContext = initializationContext;    }    @Override    public void addInstanceRules(RuleStore rs) {        super.addInstanceRules(rs);        Environment environment = this.initializationContext.getEnvironment();        rs.addRule(new ElementSelector("configuration/springProperty"), new SpringPropertyAction(environment));        rs.addRule(new ElementSelector("*/springProfile"), new SpringProfileAction(environment));        rs.addRule(new ElementSelector("*/springProfile/*"), new NOPAction());    }}

就……这么简略?顺着这个类又剖析了一遍JoranConfigurator和相干的类之后,发现这都是Logback的功绩。

Logback文档中提到,这个Joran 实际上是一个通用的配置零碎,能够独立于日志零碎应用。但我搜寻了一下,除了Logback的文档以外,并没有找到这个Joran的出处在哪。

不过这并不重要,我就把他当做一个通用的配置解析器,被logback援用了而已。

这个解析器比拟灵便,能够自定义标签/标签解析的行为,只须要重写addInstanceRules这个办法,增加自定义的标签名和行为类即可:

@Overridepublic void addInstanceRules(RuleStore rs) {    super.addInstanceRules(rs);    Environment environment = this.initializationContext.getEnvironment();    rs.addRule(new ElementSelector("configuration/springProperty"), new SpringPropertyAction(environment));    //就是这么简略……    rs.addRule(new ElementSelector("*/springProfile"), new SpringProfileAction(environment));    rs.addRule(new ElementSelector("*/springProfile/*"), new NOPAction());}

而后在SpringProfileAction中,通过Spring的Environment对象,拿到以后激活的Profiles进行匹配就能搞定

如法炮制,增加Log4j2 的自定义扩大

尽管Log4j2的XML解析并不能像Logback那样灵便,直接插入扩大。然而基于我后面的危险&兼容性剖析,重写XmlConfiguration也是能够实现自定义标签解析的:

先创立一个SpringBootXmlConfiguration

这个类的代码,是齐全复制了org.apache.logging.log4j.core.config.xml.XmlConfiguration,而后减少俩Environment相干的参数:

private final LoggingInitializationContext initializationContext;private final Environment environment;

接着在构造函数中减少initializationContext并注入:

public SpringBootXmlConfiguration(final LoggingInitializationContext initializationContext,            final LoggerContext loggerContext, final ConfigurationSource configSource) {        super(loggerContext, configSource);        this.initializationContext = initializationContext;        this.environment = initializationContext.getEnvironment();        ...}

最初只须要调整下面提到的递归解析办法,减少SpringProfile标签的反对即可:

private void constructHierarchy(final Node node, final Element element, boolean profileNode) {    //SpringProfile节点不须要解决属性    if (!profileNode) {        processAttributes(node, element);    }    final StringBuilder buffer = new StringBuilder();    final NodeList list = element.getChildNodes();    final List<Node> children = node.getChildren();    for (int i = 0; i < list.getLength(); i++) {        final org.w3c.dom.Node w3cNode = list.item(i);        if (w3cNode instanceof Element) {            final Element child = (Element) w3cNode;            final String name = getType(child);            //如果是<SpringProfile>标签,就跳过plugin的查找和解析            // Enhance log4j2.xml configuration            if (SPRING_PROFILE_TAG_NAME.equalsIgnoreCase(name)) {                //如果定义的Profile匹配以后激活的Profiles,就递归解析子节点,否则就跳过以后节点(和子节点)                if (acceptsProfiles(child.getAttribute("name"))) {                    constructHierarchy(node, child, true);                }                // Break <SpringProfile> node                continue;            }            //查找节点对应插件,解析节点,增加到node,构建rootElement树            //......    }}//判断profile是否合乎规定,从Spring Boot - Logback里复制的……private boolean acceptsProfiles(String profile) {    if (this.environment == null) {        return false;    }    String[] profileNames = StringUtils.trimArrayElements(StringUtils.commaDelimitedListToStringArray(profile));    if (profileNames.length == 0) {        return false;    }    return this.environment.acceptsProfiles(Profiles.of(profileNames));}

在配置SpringBootXmlConfiguration的入口

好了,功败垂成,就这么简略,这么点代码就实现了Log4j2 XML的加强。当初只须要在拆卸Log4j2的时候,将默认的XmlConfiguration换成我的SpringBootXmlConfiguration即可:

//org.springframework.boot.logging.log4j2.Log4J2LoggingSystem......LoggerContext ctx = getLoggerContext();URL url = ResourceUtils.getURL(location);ConfigurationSource source = getConfigurationSource(url);Configuration configuration;if (url.toString().endsWith("xml") && initializationContext != null) {    //XML文件并且initializationContext不为空时,就应用加强的SpringBootXmlConfiguration进行解析    configuration = new SpringBootXmlConfiguration(initializationContext, ctx, source);}else {    configuration = ConfigurationFactory.getInstance().getConfiguration(ctx, source);}......

筹备单元测试

性能曾经实现了,当初要筹备单元测试。这里还是能够参考Logback 相干的单元测试类,间接拷贝过去,批改成Log4j2的版本。

Spring Boot目前的版本应用的是Junit5,当初新建一个SpringBootXmlConfigurationTests类,而后模拟Logback的单元测试类写一堆测试方法和测试配置文件:

<!--profile-expression.xml--><springProfile name="production | test">  <logger name="org.springframework.boot.logging.log4j2" level="TRACE" /></springProfile><!--production-file.xml--><springProfile name="production">  <logger name="org.springframework.boot.logging.log4j2" level="TRACE" /></springProfile><!--multi-profile-names.xml--><springProfile name="production, test">  <logger name="org.springframework.boot.logging.log4j2" level="TRACE" /></springProfile><!--nested.xml--><springProfile name="outer">  <springProfile name="inner">    <logger name="org.springframework.boot.logging.log4j2" level="TRACE" />  </springProfile></springProfile>...
void profileActive();void multipleNamesFirstProfileActive();void multipleNamesSecondProfileActive();void profileNotActive();void profileExpressionMatchFirst();void profileExpressionMatchSecond();void profileExpressionNoMatch();void profileNestedActiveActive();void profileNestedActiveNotActive();......

折腾了一会,终于把单元测试编写实现,并全副测试通过。接下来能够筹备提PR了

提交PR

首先,在fork后的我的项目中,进行Pull request

而后,抉择要pr的分支,创立pr即可

而后须要具体填写你这个PR的形容

我具体的形容了我提交的性能,以及我下面剖析的兼容性和危险问题:

Enhance the configuration of log4j2 (xml), support Profile-specific Configuration (<SpringProfile>), consistent with logback extension.
Spring Boot currently only enhances the Logback (XML) configuration to support the tag. This feature is very useful, but is not supported by Log4j2.
I copied the code in Log4j2 XML to parse the XML configuration and created a new SpringBootXmlConfiguration to support the tag, which is as simple and easy to use as Logback Extension.
Compatibility issues with rewriting the Log4j2 parsing code:

  1. I just copied the XmlConfiguration code directly from Log4j2, adding very little code and making no big changes like formatting. If there is an update to Log4j2, it is easy to rewrite the parsing class and update it accordingly.
  2. The XmlConfiguration class in Log4j2 was last updated in June 2019, with no updates between [2.12.0,2.14.1] and the default dependent version of Log4j2 in Springboot (master) is 2.14.1

To sum up, there is no risk in this kind of enhancement

被冷酷有情的CI查看卡住

在提交PR后,我认为事件到这里就告一段落了……

后果Spring Boot的Github Action有一个CI查看,漫长的期待之后,通知我构建失败……

这里details能够进入详情查看具体构建日志

checkFormat/checkStyle 失败……

卧草粗心了,忘了有checkStyle了,这种开源我的项目对代码格调要求肯定很严格,我的代码是从Log4j2拷过去的,两个我的项目代码格调规范必定不一样!

调整代码格调

我又回过头去翻Spring Boot的奉献指南,发现他们提到了一个spring-javaformat插件,用于查看/格式化代码,Eclipse/Idea插件都有,还有gradle/maven插件。

我天真的认为,这个IDEA插件能够很不便的把我的代码格式化成Spring 的标准,装上之后,Reformat Code发现并没有什么卵用,依然过不了checkstyle………有晓得怎么用的同学,能够在评论区分享下

而后我就开始在本地执行它的checkstyle task,一直的调整代码格调……

这个checkstyle/checkformat的执行,是通过Gradle执行的,所以也能够在IDEA 的Gradle面板上执行:

Spring Boot的代码格调十分谨严,比方正文必须加句号啊,文件尾部必须空行结尾啊,导包程序要求啊,每行代码长度要求啊等等等等……十分多

在执行checkstyle/checkformat插件后,插件会提醒你哪个文件,哪一行有什么问题,跟着批改就行

通过我一个多小时的调整,终于通过了代码查看……眼镜都花了

再次提交代码

代码格调/格局调整实现后,我又一次的提交了代码,还是原来的分支。这里提交的话,那个PR里的CI查看会主动触发。

大略过了二十多分钟,终于构建实现,并且通过

来自官网人员的回复

过了三四天,我收到了官网人员的回复,随之而来的是我提交的PR被敞开了……

官网的回复态度还是很敌对的,大略意思是,无论我提交的代码稳定性如何,但这种暴力重写的形式还是不太好,他们心愿由Log4j2来提供一个扩大,而后Spring Boot通过扩大来实现对Log4j2的加强。

并且附上了一个issue,主题就是Spring Boot 对Log4j2反对的问题,并且追加了我这次的PR:
https://github.com/spring-projects/spring-boot/issues/22149

总结

尽管Spring Boot没有承受我奉献的代码,但并不是因为我的代码写的屎,而是这种形式侵入性太强,有危险,并不够敌对,通过扩大的形式去实现会更好。

这也体现了程序的扩展性是如许重要,在设计程序或者框架的时候,肯定要多思考扩展性,遵循开闭准则。

回绝了我的奉献也不要紧,至多Spring Boot官网理解到有这个需要,并且有现成的实现代码,日后有机会我还是会奉献其余的代码。

附录

  • [[PR地址] log4j2(xml) configuration enhancement, support <SpringProfile> tag](https://github.com/spring-pro...
  • 源码地址
原创不易,转载请分割作者。如果我的文章对您有帮忙,请点赞/珍藏/关注激励反对一下吧❤❤❤❤❤❤