1. 問題背景
某應用在啟動完提供JSF服務后,短時間內出現了大量的空指針異常。
分析日志,發現是服務依賴的藏經閣配置數據未加載完成導致。即所謂的有損上線或者是直接發布,當應用啟動時,service還沒加載完,就開始對外提供服務,導致失敗調用。
關鍵代碼如下
數據的初始化加載是通過實現CommandLineRunner接口完成的
@Component
public class LoadSystemArgsListener implements CommandLineRunner {
@Resource
private CacheLoader cjgConfigCacheLoader;
@Override
public void run(String... args) {
// 加載藏經閣配置
cjgConfigCacheLoader.refresh();
}
}
cjgConfigCacheLoader.refresh()方法內部會將數據加載到內存中
/** 藏經閣配置數據 key:租戶 value:配置數據 */
public static Map 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 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,則打印 banner
Banner 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和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() 發生
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 并注冊它們:注冊所有的ApplicationListenerbeans
registerListeners();
// 實例化所有剩余的(非延遲初始化)單例。
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 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發布有兩個地方
Ⅰ、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 {
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
// 數據準備
System.out.println("初始化工作");
}
}
此外帶有@SpringBootApplication的啟動類中實現也是可以的(在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方式發布Provider(較少使用)
應用啟動完成后,先做初始化動作,完成后再手動發布Provider。這種就可以通過實現接口ApplicationRunner或接口CommandLineRunner去執行初始化。
@Component
public class DataLoader implements ApplicationRunner {
@Override
public 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連接池等相關資源就位,可以參考本文中介紹的幾種方式。
此文是筆者通過讀取源碼+本地驗證得出的結論,如有錯誤遺漏或者更好的方案還煩請各位指出共同進步!
-
接口
+關注
關注
33文章
8596瀏覽量
151145 -
RPC
+關注
關注
0文章
111瀏覽量
11534
發布評論請先 登錄
相關推薦
評論