Spring动态注册多数据源的实现方法
上一篇 / 下一篇 2018-03-14 14:01:18 / 个人分类:软件开发
最近在做SaaS应用,数据库采用了单实例多schema的架构(详见参考资料1),每个租户有一个独立的schema,同时整个数据源有一个共享的schema,因此需要解决动态增删、切换数据源的问题。
在网上搜了很多文章后,很多都是讲主从数据源配置,或都是在应用启动前已经确定好数据源配置的,甚少讲在不停机的情况如何动态加载数据源,所以写下这篇文章,以供参考。
使用到的技术
- Java8
- Spring + SpringMVC + MyBatis
- Druid连接池
- Lombok
- (以上技术并不影响思路实现,只是为了方便浏览以下代码片段)
思路
当一个请求进来的时候,判断当前用户所属租户,并根据租户信息切换至相应数据源,然后进行后续的业务操作。
代码实现
- TenantConfigEntity(租户信息)
- @EqualsAndHashCode(callSuper = false)
- @Data
- @FieldDefaults(level = AccessLevel.PRIVATE)
- public class TenantConfigEntity {
- /**
- * 租户id
- **/
- Integer tenantId;
- /**
- * 租户名称
- **/
- String tenantName;
- /**
- * 租户名称key
- **/
- String tenantKey;
- /**
- * 数据库url
- **/
- String dbUrl;
- /**
- * 数据库用户名
- **/
- String dbUser;
- /**
- * 数据库密码
- **/
- String dbPassword;
- /**
- * 数据库public_key
- **/
- String dbPublicKey;
- }
- DataSourceUtil(辅助工具类,非必要)
- public class DataSourceUtil {
- private static final String DATA_SOURCE_BEAN_KEY_SUFFIX = "_data_source";
- private static final String JDBC_URL_ARGS = "?useUnicode=true&characterEncoding=UTF-8&useOldAliasMetadataBehavior=true&zeroDateTimeBehavior=convertToNull";
- private static final String CONNECTION_PROPERTIES = "config.decrypt=true;config.decrypt.key=";
- /**
- * 拼接数据源的spring bean key
- */
- public static String getDataSourceBeanKey(String tenantKey) {
- if (!StringUtils.hasText(tenantKey)) {
- return null;
- }
- return tenantKey + DATA_SOURCE_BEAN_KEY_SUFFIX;
- }
- /**
- * 拼接完整的JDBC URL
- */
- public static String getJDBCUrl(String baseUrl) {
- if (!StringUtils.hasText(baseUrl)) {
- return null;
- }
- return baseUrl + JDBC_URL_ARGS;
- }
- /**
- * 拼接完整的Druid连接属性
- */
- public static String getConnectionProperties(String publicKey) {
- if (!StringUtils.hasText(publicKey)) {
- return null;
- }
- return CONNECTION_PROPERTIES + publicKey;
- }
- }
DataSourceContextHolder
使用 ThreadLocal 保存当前线程的数据源key name,并实现set、get、clear方法;
- public class DataSourceContextHolder {
- private static final ThreadLocal<String> dataSourceKey = new InheritableThreadLocal<>();
- public static void setDataSourceKey(String tenantKey) {
- dataSourceKey.set(tenantKey);
- }
- public static String getDataSourceKey() {
- return dataSourceKey.get();
- }
- public static void clearDataSourceKey() {
- dataSourceKey.remove();
- }
- }
DynamicDataSource(重点)
继承 AbstractRoutingDataSource (建议阅读其源码,了解动态切换数据源的过程),实现动态选择数据源;
- public class DynamicDataSource extends AbstractRoutingDataSource {
- @Autowired
- private ApplicationContext applicationContext;
- @Lazy
- @Autowired
- private DynamicDataSourceSummoner summoner;
- @Lazy
- @Autowired
- private TenantConfigDAO tenantConfigDAO;
- @Override
- protected String determineCurrentLookupKey() {
- String tenantKey = DataSourceContextHolder.getDataSourceKey();
- return DataSourceUtil.getDataSourceBeanKey(tenantKey);
- }
- @Override
- protected DataSource determineTargetDataSource() {
- String tenantKey = DataSourceContextHolder.getDataSourceKey();
- String beanKey = DataSourceUtil.getDataSourceBeanKey(tenantKey);
- if (!StringUtils.hasText(tenantKey) || applicationContext.containsBean(beanKey)) {
- return super.determineTargetDataSource();
- }
- if (tenantConfigDAO.exist(tenantKey)) {
- summoner.registerDynamicDataSources();
- }
- return super.determineTargetDataSource();
- }
- }
DynamicDataSourceSummoner(重点中的重点)
从数据库加载数据源信息,并动态组装和注册spring bean,
- @Slf4j
- @Component
- public class DynamicDataSourceSummoner implements ApplicationListener<ContextRefreshedEvent> {
- // 跟spring-data-source.xml的默认数据源id保持一致
- private static final String DEFAULT_DATA_SOURCE_BEAN_KEY = "defaultDataSource";
- @Autowired
- private ConfigurableApplicationContext applicationContext;
- @Autowired
- private DynamicDataSource dynamicDataSource;
- @Autowired
- private TenantConfigDAO tenantConfigDAO;
- private static boolean loaded = false;
- /**
- * Spring加载完成后执行
- */
- @Override
- public void onApplicationEvent(ContextRefreshedEvent event) {
- // 防止重复执行
- if (!loaded) {
- loaded = true;
- try {
- registerDynamicDataSources();
- } catch (Exception e) {
- log.error("数据源初始化失败, Exception:", e);
- }
- }
- }
- /**
- * 从数据库读取租户的DB配置,并动态注入Spring容器
- */
- public void registerDynamicDataSources() {
- // 获取所有租户的DB配置
- List<TenantConfigEntity> tenantConfigEntities = tenantConfigDAO.listAll();
- if (CollectionUtils.isEmpty(tenantConfigEntities)) {
- throw new IllegalStateException("应用程序初始化失败,请先配置数据源");
- }
- // 把数据源bean注册到容器中
- addDataSourceBeans(tenantConfigEntities);
- }
- /**
- * 根据DataSource创建bean并注册到容器中
- */
- private void addDataSourceBeans(List<TenantConfigEntity> tenantConfigEntities) {
- Map<Object, Object> targetDataSources = Maps.newLinkedHashMap();
- DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) applicationContext.getAutowireCapableBeanFactory();
- for (TenantConfigEntity entity : tenantConfigEntities) {
- String beanKey = DataSourceUtil.getDataSourceBeanKey(entity.getTenantKey());
- // 如果该数据源已经在spring里面注册过,则不重新注册
- if (applicationContext.containsBean(beanKey)) {
- DruidDataSource existsDataSource = applicationContext.getBean(beanKey, DruidDataSource.class);
- if (isSameDataSource(existsDataSource, entity)) {
- continue;
- }
- }
- // 组装bean
- AbstractBeanDefinition beanDefinition = getBeanDefinition(entity, beanKey);
- // 注册bean
- beanFactory.registerBeanDefinition(beanKey, beanDefinition);
- // 放入map中,注意一定是刚才创建bean对象
- targetDataSources.put(beanKey, applicationContext.getBean(beanKey));
- }
- // 将创建的map对象set到 targetDataSources;
- dynamicDataSource.setTargetDataSources(targetDataSources);
- // 必须执行此操作,才会重新初始化AbstractRoutingDataSource 中的 resolvedDataSources,也只有这样,动态切换才会起效
- dynamicDataSource.afterPropertiesSet();
- }
- /**
- * 组装数据源spring bean
- */
- private AbstractBeanDefinition getBeanDefinition(TenantConfigEntity entity, String beanKey) {
- BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(DruidDataSource.class);
- builder.getBeanDefinition().setAttribute("id", beanKey);
- // 其他配置继承defaultDataSource
- builder.setParentName(DEFAULT_DATA_SOURCE_BEAN_KEY);
- builder.setInitMethodName("init");
- builder.setDestroyMethodName("close");
- builder.addPropertyValue("name", beanKey);
- builder.addPropertyValue("url", DataSourceUtil.getJDBCUrl(entity.getDbUrl()));
- builder.addPropertyValue("username", entity.getDbUser());
- builder.addPropertyValue("password", entity.getDbPassword());
- builder.addPropertyValue("connectionProperties", DataSourceUtil.getConnectionProperties(entity.getDbPublicKey()));
- return builder.getBeanDefinition();
- }
- /**
- * 判断Spring容器里面的DataSource与数据库的DataSource信息是否一致
- * 备注:这里没有判断public_key,因为另外三个信息基本可以确定唯一了
- */
- private boolean isSameDataSource(DruidDataSource existsDataSource, TenantConfigEntity entity) {
- boolean sameUrl = Objects.equals(existsDataSource.getUrl(), DataSourceUtil.getJDBCUrl(entity.getDbUrl()));
- if (!sameUrl) {
- return false;
- }
- boolean sameUser = Objects.equals(existsDataSource.getUsername(), entity.getDbUser());
- if (!sameUser) {
- return false;
TAG:
清空Cookie - 联系我们 - 51Testing软件测试网 - 交流论坛 - 空间列表 - 站点存档 - 升级自己的空间
Powered by 51Testing
© 2003-2021
沪ICP备05003035号