Spring Boot 实现根据 URL 切换多个数据库源

Spring Boot 实现根据 URL 切换多个数据库源

很多情况下,网站会用到多数据源的情况,比如多语言网站、多业务网站等。

在 Spring Boot 中,使用其自带了的路由数据源 (AbstractRoutingDataSource),可以很容易就能实现多数据库源的自动切换。

本文详细介绍如何实现以上目的,并且提供 Spring Boot 原生JDBCMyBatis 的实现方式。

1 原理简析

首先,我们需要定义一个具体类,例如叫MyRourtingDataSource,它继承自 Spring Boot 的抽象类 AbstractRoutingDataSource,使用抽象类中的 setTargetDataSources() 方法注册所有的数据源

接着,定义一个数据源拦截器(DataSource Interceptor),根据不同 URL 来切换不同的数据源。

2 范例说明

本文,我们通过实现一个简单的多语言站点,同时拥支持中文英文两种语言,而且各保存在不同的数据库中,中文保存在chinese数据库, 英文保存在english数据库中。需要注意的是,这两个表的表数量和表结构必须要完全一样

我们根据不同的 URL 来判定使用不同的数据库,效果如下:

3. 准备数据表

为方便,我们这里使用两个 MySQL 数据库,当然也可以使用其他如SQL ServerORACLEPostgreSQL等数据库,只要修改 Spring Boot 连接的 Driver 就可以了。

文章系统的数据库和表:

create database english;
use english;

create table content
(
  id int primary key,
  name varchar(50)
);

insert into content (id, name) values (1, 'Post 1');
insert into content (id, name) values (2, 'Post 2');

视频系统的数据库和表:

create database chinese;
use chinese;

create table content
(
  id int primary key,
  name varchar(50)
);

insert into content (id, name) values (1, '视频 1');
insert into content (id, name) values (2, '视频 2');

4 创建 Spring Boot 工程

打开 Idea 编辑器,选择菜单 FileNewProject…,创建一个 Spring Boot 项目,如下:

下一步选择 Spring Web Thymeleaf 模板引擎等组件,具体如下:

  • Web
    • Spring Web
  • Template Engines
    • Thymeleaf
  • SQL
    • JDBC API
    • MySQL Driver – 根据数据库选择,这里我们用 MySQL
    • MyBatis Framework – 如果直接用 JDBC 可以不选该项

再点击 Finish 完成创建,最终的 pom.xml 如下。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.2</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.awaimai</groupId>
    <artifactId>multi-ds-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>multi-ds-demo</name>
    <description>multi-ds-demo</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.0</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

5. 配置多个数据源和路由数据源

首先,创建一个多数据源配置文件 src/main/resources/datasource.properties

# DataSource1
spring.datasource.first.driverClassName=com.mysql.cj.jdbc.Driver
spring.datasource.first.jdbcUrl=jdbc:mysql://localhost:3306/english
spring.datasource.first.username=root
spring.datasource.first.password=12345678

# DataSource2
spring.datasource.second.driverClassName=com.mysql.cj.jdbc.Driver
spring.datasource.second.jdbcUrl=jdbc:mysql://localhost:3306/chinese
spring.datasource.second.username=root
spring.datasource.second.password=12345678

Spring Boot 默认会自动配置 DataSource,所以我们需要手动将自动配置关闭,然后再自己手动配置数据源。

需要关闭的自动配置项:

  1. DataSourceAutoConfiguration
  2. DataSourceTransactionManagerAutoConfiguration

打开主函数类 MainApplication.java,在 @SpringBootApplication 注解中增加 exclude 禁用自动配置:

package com.awaimai.multidsdemo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration;

@SpringBootApplication(exclude = {
        DataSourceAutoConfiguration.class,
        DataSourceTransactionManagerAutoConfiguration.class})
public class MainApplication {
    public static void main(String[] args) {
        SpringApplication.run(MainApplication.class, args);
    }
}

然后,添加数据源配置文件 config/RoutingDatasourceConfig.java

package com.awaimai.multidsdemo.config;

import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.mybatis.spring.transaction.SpringManagedTransactionFactory;
import org.springframework.beans.factory.annotation.Autowired;
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 javax.sql.DataSource;

@Configuration
public class RoutingDatasourceConfig {
    @Autowired
    @Bean(name = "dataSource")
    public DataSource getDataSource(DataSource dataSource1, DataSource dataSource2) {
      System.out.println("## Create DataSource from dataSource1 & dataSource2");
      MyRoutingDataSource dataSource = new MyRoutingDataSource();
      dataSource.initDataSources(dataSource1, dataSource2);

      return dataSource;
    }

    @Bean(name = "dataSource1")
    @ConfigurationProperties("spring.datasource.first")
    public DataSource getDataSource1() {
      return DataSourceBuilder.create().build();
    }

    @Bean(name = "dataSource2")
    @ConfigurationProperties("spring.datasource.second")
    public DataSource getDataSource2() {
      return DataSourceBuilder.create().build();
    }
}

