1. 問題背景
某應用在啟動完提供JSF服務后,短時間內出現(xiàn)了大量的空指針異常。
分析日志,發(fā)現(xiàn)是服務依賴的藏經(jīng)閣配置數(shù)據(jù)未加載完成導致。即所謂的有損上線或者是直接發(fā)布,當應用啟動時,service還沒加載完,就開始對外提供服務,導致失敗調用。
關鍵代碼如下
數(shù)據(jù)的初始化加載是通過實現(xiàn)CommandLineRunner接口完成的
@Component
public class LoadSystemArgsListener implements CommandLineRunner {
@Resource
private CacheLoader cjgConfigCacheLoader;
@Override
public void run(String... args) {
// 加載藏經(jīng)閣配置
cjgConfigCacheLoader.refresh();
}
}
cjgConfigCacheLoader.refresh()方法內部會將數(shù)據(jù)加載到內存中
/** 藏經(jīng)閣配置數(shù)據(jù) key:租戶 value:配置數(shù)據(jù) */
public static Map cjgRuleConfigMap = new HashMap?>();
如果此時還未加載完數(shù)據(jù),調用cjgRuleConfigMap.get("301").getXX(),則會報空指針異常
總結根因:JSF Provider發(fā)布早于服務依賴的初始化數(shù)據(jù)加載,導致失敗調用
2. 問題解決
在解決此問題前,我們需要先回憶并熟悉下Spring Boot的啟動過程、JSF服務的發(fā)布過程
1)Spring Boot的啟動過程(版本2.0.7.RELEASE)
run方法,主要關注refreshContext(context)刷新上下文
public ConfigurableApplicationContext run(String... args) {
// 創(chuàng)建 StopWatch 實例:用于計算啟動時間
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
Collection exceptionReporters = new ArrayList?>();
configureHeadlessProperty();
// 獲取SpringApplicationRunListeners:這些監(jiān)聽器會在啟動過程的各個階段發(fā)送對應的事件
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(
args);
// 創(chuàng)建并配置Environment:包括準備好對應的`Environment`,以及將`application.properties`或`application.yml`中的配置項加載到`Environment`中
ConfigurableEnvironment environment = prepareEnvironment(listeners,
applicationArguments);
configureIgnoreBeanInfo(environment);
// 打印Banner:如果 spring.main.banner-mode 不為 off,則打印 banner
Banner printedBanner = printBanner(environment);
// 創(chuàng)建應用上下文:根據(jù)用戶的配置和classpath下的配置,創(chuàng)建合適的`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的創(chuàng)建、依賴注入、初始化,發(fā)布事件等
refreshContext(context);
afterRefresh(context, applicationArguments);
stopWatch.stop();
// 打印啟動信息:如果 spring.main.log-startup-info 為 true,則打印啟動信息
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass)
.logStarted(getApplicationLog(), stopWatch);
}
// 發(fā)布 ApplicationStartedEvent:通知所有的 SpringApplicationRunListeners 應用已經(jīng)啟動
listeners.started(context);
// 調用 Runner:調用所有的ApplicationRunner和CommandLineRunner
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, listeners);
throw new IllegalStateException(ex);
}
try {
// 運行中:通知所有的 SpringApplicationRunListeners 應用正在運行
listeners.running(context);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, null);
throw new IllegalStateException(ex);
}
return context;
}
refreshContext(context)內部調用refresh()方法,此方法主要關注
finishBeanFactoryInitialization(beanFactory) 實例化Bean 早于 finishRefresh() 發(fā)生
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// 準備刷新的上下文環(huán)境:設置啟動日期,激活上下文,清除原有的屬性源
prepareRefresh();
// 告訴子類啟動 'refreshBeanFactory()' 方法,創(chuàng)建一個新的bean工廠。
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
// 為 BeanFactory 設置上下文特定的后處理器:主要用于支持@Autowired和@Value注解
prepareBeanFactory(beanFactory);
try {
// 為 BeanFactory 的處理提供在子類中的后處理器。
postProcessBeanFactory(beanFactory);
// 調用所有注冊的 BeanFactoryPostProcessor Bean 的處理方法。
invokeBeanFactoryPostProcessors(beanFactory);
// 注冊 BeanPostProcessor 的處理器,攔截 Bean 創(chuàng)建。
registerBeanPostProcessors(beanFactory);
// 為此上下文初始化消息源。
initMessageSource();
// 為此上下文初始化事件多播器。
initApplicationEventMulticaster();
// 在特定的上下文子類中刷新之前的進一步初始化。
onRefresh();
// 檢查監(jiān)聽器 Bean 并注冊它們:注冊所有的ApplicationListenerbeans
registerListeners();
// 實例化所有剩余的(非延遲初始化)單例。
finishBeanFactoryInitialization(beanFactory);
// 完成刷新:發(fā)布ContextRefreshedEvent,啟動所有Lifecyclebeans,初始化所有剩余的單例(lazy-init 單例和非延遲初始化的工廠 beans)。
finishRefresh();
}
...
}
實例化Bean中,需熟悉Bean的生命周期(重要)

