关于java:给顶级开源项目-Spring-Boot-贡献代码是一种什么样的体验

3次阅读

共计 11405 个字符,预计需要花费 29 分钟才能阅读完成。

背景

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 这个办法,增加自定义的标签名和行为类即可:

@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());
}

而后在 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…
  • 源码地址

原创不易,转载请分割作者。如果我的文章对您有帮忙,请点赞 / 珍藏 / 关注激励反对一下吧❤❤❤❤❤❤

正文完
 0