news 2026/5/8 3:59:48

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

作者头像

张小明

前端开发工程师

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/5/2 12:55:23

光照强度传感器采集优化:CubeMX配置ADC操作指南

用CubeMX玩转光照采集&#xff1a;从配置到优化的实战笔记最近在做一个农业物联网项目&#xff0c;需要对大棚内的光照强度进行长期监测。最开始我直接用轮询方式读ADC&#xff0c;结果发现数据跳得厉害&#xff0c;CPU还一直满载——这显然没法用于电池供电的终端节点。后来彻…

作者头像 李华
网站建设 2026/5/1 19:07:06

什么是营销管理系统,一文说清:定义、功能、选型、产品推荐

在数字化营销成为企业标配的今天&#xff0c;“营销管理系统”已成为高频词汇&#xff0c;但很多企业对其认知仍停留在“简单的客户管理工具”层面。实际上&#xff0c;一套成熟的营销管理系统能打通“获客-培育-转化-复盘”全链路&#xff0c;让营销从“零散操作”升级为“系统…

作者头像 李华
网站建设 2026/5/7 0:13:15

Keil添加文件实战:构建STM32最小系统项目应用

手动构建STM32最小系统&#xff1a;从零开始掌握Keil项目搭建核心技能 你有没有过这样的经历&#xff1f;明明代码写得没错&#xff0c;却在编译时爆出一堆“找不到头文件”或“未定义符号”的错误。点开Keil工程一看&#xff0c;文件明明就在目录里——可就是不工作。 问题出…

作者头像 李华
网站建设 2026/5/1 9:50:22

钡铼技术ARMxy系列BL440究竟多适合作为具身机器人核心控制器?

过去我们谈机器人&#xff0c;更多关注的是“机械结构”和“运动控制”。而今天&#xff0c;当**具身机器人&#xff08;Embodied AI&#xff09;**成为行业热词&#xff0c;机器人正在从“能动”走向“能感、能算、能决策”&#xff0c;核心控制器的角色也在发生变化。它不再只…

作者头像 李华
网站建设 2026/4/30 21:26:19

企业级个人理财系统管理系统源码|SpringBoot+Vue+MyBatis架构+MySQL数据库【完整版】

摘要 随着社会经济的发展和人们生活水平的提高&#xff0c;个人理财需求日益增长&#xff0c;传统的理财方式已无法满足现代人对高效、便捷、安全的财务管理需求。尤其是在企业环境中&#xff0c;员工和企业的财务数据管理需要更加系统化和智能化。企业级个人理财系统能够整合个…

作者头像 李华
网站建设 2026/5/5 0:26:54

前后端分离论坛网站系统|SpringBoot+Vue+MyBatis+MySQL完整源码+部署教程

摘要 随着互联网技术的快速发展&#xff0c;论坛网站作为信息交流的重要平台&#xff0c;其功能需求和用户体验日益受到重视。传统的前后端耦合架构在开发效率和维护成本上存在较大局限性&#xff0c;难以满足现代论坛系统的高并发、高可扩展性需求。前后端分离架构通过将前端展…

作者头像 李华