书接上文,前面在 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 容器的加载。

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

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

/**
* 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 ApplicationEvent.class.isAssignableFrom(typeHint.toClass())) {
eventType = typeHint;
}
else {
payloadType = typeHint;
}
applicationEvent (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 容器的销毁工作。

父子容器加载及销毁

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

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

五、参考资料

2.Context Hierarchy with the Spring Boot Fluent Builder API
3.How to revert initial git commit?

作者:京东科技 李君

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

Spring 应用合并之路(二):峰回路转,柳暗花明 | 京东云技术团队的更多相关文章

  1. Spring Boot 学习之路二 配置文件 application.yml

    一.创建配置文件 如图所示,我们在resources文件夹中新建配置文件application.yml   结构图 二.一些基本配置 server: port: 8090 //配置端口 session ...

  2. .net core+Spring Cloud学习之路 二

    前言: 原本计划这次写一下搭建eureka群集.但是发现上次写的只是服务的注册,忘了写服务的发现,所以这次先把服务发现补上去. 我们基于上篇文章,再新建两个.net core web api项目,分别 ...

  3. Spring Cloud进阶之路 | 二:服务提供者(discovery)

    1 创建父项目 以前文所述,以spring boot 2.1.7.RELEASE为基,spring cloud版本为Greenwich.SR2,spring cloud alibaba版本为2.1.0 ...

  4. Spring Cloud gateway 网关服务二 断言、过滤器

    微服务当前这么火爆的程度,如果不能学会一种微服务框架技术.怎么能升职加薪,增加简历的筹码?spring cloud 和 Dubbo 需要单独学习.说没有时间?没有精力?要学俩个框架?而Spring C ...

  5. 我的VSTO之路(二):VSTO程序基本知识

    原文:我的VSTO之路(二):VSTO程序基本知识 开始之前,首先我介绍一下我的开发环境:VS2010 + Office 2010,是基于.Net framework 4.0和VSTO 4.0.以下的 ...

  6. Java Spring Boot VS .NetCore (二)实现一个过滤器Filter

    Java Spring Boot VS .NetCore (一)来一个简单的 Hello World Java Spring Boot VS .NetCore (二)实现一个过滤器Filter Jav ...

  7. spring事务详解(二)简单样例

    系列目录 spring事务详解(一)初探事务 spring事务详解(二)简单样例 spring事务详解(三)源码详解 spring事务详解(四)测试验证 spring事务详解(五)总结提高 一.引子 ...

  8. idea+maven+spring+cxf创建webservice应用(二)生成客户端程序

    idea+maven+spring+cxf创建webservice应用(二)生成客户端程序,以上一篇为基础"idea+maven+spring+cxf创建webservice应用" ...

  9. Spring框架学习之IOC(二)

    Spring框架学习之IOC(二) 接着上一篇的内容,下面开始IOC基于注解装配相关的内容 在 classpath 中扫描组件 <context:component-scan> 特定组件包 ...

  10. 《Spring Cloud》学习(二) 负载均衡!

    第二章 负载均衡 负载均衡是对系统的高可用.网络压力的缓解和处理能力扩容的重要手段之一.Spring Cloud Ribbon是一个基于 HTTP 和 TCP 的客户端负载均衡工具,它基于Netfli ...

随机推荐

  1. 如何极速极速搭建个人博客?Copy攻城狮用的这一招很优秀!

    摘要:在中国功夫中,"天下武功,无坚不摧,唯快不破",在编程的世界里,如何快速搭建一个属于自己的博客呢?那么 Pagic + Vercel 应该是个不错的选择!接下来,由Copy攻 ...

  2. 教你如何在Python中读,写和解析CSV文

    摘要:在这篇文章中关于"在Python如何阅读CSV文件"中,我们将学习如何读,写和解析的CSV文件的Python. 您知道将表格数据存储到纯文本文件背后的机制是什么吗?答案是CS ...

  3. Intellij IDEA 开启 RunDashboard

    1. 关闭 Intellij IDEA (2018以下的版本无效),打开 workspace.xml 找到  RunDashboard 节点.添加如果配置 <option name=" ...

  4. SpringBoot 开发环境热部署

    开发修改代码后,无需重启idea的服务. 1 模块中添加依赖 <dependency> <groupId>org.springframework.boot</groupI ...

  5. SpringBoot Jar 包太大 瘦身 【初试】

    SpringBoot Jar 包太大 瘦身,建议使用时,参考: SpringBoot Jar 包太大 瘦身 [终极版] 29M, 排除少量JAR包方式 打包,排除指定jar 包 <build&g ...

  6. C++ Idioms Pimpl

    References C++ Coding Standard 這本書的中文版不知道是不是翻譯問題,還是原作就有這種傾向,有些咬文嚼字的很不好懂. Exceptional C++ 這本比上面那本容易理解 ...

  7. 【Boost】Windows 下个人在配置 Boost 踩到的坑以及解决方案

    要编译的 Boost 版本:1.82.0 假设 Boost 的根目录为 ${boost_root}$ 如果不想编译可以使用博主编译后的Boost:https://pan.baidu.com/s/1s2 ...

  8. AtCoder Regular Contest 119 (ABC题)

    比赛链接:Here A - 119 × 2^23 + 1 注意到 \(2^{60} > 10^{18}\)​ ,所以我们可以直接枚举 \(0\) ~ \(59\) int main() { ci ...

  9. Android NativeCrash 捕获与解析

    Android 开发中,NE一直是不可忽略却又异常难解的一个问题,原因是这里面涉及到了跨端开发和分析,需要同时熟悉 Java,C&C++,并且需要熟悉 NDK开发,并且解决起来不像 Java异 ...

  10. springboot项目统一封装返回值和异常处理(方式一)

    为什么要统一返回值: 在我们做后端应用的时候,前后端分离的情况下,我们经常会定义一个数据格式,通常会包含code,message,data这三个必不可少的信息来方便我们的交流,下面我们直接来看代码pa ...