關於Springboot微服務使用Apollo配置管理中心的熱刷新問題

      近日,公司項目中使用攜程網的Apollo配置管理中心進行統一配置管理,爲了方便環境部署和運維,能避免不少配置問題致使的環境部署錯誤;不少網友估計都用過Apollo;java

在咱們項目組使用前作了一些預研,發現還須要解決鏈接池的熱刷新問題,不然意味着Apollo的portal界面上修改配置後還得重啓服務才能生效;spring

可能不少人會說,Apollo配置管理中心自己就支持配置的熱刷新,可是,這隻適用於普通應用場景(如一些不須要複雜的初始化操做的組件和spring-boot默認支持的組件);數據庫

對於Druid鏈接池、Jedis鏈接池、Lettuce鏈接池等等之類的組件,實現配置的熱刷新仍然須要本身作一些代碼適配;app

網上有熱心網友也給出了一些對通用配置的熱刷新代碼實現,這種方式對spring-boot默認集成的第三方組件是有效的,好比spring-boot 2.x的默認數據庫鏈接池 Hikari(其實查看源代碼就能發現,spring-boot爲它監聽了EnvironmentChangeEvent事件並實現了熱刷新邏輯);運維

那麼,對基於Apollo配置管理中心的Druid鏈接池、Jedis鏈接池、Lettuce鏈接池等等之類的客戶端組件熱刷新問題如何解決呢?ide

本文暫時只給出思路,詳細代碼後續會放出來;實現方法步驟以下:spring-boot

1. 編寫配置監聽器,並將Apollo客戶端的ConfigChangeEvent事件封裝爲EnvironmentChangeEvent事件,在Spring IOC容器中廣播出去;post

@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)等獲取最新配置ui

3. 編寫Spring事件監聽器,實現以下接口ApplicationListener<EnvironmentChangeEvent>,ApplicationContextAware, BeanDefinitionRegistryPostProcessor等;this

須要記錄 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);
			}
		}
	}
}

這裏的代碼只是給出大概思路,具體實現你們能夠本身去編寫;

固然,雖然這種作法能夠解決熱刷新問題,也存在着一些問題,好比刷新過程當中會致使服務中斷;

但一般狀況下,鏈接池的地址變動一般只發生在部署過程先後,在環境部署完畢以後應該不會有這種場景,仍是有一些實用價值的;

若是你們有更好的方案,不防評論裏指點一下,謝謝你們......

相關文章
相關標籤/搜索