Spring Boot动态数据源切换:优雅实现多数据源管理

在复杂的企业应用中,多数据源管理是常见需求。本文将介绍如何基于Spring Boot实现优雅的动态数据源切换方案,通过自定义注解和AOP实现透明化切换。

核心设计思路

通过三层结构实现数据源动态路由:

  1. 注解层:声明式标记数据源
  2. 路由层:基于ThreadLocal的上下文管理
  3. 切面层:在方法执行前后自动切换数据源

核心实现代码

1. 数据源注解定义

less 复制代码
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
    String name() default ""; // 数据源名称
}

2. 动态数据源上下文

typescript 复制代码
public class DynamicDataSourceContext extends AbstractRoutingDataSource {
    
    private static String DEFAULT_DATASOURCE_NAME;
    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();

    public DynamicDataSourceContext(String defaultDataSourceName, 
                                    Map<Object, Object> targetDataSources) {
        super.setDefaultTargetDataSource(targetDataSources.get(defaultDataSourceName));
        super.setTargetDataSources(targetDataSources);
        DEFAULT_DATASOURCE_NAME = defaultDataSourceName;
        super.afterPropertiesSet(); // 关键初始化
    }

    @Override
    protected Object determineCurrentLookupKey() {
        return getDataSourceKey(); // 获取当前数据源标识
    }

    // 数据源操作工具方法
    public static void setDataSourceKey(String key) {
        CONTEXT_HOLDER.set(key);
    }
    
    public static String getDataSourceKey() {
        return CONTEXT_HOLDER.get(); 
    }
    
    public static void clearDataSourceKey() {
        CONTEXT_HOLDER.remove(); 
    }
    
    public static String getDefaultDataSourceName() {
        return DEFAULT_DATASOURCE_NAME;
    }
}

3. AOP切面实现

less 复制代码
@Aspect
@Component
@Order(-1) // 确保在事务切面前执行
public class DataSourceAspect {

    @Around("@within(DataSource) || @annotation(DataSource)")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        
        // 优先级:方法注解 > 类注解 > 默认数据源
        DataSource methodAnno = method.getAnnotation(DataSource.class);
        DataSource classAnno = method.getDeclaringClass().getAnnotation(DataSource.class);
        
        String dataSource = DynamicDataSourceContext.getDefaultDataSourceName();
        
        if (methodAnno != null && StringUtils.hasText(methodAnno.name())) {
            dataSource = methodAnno.name();
        } else if (classAnno != null && StringUtils.hasText(classAnno.name())) {
            dataSource = classAnno.name();
        }

        try {
            DynamicDataSourceContext.setDataSourceKey(dataSource);
            return point.proceed(); // 执行目标方法
        } finally {
            DynamicDataSourceContext.clearDataSourceKey(); // 清理数据源标识
        }
    }
}

自动配置

src/main/resources/META-INF/spring目录下创建文件:

org.springframework.boot.autoconfigure.AutoConfiguration.imports

复制代码
com.test.datasourcestater.aspect.DataSourceAspect

使用示例

1、新增动态切换数据源定义

less 复制代码
@Configuration
@Component
public class DynamicDataSourceConfig {

    //默认数据源定义
    @Resource(name = "defaultDataSource")
    private DataSource defaultDataSource;
    //其他数据源定义
    @Resource(name = "testDataSource")
    private DataSource testDataSource;
    
    @Bean("dynamicDataSource")
    @Primary
    public DynamicDataSourceContext dynamicDataSource() {
        Map<Object, Object> targetDataSources = new HashMap<>();
        //添加默认数据源和其他数据源
        targetDataSources.put("default",defaultDataSource);
        targetDataSources.put("testDataSource",testDataSource);
        return new DynamicDataSourceContext("default", targetDataSources);
    }


