AbstractRoutingDataSource
是 Spring 框架中的一个抽象类,它位于 org.springframework.jdbc.datasource.lookup
包下,主要用于实现动态数据源的切换。以下将从其作用、原理、使用步骤和示例代码等方面进行详细介绍。
介绍
作用
在一些复杂的业务场景中,可能需要在不同的数据源之间进行动态切换,例如读写分离(读操作使用从库,写操作使用主库)、多租户系统(不同租户使用不同的数据库)等。AbstractRoutingDataSource
提供了一个简单而灵活的机制来实现这种动态数据源切换的功能。
原理
AbstractRoutingDataSource
实现了 javax.sql.DataSource
接口,它内部维护了一个目标数据源的映射关系(通常是一个 Map
),并通过一个抽象方法 determineCurrentLookupKey()
来决定当前使用哪个数据源。当调用 getConnection()
方法获取数据库连接时,AbstractRoutingDataSource
会调用 determineCurrentLookupKey()
方法获取当前的数据源键,然后根据这个键从映射关系中查找对应的数据源,并从该数据源获取连接。
使用步骤
- 创建多个数据源:配置多个不同的数据源,例如主库数据源和从库数据源。
- 继承
AbstractRoutingDataSource
:创建一个自定义的数据源路由类,继承AbstractRoutingDataSource
,并实现determineCurrentLookupKey()
方法。 - 配置数据源映射:将多个数据源配置到自定义的数据源路由类中。
- 动态切换数据源:在业务代码中通过某种方式设置当前要使用的数据源键。
示例代码
1. 配置多个数据源
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import javax.sql.DataSource;
@Configuration
public class DataSourceConfig {
@Bean(name = "masterDataSource")
public DataSource masterDataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/master_db");
dataSource.setUsername("root");
dataSource.setPassword("password");
return dataSource;
}
@Bean(name = "slaveDataSource")
public DataSource slaveDataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/slave_db");
dataSource.setUsername("root");
dataSource.setPassword("password");
return dataSource;
}
}
2. 继承 AbstractRoutingDataSource
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSource();
}
}
3. 创建数据源上下文持有者
public class DataSourceContextHolder {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
public static void setDataSource(String dataSource) {
contextHolder.set(dataSource);
}
public static String getDataSource() {
return contextHolder.get();
}
public static void clearDataSource() {
contextHolder.remove();
}
}
4. 配置数据源映射
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class DynamicDataSourceConfig {
@Bean
public AbstractRoutingDataSource routingDataSource(DataSource masterDataSource, DataSource slaveDataSource) {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("master", masterDataSource);
targetDataSources.put("slave", slaveDataSource);
dynamicDataSource.setDefaultTargetDataSource(masterDataSource);
dynamicDataSource.setTargetDataSources(targetDataSources);
return dynamicDataSource;
}
}
5. 动态切换数据源
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Autowired
private JdbcTemplate jdbcTemplate;
public void readData() {
DataSourceContextHolder.setDataSource("slave");
try {
// 执行读操作
jdbcTemplate.queryForList("SELECT * FROM users");
} finally {
DataSourceContextHolder.clearDataSource();
}
}
public void writeData() {
DataSourceContextHolder.setDataSource("master");
try {
// 执行写操作
jdbcTemplate.update("INSERT INTO users (name, age) VALUES (?, ?)", "John", 25);
} finally {
DataSourceContextHolder.clearDataSource();
}
}
}
在业务代码中,只需要调用 DataSourceContextHolder.setDataSource()
方法设置当前要使用的数据源键,就可以切换到相应的数据源进行数据库操作。
使用
在使用 AbstractRoutingDataSource
实现动态数据源切换时,除了显式调用切换方法外,还可以通过一些隐式的方式来实现数据源切换。
1. 使用 AOP 结合注解
可以通过自定义注解和 AOP(面向切面编程)来实现隐式的数据源切换。当方法上添加了特定注解时,在方法执行前自动切换数据源,方法执行后再恢复默认数据源。
步骤:
- 定义数据源注解:用于标记需要切换数据源的方法。
- 创建 AOP 切面:在方法执行前后进行数据源的切换和恢复操作。
- 在业务方法上使用注解:标记需要切换数据源的方法。
示例代码:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
// 定义数据源注解
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSourceSwitch {
String value() default "master";
}
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
// 创建 AOP 切面
@Aspect
@Component
public class DataSourceSwitchAspect {
@Before("@annotation(com.example.demo.DataSourceSwitch)")
public void before(JoinPoint point) {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
DataSourceSwitch dataSourceSwitch = method.getAnnotation(DataSourceSwitch.class);
if (dataSourceSwitch != null) {
DataSourceContextHolder.setDataSource(dataSourceSwitch.value());
}
}
@After("@annotation(com.example.demo.DataSourceSwitch)")
public void after(JoinPoint point) {
DataSourceContextHolder.clearDataSource();
}
}
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Autowired
private JdbcTemplate jdbcTemplate;
@DataSourceSwitch("slave")
public void readData() {
// 执行读操作
jdbcTemplate.queryForList("SELECT * FROM users");
}
@DataSourceSwitch("master")
public void writeData() {
// 执行写操作
jdbcTemplate.update("INSERT INTO users (name, age) VALUES (?, ?)", "John", 25);
}
}
2. 根据请求路径或请求参数切换
在 Web 应用中,可以根据请求的路径或请求参数来隐式地切换数据源。例如,不同的租户可能有不同的请求路径前缀,根据这个前缀来切换对应的数据源。
步骤:
- 创建过滤器或拦截器:在请求处理前根据请求路径或参数切换数据源。
- 在请求处理完成后恢复默认数据源。
示例代码(使用过滤器):
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
// 创建过滤器
@WebFilter(urlPatterns = "/*")
public class DataSourceSwitchFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String requestURI = httpRequest.getRequestURI();
if (requestURI.startsWith("/tenant1")) {
DataSourceContextHolder.setDataSource("tenant1DataSource");
} else if (requestURI.startsWith("/tenant2")) {
DataSourceContextHolder.setDataSource("tenant2DataSource");
}
try {
chain.doFilter(request, response);
} finally {
DataSourceContextHolder.clearDataSource();
}
}
}
3. 根据业务逻辑自动切换
在某些业务场景中,可以根据业务逻辑的状态自动切换数据源。例如,在一个电商系统中,当订单处于不同的状态时,可能需要使用不同的数据源进行数据处理。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
@Autowired
private JdbcTemplate jdbcTemplate;
public void processOrder(Order order) {
if (order.getStatus().equals("PENDING")) {
DataSourceContextHolder.setDataSource("pendingDataSource");
} else if (order.getStatus().equals("COMPLETED")) {
DataSourceContextHolder.setDataSource("completedDataSource");
}
try {
// 处理订单业务逻辑
jdbcTemplate.update("UPDATE orders SET status = ? WHERE id = ?", "PROCESSING", order.getId());
} finally {
DataSourceContextHolder.clearDataSource();
}
}
}
class Order {
private Long id;
private String status;
// 省略 getter 和 setter 方法
}
结合Mybatis
当结合 MyBatis 与 AbstractRoutingDataSource
实现动态数据源切换时,整体思路是在 MyBatis 操作数据库前,根据业务逻辑隐式或显式地切换数据源。
1. 环境准备
首先确保项目中添加了 Spring、Spring Boot、MyBatis 和 MyBatis-Spring-Boot-Starter 的依赖。以 Maven 为例,在 pom.xml
中添加以下依赖:
<dependencies>
<!-- Spring Boot Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- MyBatis Spring Boot Starter -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.1</version>
</dependency>
<!-- MySQL Connector -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
2. 配置多个数据源
配置多个不同的数据源,例如主库数据源和从库数据源。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import javax.sql.DataSource;
@Configuration
public class DataSourceConfig {
@Bean(name = "masterDataSource")
public DataSource masterDataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/master_db");
dataSource.setUsername("root");
dataSource.setPassword("password");
return dataSource;
}
@Bean(name = "slaveDataSource")
public DataSource slaveDataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/slave_db");
dataSource.setUsername("root");
dataSource.setPassword("password");
return dataSource;
}
}
3. 继承 AbstractRoutingDataSource
创建一个自定义的数据源路由类,继承 AbstractRoutingDataSource
,并实现 determineCurrentLookupKey()
方法。
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSource();
}
}
4. 创建数据源上下文持有者
用于在当前线程中存储和获取数据源信息。
public class DataSourceContextHolder {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
public static void setDataSource(String dataSource) {
contextHolder.set(dataSource);
}
public static String getDataSource() {
return contextHolder.get();
}
public static void clearDataSource() {
contextHolder.remove();
}
}
5. 配置数据源映射
将多个数据源配置到自定义的数据源路由类中,并配置 MyBatis 的 SqlSessionFactory
使用该动态数据源。
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
@Configuration
@MapperScan("com.example.demo.mapper")
public class DynamicDataSourceConfig {
@Bean
public AbstractRoutingDataSource routingDataSource(DataSource masterDataSource, DataSource slaveDataSource) {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("master", masterDataSource);
targetDataSources.put("slave", slaveDataSource);
dynamicDataSource.setDefaultTargetDataSource(masterDataSource);
dynamicDataSource.setTargetDataSources(targetDataSources);
return dynamicDataSource;
}
@Bean
public SqlSessionFactory sqlSessionFactory(AbstractRoutingDataSource routingDataSource) throws Exception {
SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(routingDataSource);
return sessionFactory.getObject();
}
}
6. 定义 MyBatis Mapper 接口
创建一个简单的 MyBatis Mapper 接口,用于操作数据库。
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
import java.util.Map;
@Mapper
public interface UserMapper {
@Select("SELECT * FROM users")
List<Map<String, Object>> getAllUsers();
@Select("INSERT INTO users (name, age) VALUES (#{name}, #{age})")
void insertUser(String name, Integer age);
}
7. 结合 AOP 实现隐式数据源切换
使用自定义注解和 AOP 来实现隐式的数据源切换。
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
// 定义数据源注解
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSourceSwitch {
String value() default "master";
}
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
// 创建 AOP 切面
@Aspect
@Component
public class DataSourceSwitchAspect {
@Before("@annotation(com.example.demo.DataSourceSwitch)")
public void before(JoinPoint point) {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
DataSourceSwitch dataSourceSwitch = method.getAnnotation(DataSourceSwitch.class);
if (dataSourceSwitch != null) {
DataSourceContextHolder.setDataSource(dataSourceSwitch.value());
}
}
@After("@annotation(com.example.demo.DataSourceSwitch)")
public void after(JoinPoint point) {
DataSourceContextHolder.clearDataSource();
}
}
8. 业务服务类使用
在业务服务类中使用 UserMapper
进行数据库操作,并通过注解隐式切换数据源。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@DataSourceSwitch("slave")
public List<Map<String, Object>> getAllUsers() {
return userMapper.getAllUsers();
}
@DataSourceSwitch("master")
public void insertUser(String name, Integer age) {
userMapper.insertUser(name, age);
}
}
9. 控制器调用
创建一个简单的控制器来调用业务服务类的方法。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
@RestController
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/users")
public List<Map<String, Object>> getAllUsers() {
return userService.getAllUsers();
}
@PostMapping("/users")
public void insertUser(@RequestParam String name, @RequestParam Integer age) {
userService.insertUser(name, age);
}
}
通过以上步骤,就可以将 MyBatis 与 AbstractRoutingDataSource
结合使用,实现动态数据源的切换。