记录一次RPC服务有损上线的分析过程

news/2025/2/23 7:09:55/文章来源:https://www.cnblogs.com/Jcloud/p/18567228

1. 问题背景

某应用在启动完提供JSF服务后,短时间内出现了大量的空指针异常。

分析日志,发现是服务依赖的藏经阁配置数据未加载完成导致。即所谓的有损上线或者是直接发布应用启动时,service还没加载完,就开始对外提供服务,导致失败调用

关键代码如下

数据的初始化加载是通过实现CommandLineRunner接口完成的

@Component
public class LoadSystemArgsListener implements CommandLineRunner {@Resourceprivate CacheLoader cjgConfigCacheLoader;@Overridepublic void run(String... args) {// 加载藏经阁配置cjgConfigCacheLoader.refresh();}
}

cjgConfigCacheLoader.refresh()方法内部会将数据加载到内存中

/** 藏经阁配置数据 key:租户 value:配置数据 */
public static Map<String, CjgRuleConfig> cjgRuleConfigMap = new HashMap<>();

如果此时还未加载完数据,调用cjgRuleConfigMap.get("301").getXX(),则会报空指针异常

总结根因:JSF Provider发布早于服务依赖的初始化数据加载,导致失败调用

 

2. 问题解决

在解决此问题前,我们需要先回忆并熟悉下Spring Boot的启动过程、JSF服务的发布过程

1)Spring Boot的启动过程(版本2.0.7.RELEASE)

run方法,主要关注refreshContext(context)刷新上下文