    @Bean("dataSourceTransactionManager")
    public DataSourceTransactionManager dataSourceTransactionManager(@Qualifier("dynamicDataSource") DataSource dataSource){
        DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
        dataSourceTransactionManager.setDataSource(dataSource);
        return dataSourceTransactionManager;
    }


    @Bean("jdbcTemplate")
    public JdbcTemplate jdbcTemplate(@Qualifier("dynamicDataSource") DataSource dataSource){
        JdbcTemplate jdbcTemplate = new JdbcTemplate();
        jdbcTemplate.setDataSource(dataSource);
        return  jdbcTemplate;
    }
}

其他数据源定义:

typescript 复制代码
@Configuration
@ConfigurationProperties(prefix = "xxx")
@Data
public class TestDataSourceConfig {

    private String driverClassName;
    private String url;
    private String username;
    private String password;
    private String validationQuery;
    private int initialSize;
    private int maxActive;
    private int maxIdle;
    private int minIdle;


    @Bean("testDataSource")
    public BasicDataSourceDecrypt basicDataSourceDecrypt(){
        BasicDataSourceDecrypt basicDataSourceDecrypt = new BasicDataSourceDecrypt();
        basicDataSourceDecrypt.setUsername(username);
        basicDataSourceDecrypt.setPassword(password);
        basicDataSourceDecrypt.setDriverClassName(driverClassName);
        basicDataSourceDecrypt.setPoolName("xxx");
        // 数据库连接地址
        basicDataSourceDecrypt.setJdbcUrl(url);
        //  最小空闲连接,默认值10,小于0或大于maximum-pool-size,都会重置为maximum-pool-size
        basicDataSourceDecrypt.setMinimumIdle(minIdle);
        // 最大连接数,小于等于0会被重置为默认值10;大于零小于1会被重置为minimum-idle的值
        basicDataSourceDecrypt.setMaximumPoolSize(maxActive);
        // 空闲连接超时时间,默认值600000(10分钟),大于等于max-lifetime且max-lifetime>0,会被重置为0;不等于0且小于10秒,会被重置为10秒。
        basicDataSourceDecrypt.setIdleTimeout(30000);
        // 连接最大存活时间,不等于0且小于30秒,会被重置为默认值30分钟.设置应该比mysql设置的超时时间短
        basicDataSourceDecrypt.setMaxLifetime(360000L);
        // 连接超时时间:毫秒,小于250毫秒,否则被重置为默认值30秒
        basicDataSourceDecrypt.setConnectionTimeout(500);
        // 用于测试连接是否可用的查询语句
        basicDataSourceDecrypt.setConnectionTestQuery(validationQuery);

        return basicDataSourceDecrypt;
    }
}

2、修改原始的DataSourceConfig为默认数据源

  • 将datasource bean 定义名称改成@Bean("defaultDataSource")
  • 将@Qualifier("dataSource")改成@Qualifier("dynamicDataSource") 参考代码,如下:
typescript 复制代码
@Configuration
@ConfigurationProperties(prefix = "xxx")
@Data
public class DataSourceConfig {

    private String driverClassName;
    private String url;
    private String username;
    private String password;
    private String validationQuery;
    private int initialSize;
    private int maxActive;
    private int maxIdle;
    private int minIdle;


