上一篇文章(SpringBoot项目中的配置文件如何动态刷新请添加链接描述),介绍了如何利用spring cloud bus的通知机制,动态刷新配置到应用服务,本文将介绍如何在不重启微服务的情况下,动态的修改数据库连接参数。
以mysql的连接参数为例,如何在不重启的微服务的前提下,修改密码或者连接地址呢?因为mysql的datasource及sessionFactory通常是在启动的时候实例化为单利,直接修改参数刷新并不能起到重新构建的效果,要实现动态修改,就得动态的替换BeanFactory中的datasouce和sessionFactory实例,如下几个方面的问题需要考虑:
- 密码修改后通知应用服务
- 应用服务获取最新的连接信息并比较
- 应用服务释放当前sqlsession
- 重新构建datasource和sqlsessionFactory
- 将新的实例刷新到context注册的bean中
目前的情况是,除了第一个事项可以通过bus-refresh实现,其余几个事项都得一一实现。首先能想到的是Spring的事件机制,是否可以通过listener监听RefreshScopeRefreshedEvent事件,拿到最新的配置,再重新构建datasource和sessionFactory?沿着这个思路,我做了个尝试, 下面是完整的测试代码:
@Component
public class AppConfigChangeListener implements ApplicationListener<RefreshScopeRefreshedEvent> {
private static Log logger = LogFactory.getLog(AppConfigChangeListener.class);
@Resource
private DataSourceProperties dataSourceProperties;
@Value("classpath*:mybatis/mapper/**/*.xml")
private org.springframework.core.io.Resource[] mapperLocations;
@Override
public void onApplicationEvent(RefreshScopeRefreshedEvent applicationEvent) {
logger.info("refresh scope: " + JSON.toJSONString(dataSourceProperties));
DataSource dataSource = (DataSource) ApplicationContextUtils.getBean("dataSource");
if (dataSource != null && HikariDataSource.class.isInstance(dataSource)) {
HikariDataSource hikariDataSource = (HikariDataSource) dataSource;
SqlSessionFactory sqlSessionFactory = (SqlSessionFactory) ApplicationContextUtils.getBean("sqlSessionFactory");
if (sqlSessionFactory != null && connectParamChanged(hikariDataSource, dataSourceProperties)) {
if (SqlSessionManager.class.isInstance(sqlSessionFactory)) {
((SqlSessionManager) sqlSessionFactory).close();
}
rebuildSqlSessionFactory(dataSourceProperties);
}
}
}
private void rebuildSqlSessionFactory(DataSourceProperties dataSourceProperties) {
try {
DataSource dataSource = dataSource(dataSourceProperties);
SqlSessionFactory sqlSessionFactory = sqlSessionFactory(dataSource, mapperLocations);
ApplicationContextUtils.refreshBean("dataSource", dataSource);
ApplicationContextUtils.refreshBean("sqlSessionFactory", sqlSessionFactory);
DataSource newDS = (DataSource) ApplicationContextUtils.getBean("dataSource");
SqlSessionFactory newSSF = (SqlSessionFactory) ApplicationContextUtils.getBean("sqlSessionFactory");
System.out.println("....");
} catch (Exception e) {
logger.error(ExceptionUtils.getStackTrace(e));
}
}
private boolean connectParamChanged(HikariDataSource dataSource, DataSourceProperties dataSourceProperties) {
return !StringUtils.equals(dataSource.getUsername(), dataSourceProperties.getUsername()) ||
!StringUtils.equals(dataSource.getPassword(), dataSourceProperties.getPassword()) ||
!StringUtils.equals(dataSource.getJdbcUrl(), dataSourceProperties.getUrl());
}
public DataSource dataSource(DataSourceProperties dataSourceProperties) {
return DataSourceBuilder.create()
.type(HikariDataSource.class).driverClassName(dataSourceProperties.getDriverClassName())
.url(dataSourceProperties.getUrl()).username(dataSourceProperties.getUsername())
.password(dataSourceProperties.getPassword())
.build();
}
public SqlSessionFactory sqlSessionFactory(DataSource dataSource, org.springframework.core.io.Resource[] mapperLocations) throws Exception {
CustomSqlSessionFactoryBean sqlSessionFactoryBean = new CustomSqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
sqlSessionFactoryBean.setMapperLocations(mapperLocations);
sqlSessionFactoryBean.setTypeAliasesPackage(ApplicationConsts.BASE_PACKAGE);
sqlSessionFactoryBean.setTypeAliasesSuperType(PersistenceEntity.class);
PagerCountInterceptor pagerCountInterceptor = new PagerCountInterceptor();
pagerCountInterceptor.setProperties(new Properties());
Interceptor[] plugins = new Interceptor[]{
new PagerInterceptor(),
pagerCountInterceptor,
new EntityUpdateInterceptor()
};
sqlSessionFactoryBean.setPlugins(plugins);
return sqlSessionFactoryBean.getObject();
}
}
逐段说明下:
@Resource
private DataSourceProperties dataSourceProperties;
@Value("classpath*:mybatis/mapper/**/*.xml")
private org.springframework.core.io.Resource[] mapperLocations;
dataSourceProperties是构建datasource的必要参数,也是数据库配置属性的具体对象。
mapperLocations是构建sqlSessionFactory的必要参数,一般不大需要调整。
@Override
public void onApplicationEvent(RefreshScopeRefreshedEvent applicationEvent) {
logger.info("refresh scope: " + JSON.toJSONString(dataSourceProperties));
DataSource dataSource = (DataSource) ApplicationContextUtils.getBean("dataSource");
if (dataSource != null && HikariDataSource.class.isInstance(dataSource)) {
HikariDataSource hikariDataSource = (HikariDataSource) dataSource;
SqlSessionFactory sqlSessionFactory = (SqlSessionFactory) ApplicationContextUtils.getBean("sqlSessionFactory");
if (sqlSessionFactory != null && connectParamChanged(hikariDataSource, dataSourceProperties)) {
if (SqlSessionManager.class.isInstance(sqlSessionFactory)) {
((SqlSessionManager) sqlSessionFactory).close();
}
rebuildSqlSessionFactory(dataSourceProperties);
}
}
}
这段方式便是核心逻辑了,检测到configbus的refresh事件后,如果发现用户名或者密码或者uri有变化,则重新刷新datasource和sqlSessionFactory。
private boolean connectParamChanged(HikariDataSource dataSource, DataSourceProperties dataSourceProperties) {
return !StringUtils.equals(dataSource.getUsername(), dataSourceProperties.getUsername()) ||
!StringUtils.equals(dataSource.getPassword(), dataSourceProperties.getPassword()) ||
!StringUtils.equals(dataSource.getJdbcUrl(), dataSourceProperties.getUrl());
}
比较的办法很简单,基于最新的datasourceProperties属性和bean池中datasource的属性。DataSourceProperties是系统自动导入的属性类,在refresh的时候会自动状态,不需要添加@RefreshScope注解。
private void rebuildSqlSessionFactory(DataSourceProperties dataSourceProperties) {
try {
DataSource dataSource = dataSource(dataSourceProperties);
SqlSessionFactory sqlSessionFactory = sqlSessionFactory(dataSource, mapperLocations);
ApplicationContextUtils.refreshBean("dataSource", dataSource);
ApplicationContextUtils.refreshBean("sqlSessionFactory", sqlSessionFactory);
DataSource newDS = (DataSource) ApplicationContextUtils.getBean("dataSource");
SqlSessionFactory newSSF = (SqlSessionFactory) ApplicationContextUtils.getBean("sqlSessionFactory");
System.out.println("....");
} catch (Exception e) {
logger.error(ExceptionUtils.getStackTrace(e));
}
}
rebuild的过程也是关键过程,核心是refreshBean。
public static void refreshBean(String name, Object bean) {
if (getContext() != null && ConfigurableApplicationContext.class.isInstance(getContext())) {
ConfigurableApplicationContext configurableApplicationContext = (ConfigurableApplicationContext) getContext();
ConfigurableListableBeanFactory beanFactory = configurableApplicationContext.getBeanFactory();
if (DefaultListableBeanFactory.class.isInstance(beanFactory)) {
((DefaultListableBeanFactory) beanFactory).destroySingleton(name);
beanFactory.registerSingleton(name, bean);
} else {
beanFactory.destroyScopedBean(name);
}
}
}
refreshBean的逻辑比较简单,删除beanFactory的singleton对象,在重新注册。
context的获取办法不做介绍,只要实现ApplicationContextAware即可注入。
下一步就可以测试了,启动应用后,调用一个查询服务;修改密码,bus-refresh,再请求查询服务,即可看到效果。