Spring Boot 动态数据源切换
背景
随着互联网应用的快速发展,多数据源的需求日益增多。Spring Boot 以其简洁的配置和强大的功能,成为实现动态数据源切换的理想选择。本文将通过具体的配置和代码示例,详细介绍如何在 Spring Boot 应用中实现动态数据源切换,帮助开发者高效应对不同业务场景下的数据管理需求。无论是读写分离还是数据隔离,都能轻松搞定。
AOP动态代理
AOP注解
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface TargetDataSource {
String name();
}
AOP切面类
@Aspect
@Component
public class DataSourceAspect {
@Pointcut("@annotation(com.example.aliyunai.db.TargetDataSource)")
public void dataSourcePointcut() {
}
@Before("dataSourcePointcut()")
public void changeDataSource(JoinPoint point) {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
TargetDataSource targetDataSource = method.getAnnotation(TargetDataSource.class);
if (targetDataSource != null) {
String dataSourceName = targetDataSource.name();
DataSourceContextHolder.setDataSourceKey(dataSourceName);
}
}
@After("dataSourcePointcut()")
public void clearDataSource(JoinPoint point) {
DataSourceContextHolder.clearDataSourceKey();
}
}
数据源配置
@Configuration
public class DataSourceConfig {
@Bean(name = "master")
public DataSource primaryDataSource() {
return DataSourceBuilder.create()
.type(HikariDataSource.class)
.driverClassName("")
.url("")
.username("")
.password("")
.build();
}
@Bean(name = "slave")
public DataSource secondaryDataSource() {
return DataSourceBuilder.create()
.type(HikariDataSource.class)
.driverClassName("")
.url("")
.username("")
.password("")
.build();
}
@Bean(name = "dynamicDataSource")
@Primary
public DynamicDataSource dynamicDataSource(@Qualifier("master") DataSource master,
@Qualifier("slave") DataSource slave) {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("master", master);
targetDataSources.put("slave", slave);
DynamicDataSource dynamicDataSource = new DynamicDataSource();
dynamicDataSource.setTargetDataSources(targetDataSources);
dynamicDataSource.setDefaultTargetDataSource(master);
return dynamicDataSource;
}
}
线程上下文
public class DataSourceContextHolder {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
public static void setDataSourceKey(String key) {
contextHolder.set(key);
}
public static String getDataSourceKey() {
return contextHolder.get();
}
public static void clearDataSourceKey() {
contextHolder.remove();
}
}
动态数据源设置
public class DynamicDataSource extends AbstractRoutingDataSource {
private static final Logger logger = LoggerFactory.getLogger(DynamicDataSource.class);
@Override
protected Object determineCurrentLookupKey() {
String dataSourceKey = DataSourceContextHolder.getDataSourceKey();
logger.info("Determining current data source: {}", dataSourceKey);
return dataSourceKey;
}
}
service类
@Service
public class UserService {
@Resource
private UserMapper userMapper;
@TargetDataSource(name = "master")
public User queryFromPrimary() {
User user = userMapper.selectById(1);
return user;
}
@TargetDataSource(name = "slave")
public User queryFromSecondary() {
User user = userMapper.selectById(1L);
return user;
}
}
Mybatis 拦截器
@Component
@Intercepts({
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class,Object.class}
)})
public class DynamicDataSourceInterceptor implements Interceptor {
private static final Logger logger = LoggerFactory.getLogger(DynamicDataSourceInterceptor.class);
// 假设这里有一个获取当前数据源标识的方法,你需要根据实际项目中的实现来替换
private String getCurrentDataSourceKey() {
// 示例:从某个上下文持有者中获取数据源标识,这里只是示意,实际要替换
return DataSourceContextHolder.getDataSourceKey();
}
@Override
public Object intercept(Invocation invocation) throws Throwable {
MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
Object parameter = invocation.getArgs()[1];
BoundSql boundSql = mappedStatement.getBoundSql(parameter);
String sql = boundSql.getSql();
//对单个表进行处理
logger.info("sql:"+sql);
String mappedStatementId = mappedStatement.getId();
//对所有查询进行处理
if (mappedStatementId.contains("query")) {
String table = getTable(sql);
if (table.equals("user")){
DataSourceContextHolder.setDataSourceKey(getCurrentDataSourceKey());
}
//增删改
}else if (mappedStatementId.contains("update")) {
DataSourceContextHolder.setDataSourceKey(getCurrentDataSourceKey());
}
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
private static String getTable(String sql) {
// 定义正则表达式模式,用于匹配 "from" 和 "where" 之间的表名
Pattern pattern = Pattern.compile("FROM\\s+(\\w+)\\s+WHERE");
Matcher matcher = pattern.matcher(sql);
if (matcher.find()) {
return matcher.group(1);
}
return null;
}
}