Spring Boot 实现根据 URL 切换多个数据库源
Spring Boot 实现根据 URL 切换多个数据库源
很多情况下,网站会用到多数据源的情况,比如多语言网站、多业务网站等。
在 Spring Boot 中,使用其自带了的路由数据源 (AbstractRoutingDataSource
),可以很容易就能实现多数据库源的自动切换。
本文详细介绍如何实现以上目的,并且提供 Spring Boot 原生JDBC
和 MyBatis
的实现方式。
1 原理简析
首先,我们需要定义一个具体类,例如叫MyRourtingDataSource
,它继承自 Spring Boot 的抽象类 AbstractRoutingDataSource
,使用抽象类中的 setTargetDataSources()
方法注册所有的数据源。
接着,定义一个数据源拦截器(DataSource Interceptor
),根据不同 URL 来切换不同的数据源。
2 范例说明
本文,我们通过实现一个简单的多语言站点,同时拥支持中文和英文两种语言,而且各保存在不同的数据库中,中文保存在chinese
数据库, 英文保存在english
数据库中。需要注意的是,这两个表的表数量和表结构必须要完全一样。
我们根据不同的 URL 来判定使用不同的数据库,效果如下:
3. 准备数据表
为方便,我们这里使用两个 MySQL 数据库,当然也可以使用其他如SQL Server
、ORACLE
或PostgreSQL
等数据库,只要修改 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 编辑器,选择菜单 File – New – Project…,创建一个 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
,所以我们需要手动将自动配置关闭,然后再自己手动配置数据源。
需要关闭的自动配置项:
DataSourceAutoConfiguration
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 一样的效果。
参考资料: