Spring微服务如何在不重启的前提下修改数据库密码

上一篇文章(SpringBoot项目中的配置文件如何动态刷新请添加链接描述),介绍了如何利用spring cloud bus的通知机制,动态刷新配置到应用服务,本文将介绍如何在不重启微服务的情况下,动态的修改数据库连接参数。

以mysql的连接参数为例,如何在不重启的微服务的前提下,修改密码或者连接地址呢?因为mysql的datasource及sessionFactory通常是在启动的时候实例化为单利,直接修改参数刷新并不能起到重新构建的效果,要实现动态修改,就得动态的替换BeanFactory中的datasouce和sessionFactory实例,如下几个方面的问题需要考虑:

  1. 密码修改后通知应用服务
  2. 应用服务获取最新的连接信息并比较
  3. 应用服务释放当前sqlsession
  4. 重新构建datasource和sqlsessionFactory
  5. 将新的实例刷新到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即可注入。

Spring微服务如何在不重启的前提下修改数据库密码

下一步就可以测试了,启动应用后,调用一个查询服务;修改密码,bus-refresh,再请求查询服务,即可看到效果。

猜你喜欢

转载自blog.51cto.com/10705830/2450772
今日推荐