给顶级开源项目 Spring Boot 贡献代码是一种什么样的体验?
大家好,我是D哥
Java面试那些事儿
回复关键字Java,领取大厂最新面试题和简历模板。同时还可以与30w+程序员一起学习技术干货,包括JVM、Spring Cloud、Intellij IDEA、Dubbo、Zookeeper、Redis、架构设计、微服务、Git等。
Official Account
# 背景
<Root level="INFO">
<!-- 开发环境使用Console Appender,生产环境使用File Appender -->
<springProfile name="dev">
<AppenderRef ref="Console"/>
</springProfile>
<SpringProfile name="!dev">
<AppenderRef ref="File"/>
</SpringProfile>
</Root>
# 功能开发
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;
想通过继承方式,只重写部分方法来实现根本不可能。
除非重写整个类才能扩展自定义的标签……
风险 & 兼容性的思考
-
Fork一份 Spring Boot的代码 -
clone 这个fork的仓库 -
基于master,新建一个log4j2_enhancement分支用于开发
Spring Boot对Logback的支持扩展
class SpringBootJoranConfigurator extends JoranConfigurator {
private LoggingInitializationContext initializationContext;
SpringBootJoranConfigurator(LoggingInitializationContext initializationContext) {
this.initializationContext = initializationContext;
}
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());
}
}
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 的自定义扩展
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
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.1 To 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