增加一个 AbstractRoutingDataSource的实现类,用以注册所有数据源,其中determineCurrentLookupKey()方法会根据请求中保存的 key 来决定使用那个数据源(其实就是返回键名,Spring Boot会自根据 key 返回对应的DataSource)。

数据源注册和决策类 config/MyRoutingDataSource.java

package com.awaimai.multidsdemo.config;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

public class MyRoutingDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
      HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder
              .getRequestAttributes())
              .getRequest();

      String datasourceKey = (String) request.getAttribute("datasourceKey");
      if (datasourceKey == null) {
        datasourceKey = "EN_DATASOURCE";
      }

      return datasourceKey;
    }

    public void initDataSources(DataSource dataSource1, DataSource dataSource2) {
      Map<Object, Object> datasourceMap = new HashMap<>();
      datasourceMap.put("EN_DATASOURCE", dataSource1);
      datasourceMap.put("CN_DATASOURCE", dataSource2);

      this.setTargetDataSources(datasourceMap);
    }
}

6. 拦截器

拦截器解析每个请求的 URL,根据 URL 来保存不同的数据源信息(key)。

数据源拦截器:interceptor/DataSourceInterceptor.java

package com.awaimai.multidsdemo.config;

import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class DataSourceInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String contextPath = request.getServletContext().getContextPath();

        String prefixEn = contextPath + "/en";
        String prefixCn = contextPath + "/cn";

        String uri = request.getRequestURI();
        System.out.println("URI: "+ uri);

        if(uri.startsWith(prefixEn)) {
          request.setAttribute("datasourceKey", "EN_DATASOURCE");
        } else if(uri.startsWith(prefixCn)) {
          request.setAttribute("datasourceKey", "CN_DATASOURCE");
        }

        return true;
    }
}

注册拦截器类 config/WebMvcConfig.java

package com.awaimai.multidsdemo.config;

import com.awaimai.multidsdemo.interceptor.DataSourceInterceptor;
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 WebMvcConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new DataSourceInterceptor());
    }
}

7. DAO、控制器和视图

读取数据我们直接使用 JDBC,dao/DataDao.java

package com.awaimai.multidsdemo.dao;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.support.JdbcDaoSupport;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

import javax.sql.DataSource;
import java.util.List;

@Repository
@Transactional
public class DataDao extends JdbcDaoSupport {
  @Autowired
  public DataDao(DataSource dataSource) {
    this.setDataSource(dataSource);
  }

  public List<String> getContent() {
    return this.getJdbcTemplate().queryForList("select name from content", String.class);
  }
}

控制器 controller/MainController

package com.awaimai.multidsdemo.controller;

import com.awaimai.multidsdemo.dao.DataDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class MainController {
    @Autowired
    private DataDao dataDao;

    @GetMapping("/{locale:en|cn}/list")
    public String post(Model model) {
        model.addAttribute("posts", dataDao.getContent());
        return "content";
    }
}

视图文件: src/main/resources/templates/content.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8"/>
  <title>Post System</title>
</head>

<body>
<a th:href="@{/en/list}">English</a> | <a th:href="@{/cn/list}">简体中文</a>

<ul>
  <li th:each="post : ${posts}" th:utext="${post}"></li>
</ul>
</body>

</html>

8 运行

最后,运行 MainApplication.java 文件(名字可能不同,就是有 main 函数的那个文件),效果如下图。

9 使用 MyBatis

要用 MyBatis,需要额外的几个简单的配置。

首先,添加 mapper 文件 mapper/DataMapper.java

package com.awaimai.multidsdemo.mapper;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;

import java.util.List;

@Mapper
public interface DataMapper {
    @Select("select name from content")
    List<String> getContent();
}

将该 mapper 目录增加到 MapperScan 扫描路径中,同时配置 sqlSessionFactory,文件 config/RoutingDatasourceConfig.java

@Configuration
@MapperScan("com.awaimai.multidsdemo.mapper") // 增加 Mapper 扫描路径
public class RoutingDatasourceConfig {
    // 原来其他的配置

    // 增加 sqlSessionFactory bean
    @Autowired
    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
      SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
      factoryBean.setDataSource(dataSource);
      factoryBean.setTransactionFactory(new SpringManagedTransactionFactory());

      return factoryBean.getObject();
    }

    // 原来其他的配置
    //...
}

修改控制器,使用 Mapper 方式查询数据, 文件controller/MainController.java

package com.awaimai.multidsdemo.controller;

import com.awaimai.multidsdemo.mapper.DataMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class MainController {
    @Autowired
    private DataMapper dataMapper;

    @GetMapping("/{locale:en|cn}/list")
    public String post(Model model) {
        model.addAttribute("posts", dataMapper.getContent());

        return "content";
    }
}

然后重启,会看到和使用 JDBC 一样的效果。

参考资料:

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

昵称 *