Spring 应用合并之路(二):峰回路转,柳暗花明 | 京东云技术团队

书接上文,前面在 [Spring 应用合并之路(一):摸石头过河]介绍了几种不成功的经验,下面继续折腾...

四、仓库合并,独立容器

在经历了上面的尝试,在同事为啥不搞两个独立的容器提醒下,决定抛开 Spring Boot 内置的父子容器方案,完全自己实现父子容器。

如何加载 web 项目?

现在的难题只有一个:如何加载 web 项目?加载完成后,如何持续持有 web 项目?经过思考后,可以创建一个 boot 项目的 Spring Bean,在该 Bean 中加载并持有 web 项目的容器。由于 Spring Bean 默认是单例的,并且会伴随 Spring 容器长期存活,就可以保证 web 容器持久存活。结合 Spring 扩展点概览及实践 中介绍的 Spring 扩展点,有两个地方可以利用:

1.可以利用 ApplicationContextAware 获取 boot 容器的 ApplicationContext 实例,这样就可以实现自己实现的父子容器;

2.可以利用 ApplicationListener 获取 ContextRefreshedEvent 事件,该事件表示容器已经完成初始化,可以提供服务。在监听到该事件后,来进行 web 容器的加载。

思路确定后,代码实现就很简单了:

java 复制代码
package com.diguage.demo.boot.config;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.stereotype.Component;

/**
 * @author D瓜哥 · https://www.diguage.com
 */
@Component
public class WebLoaderListener implements ApplicationContextAware,
        ApplicationListener<ApplicationEvent> {
    private static final Logger logger = LoggerFactory.getLogger(WebLoaderListener.class);

    /**
     * 父容器,加载 boot 项目
     */
    private static ApplicationContext parentContext;

    /**
     * 子容器,加载 web 项目
     */
    private static ApplicationContext childContext;

    @Override
    public void setApplicationContext(ApplicationContext ctx) throws BeansException {
        WebLoaderListener.parentContext = ctx;
    }

    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        logger.info("receive application event: {}", event);
        if (event instanceof ContextRefreshedEvent) {
            WebLoaderListener.childContext = new ClassPathXmlApplicationContext(
                    new String[]{"classpath:web/spring-cfg.xml"},
                    WebLoaderListener.parentContext);
        }
    }
}

容器重复加载的问题

这次自己实现的父子容器,如同设想的那样,没有同名 Bean 的检查,省去了很多麻烦。但是,观察日志,会发现 com.diguage.demo.boot.config.WebLoaderListener#onApplicationEvent 方法被两次执行,也就是监听到了两次 ContextRefreshedEvent 事件,导致 web 容器会被加载两次。由于项目的 RPC 服务不能重复注册,第二次加载抛出异常,导致启动失败。

最初,怀疑是 web 容器,加载了 WebLoaderListener,但是跟踪代码,没有发现 childContext 容器中有 WebLoaderListener 的相关 Bean。

昨天做了个小实验,又调试了一下 Spring 的源代码,发现了其中的奥秘。直接贴代码吧:

SPRING/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java

kotlin 复制代码
/**
 * Publish the given event to all listeners.
 * <p>This is the internal delegate that all other {@code publishEvent}
 * methods refer to. It is not meant to be called directly but rather serves
 * as a propagation mechanism between application contexts in a hierarchy,
 * potentially overridden in subclasses for a custom propagation arrangement.
 * @param event the event to publish (may be an {@link ApplicationEvent}
 * or a payload object to be turned into a {@link PayloadApplicationEvent})
 * @param typeHint the resolved event type, if known.
 * The implementation of this method also tolerates a payload type hint for
 * a payload object to be turned into a {@link PayloadApplicationEvent}.
 * However, the recommended way is to construct an actual event object via
 * {@link PayloadApplicationEvent#PayloadApplicationEvent(Object, Object, ResolvableType)}
 * instead for such scenarios.
 * @since 4.2
 * @see ApplicationEventMulticaster#multicastEvent(ApplicationEvent, ResolvableType)
 */
protected void publishEvent(Object event, @Nullable ResolvableType typeHint) {
    Assert.notNull(event, "Event must not be null");
    ResolvableType eventType = null;

    // Decorate event as an ApplicationEvent if necessary
    ApplicationEvent applicationEvent;
    if (event instanceof ApplicationEvent applEvent) {
        applicationEvent = applEvent;
        eventType = typeHint;
    }
    else {
        ResolvableType payloadType = null;
        if (typeHint != null && ApplicationEvent.class.isAssignableFrom(typeHint.toClass())) {
            eventType = typeHint;
        }
        else {
            payloadType = typeHint;
        }
        applicationEvent = new PayloadApplicationEvent<>(this, event, payloadType);
    }

    // Determine event type only once (for multicast and parent publish)
    if (eventType == null) {
        eventType = ResolvableType.forInstance(applicationEvent);
        if (typeHint == null) {
            typeHint = eventType;
        }
    }

    // Multicast right now if possible - or lazily once the multicaster is initialized
    if (this.earlyApplicationEvents != null) {
        this.earlyApplicationEvents.add(applicationEvent);
    }
    else if (this.applicationEventMulticaster != null) {
        this.applicationEventMulticaster.multicastEvent(applicationEvent, eventType);
    }

    // Publish event via parent context as well...
    // 如果有父容器,则也将事件发布给父容器。
    if (this.parent != null) {
        if (this.parent instanceof AbstractApplicationContext abstractApplicationContext) {
            abstractApplicationContext.publishEvent(event, typeHint);
        }
        else {
            this.parent.publishEvent(event);
        }
    }
}

