0
点赞
收藏
分享

微信扫一扫

给顶级开源项目 Spring Boot 贡献代码是一种什么样的体验?

boomwu 2021-10-09 阅读 79
java

背景

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的官方文档

但是有时候为了性能或其他原因,我们会选择log4j2作为Spring Boot的日志框架。Spirng Boot当然也是支持log4j2的。

切换到 log4j2 虽然很简单,但是Spring Boot并没有对 log4j2进行扩展!log4j2的xml配置方式,并不支持标签,不能愉快的配置多环境!搜索了一下,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 仓库地址:github.com/spring-proj…

  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的描述

我详细的描述了我提交的功能,以及我上面分析的兼容性和风险问题:

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: github.com/spring-proj…

总结

虽然Spring Boot没有接受我贡献的代码,但并不是因为我的代码写的屎 ?,而是这种方式侵入性太强,有风险,并不够友好,通过扩展的方式去实现会更好。

这也体现了程序的扩展性是多么重要,在设计程序或者框架的时候,一定要多考虑扩展性,遵循开闭原则。

这次拒绝了我的贡献也不要紧,至少Spring Boot官方了解到有这个需求,并且有现成的实现代码,日后有机会的话,我还是会继续贡献其他的代码。

原文链接:https://juejin.cn/post/6948419381462302751

最后小编还给大家整理了一份面试宝典 有需要的私信小助理【666】来领取!!


举报

相关推荐

0 条评论