真实的国产乱ⅩXXX66竹夫人,五月香六月婷婷激情综合,亚洲日本VA一区二区三区,亚洲精品一区二区三区麻豆

成都創(chuàng)新互聯(lián)網(wǎng)站制作重慶分公司

Spring動態(tài)注冊多數(shù)據(jù)源的實(shí)現(xiàn)方法

最近在做SaaS應(yīng)用,數(shù)據(jù)庫采用了單實(shí)例多schema的架構(gòu)(詳見參考資料1),每個租戶有一個獨(dú)立的schema,同時整個數(shù)據(jù)源有一個共享的schema,因此需要解決動態(tài)增刪、切換數(shù)據(jù)源的問題。

目前創(chuàng)新互聯(lián)已為上千余家的企業(yè)提供了網(wǎng)站建設(shè)、域名、虛擬空間、綿陽服務(wù)器托管、企業(yè)網(wǎng)站設(shè)計(jì)、孫吳網(wǎng)站維護(hù)等服務(wù),公司將堅(jiān)持客戶導(dǎo)向、應(yīng)用為本的策略,正道將秉承"和諧、參與、激情"的文化,與客戶和合作伙伴齊心協(xié)力一起成長,共同發(fā)展。

在網(wǎng)上搜了很多文章后,很多都是講主從數(shù)據(jù)源配置,或都是在應(yīng)用啟動前已經(jīng)確定好數(shù)據(jù)源配置的,甚少講在不停機(jī)的情況如何動態(tài)加載數(shù)據(jù)源,所以寫下這篇文章,以供參考。

使用到的技術(shù)

  • Java8
  • Spring + SpringMVC + MyBatis
  • Druid連接池
  • Lombok
  • (以上技術(shù)并不影響思路實(shí)現(xiàn),只是為了方便瀏覽以下代碼片段)

思路

當(dāng)一個請求進(jìn)來的時候,判斷當(dāng)前用戶所屬租戶,并根據(jù)租戶信息切換至相應(yīng)數(shù)據(jù)源,然后進(jìn)行后續(xù)的業(yè)務(wù)操作。

代碼實(shí)現(xiàn)

TenantConfigEntity(租戶信息)
@EqualsAndHashCode(callSuper = false)
@Data
@FieldDefaults(level = AccessLevel.PRIVATE)
public class TenantConfigEntity {
 /**
  * 租戶id
  **/
 Integer tenantId;
 /**
  * 租戶名稱
  **/
 String tenantName;
 /**
  * 租戶名稱key
  **/
 String tenantKey;
 /**
  * 數(shù)據(jù)庫url
  **/
 String dbUrl;
 /**
  * 數(shù)據(jù)庫用戶名
  **/
 String dbUser;
 /**
  * 數(shù)據(jù)庫密碼
  **/
 String dbPassword;
 /**
  * 數(shù)據(jù)庫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=";
 /**
  * 拼接數(shù)據(jù)源的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 保存當(dāng)前線程的數(shù)據(jù)源key name,并實(shí)現(xiàn)set、get、clear方法;

public class DataSourceContextHolder {
 private static final ThreadLocal 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(重點(diǎn))

繼承 AbstractRoutingDataSource (建議閱讀其源碼,了解動態(tài)切換數(shù)據(jù)源的過程),實(shí)現(xiàn)動態(tài)選擇數(shù)據(jù)源;

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(重點(diǎn)中的重點(diǎn))

從數(shù)據(jù)庫加載數(shù)據(jù)源信息,并動態(tài)組裝和注冊spring bean,

@Slf4j
@Component
public class DynamicDataSourceSummoner implements ApplicationListener {
 // 跟spring-data-source.xml的默認(rèn)數(shù)據(jù)源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加載完成后執(zhí)行
  */
 @Override
 public void onApplicationEvent(ContextRefreshedEvent event) {
  // 防止重復(fù)執(zhí)行
  if (!loaded) {
   loaded = true;
   try {
    registerDynamicDataSources();
   } catch (Exception e) {
    log.error("數(shù)據(jù)源初始化失敗, Exception:", e);
   }
  }
 }
 /**
  * 從數(shù)據(jù)庫讀取租戶的DB配置,并動態(tài)注入Spring容器
  */
 public void registerDynamicDataSources() {
  // 獲取所有租戶的DB配置
  List tenantConfigEntities = tenantConfigDAO.listAll();
  if (CollectionUtils.isEmpty(tenantConfigEntities)) {
   throw new IllegalStateException("應(yīng)用程序初始化失敗,請先配置數(shù)據(jù)源");
  }
  // 把數(shù)據(jù)源bean注冊到容器中
  addDataSourceBeans(tenantConfigEntities);
 }
 /**
  * 根據(jù)DataSource創(chuàng)建bean并注冊到容器中
  */
 private void addDataSourceBeans(List tenantConfigEntities) {
  Map targetDataSources = Maps.newLinkedHashMap();
  DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) applicationContext.getAutowireCapableBeanFactory();
  for (TenantConfigEntity entity : tenantConfigEntities) {
   String beanKey = DataSourceUtil.getDataSourceBeanKey(entity.getTenantKey());
   // 如果該數(shù)據(jù)源已經(jīng)在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中,注意一定是剛才創(chuàng)建bean對象
   targetDataSources.put(beanKey, applicationContext.getBean(beanKey));
  }
  // 將創(chuàng)建的map對象set到 targetDataSources;
  dynamicDataSource.setTargetDataSources(targetDataSources);
  // 必須執(zhí)行此操作,才會重新初始化AbstractRoutingDataSource 中的 resolvedDataSources,也只有這樣,動態(tài)切換才會起效
  dynamicDataSource.afterPropertiesSet();
 }
 /**
  * 組裝數(shù)據(jù)源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與數(shù)據(jù)庫的DataSource信息是否一致
  * 備注:這里沒有判斷public_key,因?yàn)榱硗馊齻€信息基本可以確定唯一了
  */
 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;
  }
  try {
   String decryptPassword = ConfigTools.decrypt(entity.getDbPublicKey(), entity.getDbPassword());
   return Objects.equals(existsDataSource.getPassword(), decryptPassword);
  } catch (Exception e) {
   log.error("數(shù)據(jù)源密碼校驗(yàn)失敗,Exception:{}", e);
   return false;
  }
 }
}

spring-data-source.xml


 
 
 
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
 
 
 
  
 
 
 
  
  
   
    
   
  
 
 
 
 
 
 
  
 
 
 
  
  
 
 
  
 
 
  
  
 
 

DynamicDataSourceAspectAdvice

利用AOP自動切換數(shù)據(jù)源,僅供參考;

@Slf4j
@Aspect
@Component
@Order(1) // 請注意:這里order一定要小于tx:annotation-driven的order,即先執(zhí)行DynamicDataSourceAspectAdvice切面,再執(zhí)行事務(wù)切面,才能獲取到最終的數(shù)據(jù)源
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class DynamicDataSourceAspectAdvice {
 @Around("execution(* a.b.c.*.controller.*.*(..))")
 public Object doAround(ProceedingJoinPoint jp) throws Throwable {
  ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
  HttpServletRequest request = sra.getRequest();
  HttpServletResponse response = sra.getResponse();
  String tenantKey = request.getHeader("tenant");
  // 前端必須傳入tenant header, 否則返回400
  if (!StringUtils.hasText(tenantKey)) {
   WebUtils.toHttp(response).sendError(HttpServletResponse.SC_BAD_REQUEST);
   return null;
  }
  log.info("當(dāng)前租戶key:{}", tenantKey);
  DataSourceContextHolder.setDataSourceKey(tenantKey);
  Object result = jp.proceed();
  DataSourceContextHolder.clearDataSourceKey();
  return result;
 }
}

總結(jié)

以上所述是小編給大家介紹的Spring動態(tài)注冊多數(shù)據(jù)源的實(shí)現(xiàn)方法,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回復(fù)大家的。在此也非常感謝大家對創(chuàng)新互聯(lián)網(wǎng)站的支持!


文章名稱:Spring動態(tài)注冊多數(shù)據(jù)源的實(shí)現(xiàn)方法
分享網(wǎng)址:http://weahome.cn/article/pdhcse.html

其他資訊

在線咨詢

微信咨詢

電話咨詢

028-86922220(工作日)

18980820575(7×24)

提交需求

返回頂部