给顶级开源项目 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>
<SpringProfile>
标签,然后贡献给Spring Boot,万一被采纳了岂不妙哉。功能开发
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 Element rootElement;
风险 & 兼容性的思考
XmlConfiguration
这个类的提交历史后发现,它最近一次更新的时间在2019年6月Fork一份 Spring Boot的代码 clone 这个fork的仓库 基于master,新建一个log4j2_enhancement分支用于开发
Spring Boot对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());
}
}
@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());
}
如法炮制,添加 Log4j2 的自定义扩展
org.apache.logging.log4j.core.config.xml.XmlConfiguration
,然后增加俩Environment相关的参数:private final LoggingInitializationContext initializationContext;
private final Environment environment;
public SpringBootXmlConfiguration(final LoggingInitializationContext initializationContext,
final LoggerContext loggerContext, final ConfigurationSource configSource) {
super(loggerContext, configSource);
this.initializationContext = initializationContext;
this.environment = initializationContext.getEnvironment();
...
}
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的入口
//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);
}
......
准备单元测试
<!--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的描述
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:
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.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.1To sum up, there is no risk in this kind of enhancement
被冷漠无情的CI检查卡住
调整代码风格
再次提交代码
来自官方人员的回复
总结
附录
https://github.com/spring-projects/spring-boot/pull/25873#issuecomment-812405988 https://github.com/kongwu-/spring-boot/tree/log4j2_enhancement https://github.com/spring-projects/spring-boot/issues/22149
评论