關於Springboot微服務使用Apollo配置管理中心的熱重新整理問題
近日,公司專案中使用攜程網的Apollo配置管理中心進行統一配置管理,為了方便環境部署和運維,能避免很多配置問題導致的環境部署錯誤;很多網友估計都用過Apollo;
在我們專案組使用前做了一些預研,發現還需要解決連線池的熱重新整理問題,否則意味著Apollo的portal介面上修改配置後還得重啟服務才能生效;
可能很多人會說,Apollo配置管理中心本身就支援配置的熱重新整理,但是,這隻適用於普通應用場景(如一些不需要複雜的初始化操作的元件和spring-boot預設支援的元件);
對於Druid連線池、Jedis連線池、Lettuce連線池等等之類的元件,實現配置的熱重新整理仍然需要自己做一些程式碼適配;
網上有熱心網友也給出了一些對通用配置的熱重新整理程式碼實現,這種方式對spring-boot預設整合的第三方元件是有效的,比如spring-boot 2.x的預設資料庫連線池 Hikari(其實檢視原始碼就能發現,spring-boot為它監聽了EnvironmentChangeEvent事件並實現了熱重新整理邏輯);
那麼,對基於Apollo配置管理中心的Druid連線池、Jedis連線池、Lettuce連線池等等之類的客戶端元件熱重新整理問題如何解決呢?
本文暫時只給出思路,詳細程式碼後續會放出來;實現方法步驟如下:
1. 編寫配置監聽器,並將Apollo客戶端的ConfigChangeEvent事件封裝為EnvironmentChangeEvent事件,在Spring IOC容器中廣播出去;
@Component public class ApolloConfigListener implements ApplicationContextAware { private static final Logger LOGGER = LoggerFactory.getLogger(ApolloConfigListener.class); @Autowired private ApplicationContext applicationContext = null; @Autowired private RefreshScope refreshScope = null; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } @ApolloConfigChangeListener public void onChange(ConfigChangeEvent changeEvent) { this.refreshProperties(changeEvent); } public void refreshProperties(ConfigChangeEvent changeEvent) { this.applicationContext.publishEvent(new EnvironmentChangeEvent(changeEvent.changedKeys())); this.refreshScope.refreshAll(); } }
2. 使用 @ApolloConfig 或者API方式 ConfigService.getConfig(namespace)等獲取最新配置
3. 編寫Spring事件監聽器,實現如下介面ApplicationListener<EnvironmentChangeEvent>,ApplicationContextAware, BeanDefinitionRegistryPostProcessor等;
需要記錄 BeanDefinitionRegistry 和 ApplicationContext;
在監聽到 EnvironmentChangeEvent事件中,進行如下邏輯處理:
判斷配置變化的key是否跟對應的連線池元件相關
如果不相關,則忽略;否則,重新整理IOC容器,即通過記錄的 BeanDefinitionRegistry 引用刪除連線池對應的 BeanDefinition,並重新根據Apollo客戶端接收到的最新配置重建IOC容器中跟連線池相關的所有bean;然後,重新整理DI,即掃描類路徑下引用的連線池相關Bean的類,重新整理其依賴關係;
public abstract class AbstractRefreshListener implements ApplicationListener<EnvironmentChangeEvent>,
ApplicationContextAware, BeanDefinitionRegistryPostProcessor {
private static final Logger LOGGER = LoggerFactory.getLogger(AbstractRefreshListener.class);
protected ApplicationContext applicationContext = null;
protected BeanDefinitionRegistry registry = null;
public static interface DIFilter {
public boolean accept(Class<?> clazz);
}
@Override
public final void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
@Override
public final void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
this.registry = registry;
}
@Override
public final void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
String[] beanDefinitionNames = beanFactory.getBeanDefinitionNames();
LOGGER.info("----------------------------------------------------");
LOGGER.info("AbstractRefreshListener.postProcessBeanFactory() register beans:");
for (String beanDefinitionName : beanDefinitionNames) {
LOGGER.info("beanDefinitionName=" + beanDefinitionName);
}
LOGGER.info(
"AbstractRefreshListener.postProcessBeanFactory() register bean count=" + beanDefinitionNames.length);
LOGGER.info("----------------------------------------------------");
}
@Override
public void onApplicationEvent(EnvironmentChangeEvent changeEvent) {
if (this.needRefresh(changeEvent)) {
this.refreshIOC(changeEvent);
}
}
protected boolean needRefresh(EnvironmentChangeEvent changeEvent) {
boolean flag = (changeEvent != null && this.registry != null);
return flag;
}
protected abstract void refreshIOC(EnvironmentChangeEvent changeEvent);
protected void refreshDI(List<Class<?>> classList, DIFilter filter) {
classList = Optional.ofNullable(classList).orElse(new ArrayList<Class<?>>());
for (Class<?> item : classList) {
try {
if (filter != null && filter.accept(item)) {
this.applicationContext.getBean(item);
}
} catch (Throwable th) {
LOGGER.error("AbstractRefreshListener.refreshDI() Throwable", th);
}
}
}
}
這裡的程式碼只是給出大概思路,具體實現大家可以自己去編寫;
當然,雖然這種做法可以解決熱重新整理問題,也存在著一些問題,比如重新整理過程中會導致服務中斷;
但通常情況下,連線池的地址變更通常只發生在部署過程前後,在環境部署完畢之後應該不會有這種場景,還是有一些實用價值的;
如果大家有更好的方案,不防評論裡指點一下