    @Bean("defaultDataSource")
    public BasicDataSourceDecrypt basicDataSourceDecrypt(){
        BasicDataSourceDecrypt basicDataSourceDecrypt = new BasicDataSourceDecrypt();
        basicDataSourceDecrypt.setDriverClassName(driverClassName);
        basicDataSourceDecrypt.setUsername(username);
        basicDataSourceDecrypt.setPassword(password);
        basicDataSourceDecrypt.setPoolName("xx");
        // 数据库连接地址
        basicDataSourceDecrypt.setJdbcUrl(url);
        //  最小空闲连接,默认值10,小于0或大于maximum-pool-size,都会重置为maximum-pool-size
        basicDataSourceDecrypt.setMinimumIdle(minIdle);
        // 最大连接数,小于等于0会被重置为默认值10;大于零小于1会被重置为minimum-idle的值
        basicDataSourceDecrypt.setMaximumPoolSize(maxActive);
        // 空闲连接超时时间,默认值600000(10分钟),大于等于max-lifetime且max-lifetime>0,会被重置为0;不等于0且小于10秒,会被重置为10秒。
        basicDataSourceDecrypt.setIdleTimeout(60000);
        // 连接最大存活时间,不等于0且小于30秒,会被重置为默认值30分钟.设置应该比mysql设置的超时时间短
        basicDataSourceDecrypt.setMaxLifetime(600000);
        // 连接超时时间:毫秒,小于250毫秒,否则被重置为默认值30秒
        basicDataSourceDecrypt.setConnectionTimeout(30000);
        // 用于测试连接是否可用的查询语句
        basicDataSourceDecrypt.setConnectionTestQuery(validationQuery);


        return basicDataSourceDecrypt;
    }

    @Bean("messageResource")
    public ResourceBundleMessageSource resourceBundleMessageSource(){
        ResourceBundleMessageSource messageResource = new ResourceBundleMessageSource();
        messageResource.setDefaultEncoding("UTF-8");
        messageResource.setCacheSeconds(0);
        return  messageResource;
    }

    @Bean("dataSourceTransactionManager")
    public DataSourceTransactionManager dataSourceTransactionManager(@Qualifier("dynamicDataSource") BasicDataSourceDecrypt dataSource){
        DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
        dataSourceTransactionManager.setDataSource(dataSource);
        return dataSourceTransactionManager;
    }



    @Bean("jdbcTemplate")
    public JdbcTemplate jdbcTemplate(@Qualifier("dynamicDataSource") BasicDataSourceDecrypt dataSource){
        JdbcTemplate jdbcTemplate = new JdbcTemplate();
        jdbcTemplate.setDataSource(dataSource);
        return  jdbcTemplate;
    }

}

3、修改mybatisPlusConfig配置

dataSource注入,改成注入@Resource(name="dynamicDataSource")

less 复制代码
@Configuration
@EnableTransactionManagement
public class MybatisPlusConfig {

    @Resource(name="dynamicDataSource")
    private DataSource dataSource;
    @Autowired
    private MybatisPlusProperties properties;
    @Autowired
    private ResourceLoader resourceLoader = new DefaultResourceLoader();

    @Autowired(required = false)
    private DatabaseIdProvider databaseIdProvider;

    @Bean
    public DatabaseIdProvider getDatabaseIdProvider() {
        DatabaseIdProvider databaseIdProvider = new VendorDatabaseIdProvider();
        Properties properties = new Properties();
        databaseIdProvider.setProperties(properties);
        return databaseIdProvider;
    }

    /**
     *    mybatis-plus分页插件
     */
    @Bean("paginationInterceptor")
    public PaginationInnerInterceptor paginationInterceptor(@Value("${database.type:mysql}") String databaseType) {
        PaginationInnerInterceptor page = new PaginationInnerInterceptor();
        page.setDbType(DbType.getDbType(databaseType));
        return page;
    }

    @Bean("optimisticLockerInterceptor")
    public OptimisticLockerInnerInterceptor optimisticLockerInterceptor() {
        return new OptimisticLockerInnerInterceptor();
    }
    @Bean("mybatisPlusInterceptor")
    public MybatisPlusInterceptor mybatisPlusInterceptor(@Qualifier("paginationInterceptor") PaginationInnerInterceptor paginationInterceptor,
                                                         @Qualifier("optimisticLockerInterceptor") OptimisticLockerInnerInterceptor optimisticLockerInterceptor){
            MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
            mybatisPlusInterceptor.addInnerInterceptor(paginationInterceptor);
            mybatisPlusInterceptor.addInnerInterceptor(optimisticLockerInterceptor);
        return mybatisPlusInterceptor;
    }