public ConfigurableApplicationContext run(String... args) {// 创建 StopWatch 实例:用于计算启动时间StopWatch stopWatch = new StopWatch();stopWatch.start();ConfigurableApplicationContext context = null;Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();configureHeadlessProperty();// 获取SpringApplicationRunListeners:这些监听器会在启动过程的各个阶段发送对应的事件SpringApplicationRunListeners listeners = getRunListeners(args);listeners.starting();try {ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);// 创建并配置Environment:包括准备好对应的`Environment`,以及将`application.properties`或`application.yml`中的配置项加载到`Environment`中ConfigurableEnvironment environment = prepareEnvironment(listeners,applicationArguments);configureIgnoreBeanInfo(environment);// 打印Banner:如果 spring.main.banner-mode 不为 off,则打印 bannerBanner printedBanner = printBanner(environment);// 创建应用上下文:根据用户的配置和classpath下的配置,创建合适的`ApplicationContext`context = createApplicationContext();exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,new Class[] { ConfigurableApplicationContext.class }, context);// 准备上下文:主要是将`Environment`、`ApplicationArguments`等关键属性设置到`ApplicationContext`中,以及加载`ApplicationListener`、`ApplicationRunner`、`CommandLineRunner`等。prepareContext(context, environment, listeners, applicationArguments,printedBanner);// 刷新上下文:这是Spring IoC容器启动的关键,包括Bean的创建、依赖注入、初始化,发布事件等refreshContext(context);afterRefresh(context, applicationArguments);stopWatch.stop();// 打印启动信息:如果 spring.main.log-startup-info 为 true,则打印启动信息if (this.logStartupInfo) {new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);}// 发布 ApplicationStartedEvent:通知所有的 SpringApplicationRunListeners 应用已经启动listeners.started(context);// 调用 Runner:调用所有的ApplicationRunner和CommandLineRunnercallRunners(context, applicationArguments);}catch(Throwable ex){handleRunFailure(context, ex, exceptionReporters, listeners);thrownewIllegalStateException(ex);}try{// 运行中:通知所有的 SpringApplicationRunListeners 应用正在运行listeners.running(context);}catch(Throwable ex){handleRunFailure(context, ex, exceptionReporters,null);thrownewIllegalStateException(ex);}return context;}

refreshContext(context)内部调用refresh()方法,此方法主要关注finishBeanFactoryInitialization(beanFactory) 实例化Bean 早于 finishRefresh() 发生

public void refresh() throws BeansException, IllegalStateException {synchronized (this.startupShutdownMonitor) {// 准备刷新的上下文环境:设置启动日期,激活上下文,清除原有的属性源prepareRefresh();// 告诉子类启动 'refreshBeanFactory()' 方法,创建一个新的bean工厂。ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();// 为 BeanFactory 设置上下文特定的后处理器:主要用于支持@Autowired和@Value注解prepareBeanFactory(beanFactory);try {// 为 BeanFactory 的处理提供在子类中的后处理器。postProcessBeanFactory(beanFactory);// 调用所有注册的 BeanFactoryPostProcessor Bean 的处理方法。invokeBeanFactoryPostProcessors(beanFactory);// 注册 BeanPostProcessor 的处理器,拦截 Bean 创建。registerBeanPostProcessors(beanFactory);// 为此上下文初始化消息源。initMessageSource();// 为此上下文初始化事件多播器。initApplicationEventMulticaster();// 在特定的上下文子类中刷新之前的进一步初始化。onRefresh();// 检查监听器 Bean 并注册它们:注册所有的ApplicationListenerbeansregisterListeners();// 实例化所有剩余的(非延迟初始化)单例。finishBeanFactoryInitialization(beanFactory);// 完成刷新:发布ContextRefreshedEvent,启动所有Lifecyclebeans,初始化所有剩余的单例(lazy-init 单例和非延迟初始化的工厂 beans)。finishRefresh();}...}

实例化Bean中,需熟悉Bean的生命周期(重要)

 


 

2)JSF Provider的发布过程(版本1.7.5-HOTFIX-T6)

类com.jd.jsf.gd.config.spring.ProviderBean调用方法com.jd.jsf.gd.config.ProviderConfig#export进行发布

JSF源码地址:http://xingyun.jd.com/codingRoot/jsf/jsf-sdk

public class ProviderBean<T> extends ProviderConfig<T> implements InitializingBean, DisposableBean, ApplicationContextAware, ApplicationListener, BeanNameAware {// 此处代码省略...public void onApplicationEvent(ApplicationEvent event) {if (event instanceof ContextRefreshedEvent && this.isDelay() && !this.exported && !CommonUtils.isUnitTestMode()) {LOGGER.info("JSF export provider with beanName {} after spring context refreshed.", this.beanName);if (this.delay < -1) {Thread thread = new Thread(new Runnable() {public void run() {try {Thread.sleep((long)(-ProviderBean.this.delay));} catch (Throwable var2) {}ProviderBean.this.export();}});thread.setDaemon(true);thread.setName("DelayExportThread");thread.start();} else {this.export();}}}private boolean isDelay() {return this.supportedApplicationListener && this.delay < 0;}public void afterPropertiesSet() throws Exception {// 此处代码省略...if (!this.isDelay() && !CommonUtils.isUnitTestMode()) {LOGGER.info("JSF export provider with beanName {} after properties set.", this.beanName);this.export();}}
}
public synchronized void export() throws InitErrorException {if (this.delay > 0) {Thread thread = new Thread(new Runnable() {public void run() {try {Thread.sleep((long)ProviderConfig.this.delay);} catch (Throwable var2) {}ProviderConfig.this.doExport();}});thread.setDaemon(true);thread.setName("DelayExportThread");thread.start();} else {this.doExport();}}

 

可以看出Provider发布有两个地方

Ⅰ、Bean的初始化过程(delay>=0)

实现InitializingBean接口,重写afterPropertiesSet方法。这里会判断是否延迟发布,如果大于等于0,则会此处进行发布。具体在export方法中,当delay>0,则会延迟发布,如配置5000,表示延迟5秒发布;当delay=0,则立即发布。

 

Ⅱ、监听ContextRefreshedEvent事件触发(delay<0)

实现ApplicationListener接口,重写onApplicationEvent方法。属于事件ContextRefreshedEvent,当delay<-1,则会延迟发布,如配置-5000,表示延迟5秒发布;反之,则立即发布。

 

3)解决方案

 

场景1:XML方式自动发布Provider(常用)

由上面的介绍,了解到执行顺序1.Bean初始化 > 2.ContextRefreshedEvent事件触发 > 3.调用ApplicationRunner或CommandLineRunner;

上面已经知道Provider发布处于1、2过程,需避免使用方式3进行数据的初始化。

前提建议:delay默认配置为-1,可以不配置,或者配置负数。则JSF Provider发布则处于过程2,即监听ContextRefreshedEvent事件触发

 

方式1:Bean的初始化过程中

解决方法:使用@PostConstruct注解、实现InitializingBean接口、配置init-method方法均可
@Component
public class DataLoader {@PostConstruct@Scheduled(cron = "${cron.config}")public void loadData() {// 数据加载System.out.println("数据加载工作");}}

注意:该Bean如果依赖了其他Bean,需确保依赖Bean已实例化,否则会报空指针异常。

 

方式2:ContextRefreshedEvent事件触发

ContextRefreshedEvent事件是如何发布的

调用过程 AbstractApplicationContext#finishRefresh -> AbstractApplicationContext#publishEvent-> SimpleApplicationEventMulticaster#multicastEvent

public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));for (final ApplicationListener<?> listener : getApplicationListeners(event, type)) {Executor executor = getTaskExecutor();if (executor != null) {executor.execute(() -> invokeListener(listener, event));}else {invokeListener(listener, event);}}
}