publishEvent 方法的最后,如果父容器不为 null 的情况下,则也会向父容器广播容器的相关事件。

看到这里就清楚了,不是 web 容器持有了 WebLoaderListener 这个 Bean,而是 web 容器主动向父容器广播了 ContextRefreshedEvent 事件。

容器销毁

除了上述问题,还有一个问题需要思考:如何销毁 web 容器?如果不能销毁容器,会有一些意想不到的问题。比如,注册中心的 RPC 提供方不能及时销毁等等。

这里的解决方案也比较简单:同样基于事件监听,Spring 容器销毁会有 ContextClosedEvent 事件,在 WebLoaderListener 中监听该事件,然后调用 AbstractApplicationContext#close 方法就可以完成 Spring 容器的销毁工作。

父子容器加载及销毁

结合上面的所有论述,完整的代码如下:

java 复制代码
package com.diguage.demo.boot.config;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.stereotype.Component;

import java.util.Objects;

/**
 * 基于事件监听的 web 项目加载器
 *
 * @author D瓜哥 · https://www.diguage.com
 */
@Component
public class WebLoaderListener implements ApplicationContextAware,
        ApplicationListener<ApplicationEvent> {
    private static final Logger logger = LoggerFactory.getLogger(WebLoaderListener.class);

    /**
     * 父容器,加载 boot 项目
     */
    private static ApplicationContext parentContext;

    /**
     * 子容器,加载 web 项目
     */
    private static ClassPathXmlApplicationContext childContext;

    @Override
    public void setApplicationContext(ApplicationContext ctx) throws BeansException {
        WebLoaderListener.parentContext = ctx;
    }

    /**
     * 事件监听
     *
     * @author D瓜哥 · https://www.diguage.com
     */
    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        logger.info("receive application event: {}", event);
        if (event instanceof ContextRefreshedEvent refreshedEvent) {
            ApplicationContext context = refreshedEvent.getApplicationContext();
            if (Objects.equals(WebLoaderListener.parentContext, context)) {
                // 加载 web 容器
                WebLoaderListener.childContext = new ClassPathXmlApplicationContext(
                        new String[]{"classpath:web/spring-cfg.xml"},
                        WebLoaderListener.parentContext);
            }
        } else if (event instanceof ContextClosedEvent) {
            // 处理容器销毁事件
            if (Objects.nonNull(WebLoaderListener.childContext)) {
                synchronized (WebLoaderListener.class) {
                    if (Objects.nonNull(WebLoaderListener.childContext)) {
                        AbstractApplicationContext ctx = WebLoaderListener.childContext;
                        WebLoaderListener.childContext = null;
                        ctx.close();
                    }
                }
            }
        }
    }
}

五、参考资料

1.Spring 扩展点概览及实践 - "地瓜哥"博客网

2.Context Hierarchy with the Spring Boot Fluent Builder API

3.How to revert initial git commit?

作者:京东科技 李君

来源:京东云开发者社区 转载请注明来源

相关推荐
姑苏洛言3 分钟前
扫码小程序实现仓库进销存管理中遇到的问题 setStorageSync 存储大小限制错误解决方案
前端·后端
光而不耀@lgy18 分钟前
C++初登门槛
linux·开发语言·网络·c++·后端
方圆想当图灵37 分钟前
由 Mybatis 源码畅谈软件设计(七):SQL “染色” 拦截器实战
后端·mybatis·代码规范
毅航1 小时前
MyBatis 事务管理:一文掌握Mybatis事务管理核心逻辑
java·后端·mybatis
我的golang之路果然有问题1 小时前
速成GO访问sql,个人笔记
经验分享·笔记·后端·sql·golang·go·database
柏油1 小时前
MySql InnoDB 事务实现之 undo log 日志
数据库·后端·mysql
写bug写bug3 小时前
Java Streams 中的7个常见错误
java·后端
Luck小吕3 小时前
两天两夜!这个 GB28181 的坑让我差点卸载 VSCode
后端·网络协议
M1A14 小时前
全栈开发必备:Windows安装VS Code全流程
前端·后端·全栈
蜗牛快跑1234 小时前
github 源码阅读神器 deepwiki,自动生成源码架构图和知识库
前端·后端