    /**
     * 这里全部使用mybatis-autoconfigure 已经自动加载的资源。不手动指定
     * 配置文件和mybatis-boot的配置文件同步
     * @return
     */
    @Bean("sqlSessionFactory")
    public MybatisSqlSessionFactoryBean mybatisSqlSessionFactoryBean(@Qualifier("globalConfiguration") GlobalConfig globalConfig,
                                                                     @Qualifier("mybatisPlusInterceptor") MybatisPlusInterceptor mybatisPlusInterceptor) {
        MybatisSqlSessionFactoryBean mybatisPlus = new MybatisSqlSessionFactoryBean();
        mybatisPlus.setDataSource(dataSource);
        mybatisPlus.setVfs(SpringBootVFS.class);
        if (StringUtils.hasText(this.properties.getConfigLocation())) {
            mybatisPlus.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation()));
        }
        mybatisPlus.setConfiguration(properties.getConfiguration());
        mybatisPlus.setPlugins(mybatisPlusInterceptor);

        mybatisPlus.setGlobalConfig(globalConfig);
        MybatisConfiguration mc = new MybatisConfiguration();
        mc.setDefaultScriptingLanguage(MybatisXMLLanguageDriver.class);
        mybatisPlus.setConfiguration(mc);
        if (this.databaseIdProvider != null) {
            mybatisPlus.setDatabaseIdProvider(this.databaseIdProvider);
        }
        if (StringUtils.hasLength(this.properties.getTypeAliasesPackage())) {
            mybatisPlus.setTypeAliasesPackage(this.properties.getTypeAliasesPackage());
        }
        if (StringUtils.hasLength(this.properties.getTypeHandlersPackage())) {
            mybatisPlus.setTypeHandlersPackage(this.properties.getTypeHandlersPackage());
        }
        if (!ObjectUtils.isEmpty(this.properties.resolveMapperLocations())) {
            mybatisPlus.setMapperLocations(this.properties.resolveMapperLocations());
        }
        return mybatisPlus;
    }


    @Bean("globalConfiguration")
    public GlobalConfig globalConfig(@Qualifier("myMetaObjectHandler") ModelMetaObjectHandler myMetaObjectHandler,
                                     @Qualifier("customIdGenerator") CustomerIdGenerator customIdGenerator){
        GlobalConfig globalConfig = new GlobalConfig();
        globalConfig.setMetaObjectHandler(myMetaObjectHandler);
        globalConfig.setIdentifierGenerator(customIdGenerator);
        return globalConfig;
    }

    @Bean("myMetaObjectHandler")
    public ModelMetaObjectHandler modelMetaObjectHandler(){
        return new ModelMetaObjectHandler();
    }

    @Bean("customIdGenerator")
    public CustomerIdGenerator customerIdGenerator(){
        return new CustomerIdGenerator();
    }
}

4、使用说明

在Controller层、Service层和Dao层的方法或者类加上@DataSource(name="数据源名字")注解,完成数据源的自动切换,其中数据源名字来自DynamicDataSourceConfig中dynamicDataSource方法中定义的数据源

4.1、类级别注解

在类上添加多数据源注解,类中的所有方法都是使用注解中设置的数据源

4.1.1、Controller层用法
less 复制代码
@RestController
@DataSource(name = "default")
@RequestMapping("/Order")
public class OrderController{
    
    // 所有方法默认使用default数据源
    @GetMapping("/list")
    public List<Order> queryAll() {
        // ...
    }
    
    // 默认使用default数据源
     @GetMapping("/{id}")
    public Order selectById(@PathVariable Long id) {
        // ...
    }
}
4.1.2、Service层用法

Service层使用多数据源注解时,需使用在@Service修饰的类上多数据源注解才能生效

less 复制代码
@Service
@DataSource(name = "default")
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {
    
    // 所有方法默认使用default数据源
    public List<Order> findAll() {
        // ...
    }
    
    // 默认使用default数据源
    public Order findById() {
        // ...
    }
}
4.1.3、Dao层用法