SimpleApplicationEventMulticaster的multicastEvent方法中调用invokeListener()进行事件发布getTaskExecutor()默认值是null(除自定义设置Executor对象),所有ApplicationListener实现类串行执行onApplicationEvent方法。

getApplicationListeners(event, type)获取所有的实现类,继续向下看内部会调用AnnotationAwareOrderComparator.sort(allListeners)对所有ApplicationListener进行排序,allListeners 是待排序的对象列表。该方法将根据对象上的排序注解或接口来确定排序顺序,并返回一个按照指定顺序排序的对象列表。具体来说,排序的规则如下:

1.首先,根据对象上的 @Order 注解的值进行排序。@Order 注解的值越小,排序优先级越高
2.如果对象上没有 @Order 注解,或者多个对象的 @Order 注解值相同,则根据对象是否实现了 Ordered 接口进行排序。实现了 Ordered 接口的对象,可以通过 getOrder() 方法返回一个排序值。
3.如果对象既没有 @Order 注解,也没有实现 Ordered 接口,则使用默认的排序值 LOWEST_PRECEDENCE(Integer.MAX_VALUE)。特别的:如果BeanA和BeanB排序值都是默认值,则保持原顺序,即Bean的加载顺序

 

总结:默认情况所有ApplicationListener实现类串行执行onApplicationEvent方法,而顺序取决于AnnotationAwareOrderComparator.sort(allListeners),@Order 注解的值越小,排序优先级越高

