news 2026/4/17 12:56:27

基于域名的动态数据源切换实现教程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于域名的动态数据源切换实现教程

概述

这是一个基于Spring Boot的多数据源动态切换方案,通过解析请求的域名自动选择对应的数据源。

核心组件实现

1. 会话上下文管理 (SessionContext)

  • 使用 TransmittableThreadLocal 实现线程间数据传递
  • 提供统一的键值对存储接口
  • 在请求开始时清理旧数据,在结束时移除数据避免内存泄漏

2. 请求拦截器 (HeaderInterceptor)

  • 在 preHandle 阶段解析请求头中的 Origin
  • 提取域名并存储到 SessionContext 中
  • 调用 SessionContext.remove() 清理线程数据

3.动态数据源实现 (DynamicDataSource)

  • 继承 AbstractRoutingDataSource
  • 重写 determineCurrentLookupKey() 方法
  • 从 SessionContext 获取域名作为数据源标识

4.数据源配置 (DataSourceConfig)

  • 配置多个实际数据源(如 bjDataSource 和 cdDataSource)
  • 构建 targetDataSources 映射表,以域名为键关联具体数据源
  • 设置默认数据源

工作流程

  1. 请求进入 → HeaderInterceptor 解析 Origin 头部
  2. 域名提取 → 将域名存入 SessionContext
  3. 数据源路由 → DynamicDataSource 根据域名选择对应数据源
  4. 执行操作 → 使用选定的数据源执行数据库操作

会话上下文管理

package com.park.context; import cn.hutool.core.convert.Convert; import com.alibaba.ttl.TransmittableThreadLocal; import com.park.constants.SecurityConstants; import org.apache.commons.lang3.StringUtils; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; public class SessionContext { private static final TransmittableThreadLocal<Map<String, Object>> THREAD_LOCAL = new TransmittableThreadLocal<>(); public static void set(String key, Object value) { Map<String, Object> map = getLocalMap(); map.put(key, value == null ? StringUtils.EMPTY : value); } public static String get(String key) { Map<String, Object> map = getLocalMap(); return Convert.toStr(map.getOrDefault(key, StringUtils.EMPTY)); } public static Map<String, Object> getLocalMap() { Map<String, Object> map = THREAD_LOCAL.get(); if (map == null) { map = new ConcurrentHashMap<>(); THREAD_LOCAL.set(map); } return map; } public static void setCompanyId(String companyId) { set(SecurityConstants.COMPANY_ID, companyId); } public static String getCompanyId() { return get(SecurityConstants.COMPANY_ID); } public static void setParkingLotId(Set<String> parkingLotIds) { set(SecurityConstants.PARKINGLOT_ID, parkingLotIds); } public static Set<String> getParkingLotId() { return get(SecurityConstants.PARKINGLOT_ID, Set.class); } public static <T> T get(String key, Class<T> clazz) { Map<String, Object> map = getLocalMap(); return cast(map.getOrDefault(key, null)); } public static <T> T cast(Object obj) { if (obj == null) { return null; } return (T) obj; } public static void remove() { THREAD_LOCAL.remove(); } }

核心动态数据源类

package com.park.source; import com.park.constants.SecurityConstants; import com.park.context.SessionContext; import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; public class DynamicDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { System.out.println("当前数据源:" + SessionContext.get(SecurityConstants.DOMAIN)); return SessionContext.get(SecurityConstants.DOMAIN); } }

请求拦截器

