亚洲香蕉成人av网站在线观看_欧美精品成人91久久久久久久_久久久久久久久久久亚洲_热久久视久久精品18亚洲精品_国产精自产拍久久久久久_亚洲色图国产精品_91精品国产网站_中文字幕欧美日韩精品_国产精品久久久久久亚洲调教_国产精品久久一区_性夜试看影院91社区_97在线观看视频国产_68精品久久久久久欧美_欧美精品在线观看_国产精品一区二区久久精品_欧美老女人bb

首頁 > 開發 > Java > 正文

Spring動態注冊多數據源的實現方法

2024-07-13 10:16:52
字體:
來源:轉載
供稿:網友

最近在做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@Componentpublic 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;  }  try {   String decryptPassword = ConfigTools.decrypt(entity.getDbPublicKey(), entity.getDbPassword());   return Objects.equals(existsDataSource.getPassword(), decryptPassword);  } catch (Exception e) {   log.error("數據源密碼校驗失敗,Exception:{}", e);   return false;  } }}

spring-data-source.xml

<!-- 引入jdbc配置文件 --> <context:property-placeholder location="classpath:data.properties" ignore-unresolvable="true"/> <!-- 公共(默認)數據源 --> <bean id="defaultDataSource" class="com.alibaba.druid.pool.DruidDataSource"   init-method="init" destroy-method="close">  <!-- 基本屬性 url、user、password -->  <property name="url" value="${ds.jdbcUrl}" />  <property name="username" value="${ds.user}" />  <property name="password" value="${ds.password}" />  <!-- 配置初始化大小、最小、最大 -->  <property name="initialSize" value="5" />  <property name="minIdle" value="2" />  <property name="maxActive" value="10" />  <!-- 配置獲取連接等待超時的時間,單位是毫秒 -->  <property name="maxWait" value="1000" />  <!-- 配置間隔多久才進行一次檢測,檢測需要關閉的空閑連接,單位是毫秒 -->  <property name="timeBetweenEvictionRunsMillis" value="5000" />  <!-- 配置一個連接在池中最小生存的時間,單位是毫秒 -->  <property name="minEvictableIdleTimeMillis" value="240000" />  <property name="validationQuery" value="SELECT 1" />  <!--單位:秒,檢測連接是否有效的超時時間-->  <property name="validationQueryTimeout" value="60" />  <!--建議配置為true,不影響性能,并且保證安全性。申請連接的時候檢測,如果空閑時間大于timeBetweenEvictionRunsMillis,執行validationQuery檢測連接是否有效-->  <property name="testWhileIdle" value="true" />  <!--申請連接時執行validationQuery檢測連接是否有效,做了這個配置會降低性能。-->  <property name="testOnBorrow" value="true" />  <!--歸還連接時執行validationQuery檢測連接是否有效,做了這個配置會降低性能。-->  <property name="testOnReturn" value="false" />  <!--Config Filter-->  <property name="filters" value="config" />  <property name="connectionProperties" value="config.decrypt=true;config.decrypt.key=${ds.publickey}" /> </bean> <!-- 事務管理器 --> <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">  <property name="dataSource" ref="multipleDataSource"/> </bean> <!--多數據源--> <bean id="multipleDataSource" class="a.b.c.DynamicDataSource">  <property name="defaultTargetDataSource" ref="defaultDataSource"/>  <property name="targetDataSources">   <map>    <entry key="defaultDataSource" value-ref="defaultDataSource"/>   </map>  </property> </bean> <!-- 注解事務管理器 --> <!--這里的order值必須大于DynamicDataSourceAspectAdvice的order值--> <tx:annotation-driven transaction-manager="txManager" order="2"/> <!-- 創建SqlSessionFactory,同時指定數據源 --> <bean id="mainSqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">  <property name="dataSource" ref="multipleDataSource"/> </bean> <!-- DAO接口所在包名,Spring會自動查找其下的DAO --> <bean id="mainSqlMapper" class="org.mybatis.spring.mapper.MapperScannerConfigurer">  <property name="sqlSessionFactoryBeanName" value="mainSqlSessionFactory"/>  <property name="basePackage" value="a.b.c.*.dao"/> </bean> <bean id="defaultSqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">  <property name="dataSource" ref="defaultDataSource"/> </bean> <bean id="defaultSqlMapper" class="org.mybatis.spring.mapper.MapperScannerConfigurer">  <property name="sqlSessionFactoryBeanName" value="defaultSqlSessionFactory"/>  <property name="basePackage" value="a.b.c.base.dal.dao"/> </bean> <!-- 其他配置省略 -->

DynamicDataSourceAspectAdvice

利用AOP自動切換數據源,僅供參考;

@Slf4j@Aspect@Component@Order(1) // 請注意:這里order一定要小于tx:annotation-driven的order,即先執行DynamicDataSourceAspectAdvice切面,再執行事務切面,才能獲取到最終的數據源@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("當前租戶key:{}", tenantKey);  DataSourceContextHolder.setDataSourceKey(tenantKey);  Object result = jp.proceed();  DataSourceContextHolder.clearDataSourceKey();  return result; }}

總結

以上所述是小編給大家介紹的Spring動態注冊多數據源的實現方法,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回復大家的。在此也非常感謝大家對VeVb武林網網站的支持!