??
2)JSF Provider的發(fā)布過程(版本1.7.5-HOTFIX-T6)
類
com.jd.jsf.gd.config.spring.ProviderBean調用方法com.jd.jsf.gd.config.ProviderConfig#export進行發(fā)布
JSF源碼地址:
http://xingyun.jd.com/codingRoot/jsf/jsf-sdk?
public class ProviderBean extends ProviderConfig 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發(fā)布有兩個地方
Ⅰ、Bean的初始化過程(delay>=0)
實現(xiàn)InitializingBean接口,重寫afterPropertiesSet方法。這里會判斷是否延遲發(fā)布,如果大于等于0,則會此處進行發(fā)布。具體在export方法中,當delay>0,則會延遲發(fā)布,如配置5000,表示延遲5秒發(fā)布;當delay=0,則立即發(fā)布。
Ⅱ、監(jiān)聽ContextRefreshedEvent事件觸發(fā)(delay<0)
實現(xiàn)ApplicationListener接口,重寫onApplicationEvent方法。屬于事件ContextRefreshedEvent,當delay<-1,則會延遲發(fā)布,如配置-5000,表示延遲5秒發(fā)布;反之,則立即發(fā)布。
3)解決方案
場景1:XML方式自動發(fā)布Provider(常用)
由上面的介紹,了解到執(zhí)行順序:1.Bean初始化 > 2.ContextRefreshedEvent事件觸發(fā) > 3.調用ApplicationRunner或CommandLineRunner;
上面已經(jīng)知道Provider發(fā)布處于1、2過程,需避免使用方式3進行數(shù)據(jù)的初始化。
前提建議:delay默認配置為-1,可以不配置,或者配置負數(shù)。則JSF Provider發(fā)布則處于過程2,即監(jiān)聽ContextRefreshedEvent事件觸發(fā)
方式1:Bean的初始化過程中
解決方法:使用@PostConstruct注解、實現(xiàn)InitializingBean接口、配置init-method方法均可
@Component
public class DataLoader {
@PostConstruct
@Scheduled(cron = "${cron.config}")
public void loadData() {
// 數(shù)據(jù)加載
System.out.println("數(shù)據(jù)加載工作");
}
}
注意:該Bean如果依賴了其他Bean,需確保依賴Bean已實例化,否則會報空指針異常。
方式2:ContextRefreshedEvent事件觸發(fā)
ContextRefreshedEvent事件是如何發(fā)布的
調用過程
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()進行事件發(fā)布,getTaskExecutor()默認值是null(除自定義設置Executor對象),所有ApplicationListener實現(xiàn)類串行執(zhí)行onApplicationEvent方法。
getApplicationListeners(event, type)獲取所有的實現(xiàn)類,繼續(xù)向下看內部會調用
AnnotationAwareOrderComparator.sort(allListeners)對所有ApplicationListener進行排序,allListeners 是待排序的對象列表。該方法將根據(jù)對象上的排序注解或接口來確定排序順序,并返回一個按照指定順序排序的對象列表。具體來說,排序的規(guī)則如下:
1.首先,根據(jù)對象上的 @Order 注解的值進行排序。@Order 注解的值越小,排序優(yōu)先級越高。
2.如果對象上沒有 @Order 注解,或者多個對象的 @Order 注解值相同,則根據(jù)對象是否實現(xiàn)了 Ordered 接口進行排序。實現(xiàn)了 Ordered 接口的對象,可以通過 getOrder() 方法返回一個排序值。
3.如果對象既沒有 @Order 注解,也沒有實現(xiàn) Ordered 接口,則使用默認的排序值 LOWEST_PRECEDENCE(Integer.MAX_VALUE)。特別的:如果BeanA和BeanB排序值都是默認值,則保持原順序,即Bean的加載順序
總結:默認情況所有ApplicationListener實現(xiàn)類串行執(zhí)行onApplicationEvent方法,而順序取決于
AnnotationAwareOrderComparator.sort(allListeners),@Order 注解的值越小,排序優(yōu)先級越高
解決方法:使用@Order注解保證執(zhí)行順序早于ProviderBean
@Component
@Order(1)
public class DataLoader implements ApplicationListener {
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
// 數(shù)據(jù)準備
System.out.println("初始化工作");
}
}
此外帶有@SpringBootApplication的啟動類中實現(xiàn)也是可以的(在Spring Boot中默認使用基于注解的方式進行配置和管理Bean,所以注解定義的Bean會在XML定義的Bean之前被加載)
@SpringBootApplication
public class DemoApplication implements ApplicationListener {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
System.out.println("初始化工作");
}
}
場景2:API方式發(fā)布Provider(較少使用)
應用啟動完成后,先做初始化動作,完成后再手動發(fā)布Provider。這種就可以通過實現(xiàn)接口ApplicationRunner或接口CommandLineRunner去執(zhí)行初始化。
@Component
public class DataLoader implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
// 數(shù)據(jù)準備
System.out.println("初始化工作");
// 發(fā)布provider
// 參考:https://cf.jd.com/pages/viewpage.action?pageId=296129902
}
}
場景3:XML方式手動發(fā)布(不常用)
provider的dynamic屬性設置為false
屬性 |
類型 |
是否必填 |
默認值 |
描述 |
|
provider |
dynamic |
boolean |
否 |
true |
是否動態(tài)注冊Provider,默認為true,配置為false代表不主動發(fā)布,需要到管理端進行上線操作 |
3. 總結
RPC服務(如JSF、Dubbo)進行優(yōu)雅上線,常用的兩種方式:1、延遲發(fā)布 2、手動發(fā)動
如果你的服務需要一些初始化操作后才能對外提供服務,如初始化緩存(不限與藏經(jīng)閣、ducc、mysql、甚至調用其他jsf服務)、redis連接池等相關資源就位,可以參考本文中介紹的幾種方式。
此文是筆者通過讀取源碼+本地驗證得出的結論,如有錯誤遺漏或者更好的方案還煩請各位指出共同進步!
-
接口
+關注
關注
33文章
8918瀏覽量
153138 -
RPC
+關注
關注
0文章
111瀏覽量
11775
發(fā)布評論請先 登錄
一次消諧裝置與二次消諧裝置區(qū)別、一次消諧器與二次消諧器的區(qū)別

TCP三次握手的詳細過程
一次電源與二次電源有什么不同
一次電池分類以及應用場景詳解

電氣一次設備有哪些其功能是什么
labview如何做到一次觸發(fā)采集一次
在例程 ”BLE_GATTS_SPP“中,手機第一次連接藍牙服務時,怎么添加配對密碼?
記錄一次使用easypoi時與源碼博弈的過程

一次消諧器的構造
鴻蒙OS開發(fā):【一次開發(fā),多端部署】(視頻應用)

評論