1. 程式人生 > >關於Springboot微服務使用Apollo配置管理中心的熱重新整理問題

關於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);
			}
		}
	}
}

這裡的程式碼只是給出大概思路,具體實現大家可以自己去編寫;

當然,雖然這種做法可以解決熱重新整理問題,也存在著一些問題,比如重新整理過程中會導致服務中斷;

但通常情況下,連線池的地址變更通常只發生在部署過程前後,在環境部署完畢之後應該不會有這種場景,還是有一些實用價值的;

如果大家有更好的方案,不防評論裡指點一下