You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用Spring Data JPA实现单WAR包运行时按域名切换数据库

解决方案:基于请求域名动态切换数据库/Schema

核心思路

用Spring原生的AbstractRoutingDataSource实现数据源动态路由,无需修改现有Repository代码,仅通过请求域名自动匹配对应的数据库或schema,实现单WAR包适配多域名多数据源的需求。

具体实现步骤

1. 线程安全存储请求域名

用ThreadLocal存储当前请求的域名,保证每个请求线程的独立性,避免多请求间的数据源串用:

public class DomainContextHolder {
    private static final ThreadLocal<String> DOMAIN_HOLDER = new ThreadLocal<>();

    public static void setDomain(String domain) {
        DOMAIN_HOLDER.set(domain);
    }

    public static String getDomain() {
        return DOMAIN_HOLDER.get();
    }

    public static void clear() {
        DOMAIN_HOLDER.remove();
    }
}

2. 拦截请求提取域名

创建Spring MVC拦截器,在请求进入时提取域名存入上下文,请求结束后清理:

public class DomainInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String domain = request.getServerName();
        DomainContextHolder.setDomain(domain);
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        DomainContextHolder.clear();
    }
}

注册拦截器到Web配置:

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new DomainInterceptor()).addPathPatterns("/**");
    }
}

3. 配置动态路由数据源

分两种场景实现,根据你的实际数据库架构选择:

场景A:同一数据库实例下的不同Schema

所有schema共享同一个数据库连接,仅动态切换schema名称:

@Configuration
public class DataSourceConfig {
    // 基础数据源(连接到数据库实例,不指定schema)
    @Bean
    public DataSource baseDataSource() {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/");
        dataSource.setUsername("root");
        dataSource.setPassword("your_password");
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        return dataSource;
    }

    // 动态路由数据源
    @Bean
    public DataSource routingDataSource(DataSource baseDataSource) {
        AbstractRoutingDataSource routingDataSource = new AbstractRoutingDataSource() {
            @Override
            protected Object determineCurrentLookupKey() {
                String domain = DomainContextHolder.get();
                return switch (domain) {
                    case "myapp1.mydomain.com" -> "schema1";
                    case "myapp2.mydomain.com" -> "schema2";
                    case "myapp3.mydomain.com" -> "schema3";
                    default -> throw new IllegalArgumentException("Unknown domain: " + domain);
                };
            }
        };
        routingDataSource.setDefaultTargetDataSource(baseDataSource);
        return routingDataSource;
    }

    // 配置JPA实体管理器
    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource routingDataSource) {
        LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
        em.setDataSource(routingDataSource);
        em.setPackagesToScan("com.yourpackage.entity");
        HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        em.setJpaVendorAdapter(vendorAdapter);
        em.setJpaProperties(hibernateProperties());
        return em;
    }

    private Properties hibernateProperties() {
        Properties props = new Properties();
        props.put("hibernate.hbm2ddl.auto", "none");
        props.put("hibernate.show_sql", "true");
        return props;
    }
}

添加AOP切面,在Repository操作前执行schema切换SQL(不同数据库语法不同):

@Aspect
@Component
public class SchemaSwitchAspect {
    @Autowired
    private DataSource routingDataSource;

    @Before("execution(* com.yourpackage.repository.*.*(..))")
    public void switchSchema() throws SQLException {
        String schema = (String) ((AbstractRoutingDataSource) routingDataSource).determineCurrentLookupKey();
        if (schema != null) {
            // MySQL用USE语句,PostgreSQL用SET search_path,Oracle用ALTER SESSION
            try (Connection conn = routingDataSource.getConnection()) {
                String sql = String.format("USE %s;", schema);
                conn.createStatement().execute(sql);
            }
        }
    }
}

场景B:不同数据库实例(每个域名对应独立数据库)

每个域名对应独立的数据库连接,直接路由到对应数据源:

@Configuration
public class DataSourceConfig {
    // 配置各schema对应的数据源
    @Bean
    public DataSource schema1DataSource() {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/schema1");
        dataSource.setUsername("root");
        dataSource.setPassword("your_password");
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        return dataSource;
    }

    @Bean
    public DataSource schema2DataSource() {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/schema2");
        dataSource.setUsername("root");
        dataSource.setPassword("your_password");
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        return dataSource;
    }

    @Bean
    public DataSource schema3DataSource() {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/schema3");
        dataSource.setUsername("root");
        dataSource.setPassword("your_password");
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        return dataSource;
    }

    // 动态路由数据源
    @Bean
    public DataSource routingDataSource(DataSource schema1DataSource, DataSource schema2DataSource, DataSource schema3DataSource) {
        AbstractRoutingDataSource routingDataSource = new AbstractRoutingDataSource() {
            @Override
            protected Object determineCurrentLookupKey() {
                String domain = DomainContextHolder.get();
                return switch (domain) {
                    case "myapp1.mydomain.com" -> "schema1";
                    case "myapp2.mydomain.com" -> "schema2";
                    case "myapp3.mydomain.com" -> "schema3";
                    default -> throw new IllegalArgumentException("Unknown domain: " + domain);
                };
            }
        };

        // 绑定数据源与路由key
        Map<Object, Object> dataSourceMap = new HashMap<>();
        dataSourceMap.put("schema1", schema1DataSource);
        dataSourceMap.put("schema2", schema2DataSource);
        dataSourceMap.put("schema3", schema3DataSource);
        routingDataSource.setTargetDataSources(dataSourceMap);
        routingDataSource.setDefaultTargetDataSource(schema1DataSource);

        return routingDataSource;
    }

    // JPA实体管理器配置使用routingDataSource即可
    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource routingDataSource) {
        LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
        em.setDataSource(routingDataSource);
        em.setPackagesToScan("com.yourpackage.entity");
        HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        em.setJpaVendorAdapter(vendorAdapter);
        return em;
    }
}

关键优势

  • 无需修改现有Repository代码,原有CRUD逻辑直接复用
  • 单WAR包部署,避免维护多个重复代码包
  • 线程安全,每个请求的域名与数据源绑定,无串用风险

注意事项

  • 确保Tomcat已配置多域名虚拟主机,指向同一个WAR包
  • 数据库连接池需配置足够连接数,应对多数据源并发
  • 切换schema的SQL需根据数据库类型调整(如MySQL/PostgreSQL/Oracle语法不同)

内容的提问来源于stack exchange,提问作者Abhijit C

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.15 19:17:42