注:相關教程知識閱讀請移步到JAVA教程頻道。
發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
亚洲香蕉成人av网站在线观看_欧美精品成人91久久久久久久_久久久久久久久久久亚洲_热久久视久久精品18亚洲精品_国产精自产拍久久久久久_亚洲色图国产精品_91精品国产网站_中文字幕欧美日韩精品_国产精品久久久久久亚洲调教_国产精品久久一区_性夜试看影院91社区_97在线观看视频国产_68精品久久久久久欧美_欧美精品在线观看_国产精品一区二区久久精品_欧美老女人bb
欧美一区二区.| 97超级碰在线看视频免费在线看| 国产精品高潮呻吟视频| 中文在线资源观看视频网站免费不卡| 久久久精品电影| 国产精品久久久久7777婷婷| 色悠悠国产精品| 国模视频一区二区| 国产精品 欧美在线| 国产精品美女久久| 性欧美激情精品| 亚洲色无码播放| 欧美大胆a视频| 欧美人与性动交a欧美精品| 欧美日韩激情视频8区| 亚洲a一级视频| 国内精品视频在线| 亚洲精品福利在线观看| 日韩中文字幕在线视频播放| 精品女同一区二区三区在线播放| 黄色成人av在线| 中文字幕一区二区精品| 国产欧美一区二区三区在线| 91在线视频免费| 精品视频www| 精品国产精品三级精品av网址| 亚洲欧美在线免费观看| 久久色免费在线视频| 黄色精品一区二区| 日韩电影免费观看在线| 国产精品偷伦一区二区| 国产精品91久久久久久| 久久99热这里只有精品国产| 久久久久久欧美| 九九精品视频在线观看| 国产成人精彩在线视频九色| 日本韩国在线不卡| 亚洲自拍偷拍在线| 欧美国产高跟鞋裸体秀xxxhd| 亚洲一区二区免费| 日韩中文在线中文网三级| 91精品综合久久久久久五月天| 亚洲最大av在线| 精品动漫一区二区三区| 91视频国产高清| 国内精品久久久久久久| 一区二区亚洲精品国产| 18一19gay欧美视频网站| 国内精品免费午夜毛片| www.久久草.com| 欧美激情久久久久| 欧美日韩另类视频| 在线电影av不卡网址| 成人激情视频在线| 久久成人综合视频| 亚洲精品在线不卡| 日韩高清免费观看| 91在线观看免费高清完整版在线观看| 伊人久久免费视频| 欧美黄色成人网| 中文字幕在线看视频国产欧美在线看完整| 国产精品成人免费电影| 欧美成人精品在线观看| 精品国产乱码久久久久久天美| 国产精品日韩欧美大师| 成人免费xxxxx在线观看| 国产精品视频内| 91亚洲精品久久久久久久久久久久| 欧美xxxx18性欧美| 欧美激情精品久久久久久变态| 粉嫩老牛aⅴ一区二区三区| 国产精品狠色婷| 国产亚洲精品日韩| 亚洲国产成人精品久久久国产成人一区| 亚洲自拍高清视频网站| 亚洲精品视频在线播放| 91精品国产乱码久久久久久蜜臀| 成人久久精品视频| 91国内精品久久| 成人免费观看49www在线观看| 精品免费在线视频| 久久久999精品免费| 2019日本中文字幕| 久久亚洲精品一区| 国产亚洲视频在线观看| 中文字幕亚洲图片| 人九九综合九九宗合| 精品国产一区二区三区久久| 国产一区二区三区视频| 欧美激情国产精品| 午夜精品久久久久久久99热浪潮| 亚洲欧美日韩天堂一区二区| 国产精品成人国产乱一区| 国产精品久久久久久久久久ktv| 7777免费精品视频| 国产精品va在线播放我和闺蜜| 亚洲三级黄色在线观看| 91免费国产网站| 国内免费精品永久在线视频| 亚洲成人黄色网| 亚洲成人久久电影| 日韩视频精品在线| 日韩亚洲综合在线| 538国产精品一区二区免费视频| 亚洲欧美在线第一页| 91探花福利精品国产自产在线| 久久久久久久91| 亚洲在线第一页| 亚洲无限乱码一二三四麻| 国产98色在线| 亚洲国产中文字幕在线观看| 欧美成人精品h版在线观看| 亚洲日本成人网| 欧美性开放视频| 国产一区二区视频在线观看| www.99久久热国产日韩欧美.com| 欧美性生交xxxxxdddd| 精品中文字幕在线观看| 国产原创欧美精品| 欧美黄色三级网站| 欧美成人国产va精品日本一级| 隔壁老王国产在线精品| 92看片淫黄大片欧美看国产片| 国产精品永久免费在线| 亚洲欧美国产一本综合首页| 中文字幕亚洲欧美日韩高清| 九九久久精品一区| 日韩亚洲精品视频| 国内自拍欧美激情| 亚洲一区二区免费在线| 欧美专区中文字幕| 久久久精品网站| 69久久夜色精品国产69| 91av在线看| 亚洲老板91色精品久久| 26uuu另类亚洲欧美日本一| 亚洲第一网站免费视频| 亚洲色图国产精品| 午夜免费久久久久| 欧美亚洲另类制服自拍| 国产日韩欧美91| 国内精品久久久久久中文字幕| 欧美专区中文字幕| 国产一区二区三区欧美| 久久全球大尺度高清视频| 成人xvideos免费视频| 国产在线观看精品| 久久国产精品影视| 国产成人精品国内自产拍免费看| 美女久久久久久久久久久| 日韩欧美在线国产| 国产精品视频精品视频| 亚州欧美日韩中文视频| 国产精品一区二区久久精品| 日韩成人av在线播放| 久热精品视频在线免费观看| 亚洲国产成人av在线| 最近中文字幕mv在线一区二区三区四区| 欧美国产日韩xxxxx| 色播久久人人爽人人爽人人片视av| 国产日产亚洲精品| 国产精品免费看久久久香蕉| 亚洲aⅴ男人的天堂在线观看| 国产精品扒开腿做爽爽爽男男|