近日,公司项目中使用携程网的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);
}
}
}
}
这里的代码只是给出大概思路,具体实现大家可以自己去编写;
当然,虽然这种做法可以解决热刷新问题,也存在着一些问题,比如刷新过程中会导致服务中断;
但通常情况下,连接池的地址变更通常只发生在部署过程前后,在环境部署完毕之后应该不会有这种场景,还是有一些实用价值的;
如果大家有更好的方案,不防评论里指点一下,谢谢大家......