Dao层使用多数据源注解时,需使用在@Component、@Repository或者@Mapper修饰的Dao层接口上多数据源注解才能生效

less 复制代码
@Component
@DataSource(name = "default")
public interface OrderMapper extends BaseMapper<Order> {

    // 使用default数据源
    Order getById(@Param("id")Long id);

    // 使用default数据源
    List<Order> getList();

}

4.2、方法级别注解

在方法上添加多数据源注解,具体方法使用注解中设置的数据源

less 复制代码
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService{

    // 使用testDataSource数据源
    @DataSource(name = "testDataSource")
    public User getUserById(Long id) {
        // ...
    }
    
    // 使用default数据源
    @DataSource(name = "default") 
    public User deleteUserById(Long id) {
        // ...
    }

    // 使用默认数据源(default数据源)
    public void updateUser(User user) {
        // ...
    }
}

Controller层与Dao层用法与类级别注解部分的使用介绍类似,此处不再赘述。

4.3、混合使用

多数据源注解的优先级别:方法级别注解>类级别注解>无注解(默认数据源)

当类和方法中都使用多数据源注解,会按照优先级别选择具体数据源

less 复制代码
@Service
@DataSource(name = "testDataSource")
public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> implements ProductService{

    // 继承类注解,使用testDataSource数据源
    public Product getProduct(Long id) {
        // ...
    }

    // 方法级别注解的优先级最高,优先使用方法注解,使用default数据源
    @DataSource(name = "default")
    public void updateProduct(Product product) {
        // ...
    }
}

Controller层与Dao层用法与类级别注解部分的使用介绍类似,此处不再赘述。

5、注意事项

⚠️ 重要限制 :由于数据源切换基于AOP,与@Transactional注解联用时需注意:

  1. 事务注解应加在数据源注解外层
less 复制代码
// ✅ 正确:事务在外层
@Transactional
@DataSource(name = "slave1")
public void transactionalMethod() { /* ... */ }

// ❌ 危险:数据源切换可能不生效
@DataSource(name = "slave1")
@Transactional
public void riskyMethod() { /* ... */ }

总结

本文实现的多数据源方案具有以下优势:

  1. 非侵入式:通过注解透明切换,不影响业务逻辑
  2. 灵活配置:支持方法级和类级数据源指定
  3. 线程安全:基于ThreadLocal的上下文管理
  4. 易于扩展:可快速添加新数据源

通过这种设计,开发者可以轻松管理多个数据源,特别适用于多租户系统、读写分离、分库分表等复杂场景。完整代码已托管至Gitee,gitee地址:gitee.com/mutigmss/mu...

欢迎在评论区交流使用体验和优化建议!

相关推荐
扑克中的黑桃A9 分钟前
Python 如何获取 request response body
java
用户307429716715816 分钟前
Spring AI 评估-优化器模式完整指南
java·spring boot
扑克中的黑桃A16 分钟前
Spring Mvc + Easyui中根据查询结果导出文件
java
BillKu24 分钟前
【前后前】导入Excel文件闭环模型:Vue3前端上传Excel文件,【Java后端接收、解析、返回数据】,Vue3前端接收展示数据
java·前端·excel
用户05956611920929 分钟前
现代化 Java 企业级应用分层开发架构设计最佳实践
java·架构·全栈
代码or搬砖32 分钟前
Spring AOP全面详讲
java·spring
Musennn40 分钟前
leetcode51.N皇后:回溯算法与冲突检测的核心逻辑
java·数据结构·算法·leetcode
TT哇40 分钟前
【数据结构试题】
java·数据结构
嗜好ya44 分钟前
JAVA集合篇--深入理解ConcurrentHashMap图解版
java·开发语言
stein_java1 小时前
springMVC-15 异常处理
java·spring