package com.park.constants; public class SecurityConstants { public static final String AUTHORIZATION = "Authorization"; public static final String COMPANY_ID = "companyId"; public static final String PARKINGLOT_ID = "parkingLotIds"; public static final String DOMAIN = "domain"; } package com.park.interceptor; import com.park.constants.SecurityConstants; import com.park.context.SessionContext; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.stereotype.Component; import org.springframework.web.servlet.AsyncHandlerInterceptor; import java.net.URI; import java.net.URISyntaxException; @Component public class DomainInterceptor implements AsyncHandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 清除之前的会话数据 SessionContext.remove(); String origin = request.getHeader("Origin"); if (origin != null) { try { URI uri = new URI(origin); String domain = uri.getHost(); SessionContext.setDomain(domain); } catch (URISyntaxException e) { // 记录错误日志 } } return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { // 请求结束后清除线程本地变量 SessionContext.remove(); } } package com.park.config; import com.park.interceptor.DomainInterceptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class WebConfig implements WebMvcConfigurer { @Autowired private DomainInterceptor domainInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(domainInterceptor) .addPathPatterns("/**") .excludePathPatterns("/login", "/public/**"); } }

数据源配置

spring: datasource: bj: jdbc-url: jdbc:postgresql://localhost:5432/bj_db username: your_username password: your_password driver-class-name: org.postgresql.Driver cd: jdbc-url: jdbc:postgresql://localhost:5432/cd_db username: your_username password: your_password driver-class-name: org.postgresql.Driver package com.park.config; import com.park.source.DynamicDataSource; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import javax.sql.DataSource; import java.util.HashMap; import java.util.Map; @Configuration public class DataSourceConfig { @Bean @ConfigurationProperties(prefix = "spring.datasource.bj") public DataSource bjDataSource() { return DataSourceBuilder.create().build(); } @Bean @ConfigurationProperties(prefix = "spring.datasource.cd") public DataSource cdDataSource() { return DataSourceBuilder.create().build(); } @Bean @Primary public DataSource dynamicDataSource() { Map<Object, Object> targetDataSources = new HashMap<>(); targetDataSources.put("bj.park.com", bjDataSource()); targetDataSources.put("cd.park.com", cdDataSource()); DynamicDataSource dynamicDataSource = new DynamicDataSource(); dynamicDataSource.setTargetDataSources(targetDataSources); dynamicDataSource.setDefaultTargetDataSource(primaryDataSource()); return dynamicDataSource; } }
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/6 19:39:20

SPI控制器功能验证实践:基于iverilog的端到端流程

SPI控制器功能验证实践&#xff1a;从零构建基于Icarus Verilog的开源仿真流程 你有没有遇到过这样的场景&#xff1f;手头有个SPI控制器的RTL代码&#xff0c;想快速跑个仿真看看时序对不对&#xff0c;结果发现公司没有VCS许可证&#xff0c;ModelSim又太重启动慢&#xff0c…

作者头像 李华
网站建设 2026/4/18 5:43:24

AUTOSAR经典平台入门:ECU抽象层全面讲解

AUTOSAR经典平台入门&#xff1a;深入理解ECU抽象层的“软硬桥梁”作用你有没有遇到过这样的场景&#xff1f;一个原本在英飞凌TC3xx平台上运行良好的刹车踏板检测模块&#xff0c;因为项目换用了NXP S32K芯片&#xff0c;结果整个ADC采集代码几乎要重写一遍——引脚变了、寄存…

作者头像 李华
网站建设 2026/4/16 19:04:55

别再把树莓派当玩具了,它已经能胜任工业级 AI 控制器

在工业物联网、智能制造、储能系统和自主移动机器人等场景中&#xff0c;设备数量激增、协议复杂、业务实时性要求高。企业希望快速部署智能化控制和边缘 AI 推理&#xff0c;却常被“算力不足、开发周期长、硬件兼容差”所困扰。钡铼技术带来的基于树莓派 CM5 的工业 AI 控制器…

作者头像 李华
网站建设 2026/4/12 8:12:57

Proteus汉化与原版切换技巧:项目应用实例分享

Proteus汉化实战&#xff1a;如何优雅地在中英文界面间自由切换&#xff1f; 你有没有过这样的经历&#xff1f;—— 站在讲台上给学生演示Proteus仿真&#xff0c;刚打开软件&#xff0c;一个学生举手&#xff1a;“老师&#xff0c;‘Pick Device’是啥意思&#xff1f;” …

作者头像 李华
网站建设 2026/4/16 9:07:31

乌班图mysql如何小版本升级

Ubuntu 20.04 下 MySQL 8.0.42 (系统源) 升级至 8.0.43 (官方源) 的完整操作手册。第一阶段&#xff1a;备份 (生命线) 在执行任何操作前&#xff0c;必须完成。 备份所有数据库数据&#xff1a; mysqldump -u root -p --all-databases --master-data2 --single-transaction &g…

作者头像 李华
网站建设 2026/4/9 0:10:30

ARM开发深度剖析:STM32中断系统NVIC全面讲解

ARM开发深度剖析&#xff1a;STM32中断系统NVIC全面讲解在嵌入式系统的战场上&#xff0c;时间就是生命。一次按键按下、一个串口数据到达、一场电机过流故障——这些事件能否被及时响应&#xff0c;往往决定了整个系统是稳定运行还是突然宕机。尤其是在工业控制、智能仪表和实…

作者头像 李华