解决方法:使用@Order注解保证执行顺序早于ProviderBean
@Component
@Order(1)
public class DataLoader implements ApplicationListener<ContextRefreshedEvent> {@Overridepublic void onApplicationEvent(ContextRefreshedEvent event) {// 数据准备System.out.println("初始化工作");}
}

此外带有@SpringBootApplication的启动类中实现也是可以的(在Spring Boot中默认使用基于注解的方式进行配置和管理Bean,所以注解定义的Bean会在XML定义的Bean之前被加载)

@SpringBootApplication
public class DemoApplication implements ApplicationListener<ContextRefreshedEvent> {public static void main(String[] args) {SpringApplication.run(DemoApplication.class, args);}@Overridepublic void onApplicationEvent(ContextRefreshedEvent event) {System.out.println("初始化工作");}
}

场景2:API方式发布Provider(较少使用)

应用启动完成后,先做初始化动作,完成后再手动发布Provider。这种就可以通过实现接口ApplicationRunner或接口CommandLineRunner去执行初始化。

@Component
public class DataLoader implements ApplicationRunner {@Overridepublic void run(ApplicationArguments args) throws Exception {// 数据准备System.out.println("初始化工作");// 发布provider// 参考:https://cf.jd.com/pages/viewpage.action?pageId=296129902}
}

场景3:XML方式手动发布(不常用)

provider的dynamic属性设置为false

标签 属性 类型 是否必填 默认值 描述
provider dynamic boolean true 是否动态注册Provider,默认为true,配置为false代表不主动发布,需要到管理端进行上线操作

 

3. 总结

RPC服务(如JSF、Dubbo)进行优雅上线,常用的两种方式:1、延迟发布 2、手动发动

如果你的服务需要一些初始化操作后才能对外提供服务,如初始化缓存(不限与藏经阁、ducc、mysql、甚至调用其他jsf服务)、redis连接池等相关资源就位,可以参考本文中介绍的几种方式。

此文是笔者通过读取源码+本地验证得出的结论,如有错误遗漏或者更好的方案还烦请各位指出共同进步!

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/840783.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

阿里二面:如何设计一个高并发系统?

大家好,我是苏三,又跟大家见面了。 前言 最近有位粉丝问了我一个问题:如何设计一个高并发系统? 这是一个非常高频的面试题,面试官可以从多个角度,考查技术的广度和深度。 今天这篇文章跟大家一起聊聊高并发系统设计一些关键点,希望对你会有所帮助。1 页面静态化 对于高并…

Linux之CPU调度策略和CPU亲和性

一、调度策略 调度进程 单个 CPU一次只能执行一个进程,虽然 Linux 系统通过使用多任务同时处理多个进程,但当多个进程同时运行在一个CPU 上时,它通过交错执行这些进程。 内核使用进程调度器来决定在某一时间点上哪个进程在运行。调度器必须平衡几个选项:快速决定下一个该运…

Linux之CPU调度策略

调度进程 单个 CPU一次只能执行一个进程,虽然 Linux 系统通过使用多任务同时处理多个进程,但当多个进程同时运行在一个CPU 上时,它通过交错执行这些进程。 内核使用进程调度器来决定在某一时间点上哪个进程在运行。调度器必须平衡几个选项:快速决定下一个该运行的进程进程可…

2025年IT项目经理必看!9大项目管理平台完全对比,选错软件你后悔一辈子!

一、引言2025 年,IT 项目管理面临着更多的挑战和机遇。选择合适的项目管理平台对于 IT 项目经理来说至关重要,一个好的平台能够提高项目管理效率,确保项目顺利进行。本文将对 2025 年九大热门项目管理平台进行全面对比,帮助 IT 项目经理做出明智的选择。 在当今数字化时代,…

Unity版本使用情况统计(更新至2024年11月)

UWA发布|本期UWA发布的内容是第十五期Unity版本使用统计,统计周期为2024年5月至2024年11月,数据来源于UWA网站(www.uwa4d.com)性能诊断提测的项目。希望给Unity开发者提供相关的行业趋势作为参考。2024年5月 - 2024年11月版本分布 以近半年的数据统计来看,如图1所示,2…

sql学习~

一.mysql数据模型二.sql简介三、mysql通用语法 1.注释 单行注释。-- 内容(注意空格) 多行注释。/* 内容 */ 2.不区分大小写 3.分号结尾。 四、sql分类1.DDL 1>进入mysql mysql -uroot -p+密码 2>操作数据库查询数据库show databases; 创建数据库create database 数据库…

Qt 打包为可执行文件(详解)

https://blog.csdn.net/dfr110719/article/details/136992619<div id="content_views" class="htmledit_views"><p id="main-toc"><strong>目录</strong></p> 一.打包为文件夹 二.打包为可执行文件(.exe) 当我…

关于train, evaluate 和 作图——dymean3

TMscore 从代码实现来看,这里的 TMscore 计算是通过调用外部的 TMscore 可执行程序完成的,输入的 PDB 文件包含了原子的三维坐标信息。那么具体答案可以分以下几个方面来分析: 1. 是否只对 CA 坐标进行计算答案:否。 TMscore 通常会基于整个 PDB 文件中所有原子的坐标计算分…

微信小程序生成朋友圈分享图/海报

效果图海报生成原文链接:https://developers.weixin.qq.com/community/develop/article/doc/000ac686c5c5506f18b87ee825b013demo 代码片段:https://developers.weixin.qq.com/s/J38pKsmK7Qw5小程序海报生成工具链接:https://developers.weixin.qq.com/community/develop/ar…

嵌入式Linux系统构建

参考资料 本篇内容主要参考 韦东山的《嵌入式Linux应用开发完全手册V5.2_IMX6ULL_Pro开发板.pdf》 具体课程见 百问网嵌入式专家-韦东山嵌入式专注于嵌入式课程及硬件研发 实践环境为百问网官方开发板 100ASK_IMX6ULL-Pro 目标系统组成 Linux系统启动流程一个有效的根文件系统集…

OSG开发笔记(三十四): OsgUtil::Simplifier:简化几何体,提升显示性能和渲染效率

前言对于一些较大的图形,会出现显示卡顿和渲染缓慢的问题,这时候就要使用到osgUtil::Simplifier简化器,来对其进行简化。 Demo osgUtilosgUtil库是osg的四大核心库之一,OSG 核心库提供了用于场景图形操作的核心场景图形功能、类和方法;开发3D图形程序所需的某些特定功能函…

带排序功能的js masonry瀑布流插件

在线预览 下载sortableJs是一款带排序功能的js masonry瀑布流插件。sortableJs能够使元素以卡片形式显示,并以masonry瀑布流方式进行布局,通过点击分类按钮,可以将卡片按指定的方式动态排序。使用方法 在页面中引入sortable.min.css和sortable.min.js文件。< link rel=…