Spring Boot動態數據源實現詳解

一、為什麼需要動態數據源

在項目開發中,我們經常需要使用多個數據源,比如主庫和從庫的讀寫分離,或者不同的租戶使用不同的數據源等。如果每個數據源都需要手動配置,那麼會增加很多不必要的工作量,而且代碼會變得冗長和難以維護。這時候,動態數據源就可以派上用場。

動態數據源就是可以在程序運行時根據需要動態地切換資料庫連接的數據源。它可以減少不必要的配置工作,提高代碼的可維護性和可擴展性。

二、如何實現動態數據源

1. 自定義數據源

首先,我們需要定義一個數據源的抽象,具體實現交給具體的數據源來完成。下面是一個簡單的抽象示例:

public abstract class AbstractRoutingDataSource extends AbstractDataSource {

    /**
     * 獲取當前線程上的數據源路由
     *
     * @return 數據源路由,如果為空,則默認返回默認數據源
     */
    protected abstract Object determineCurrentDataSourceKey();

    @Override
    public Connection getConnection() throws SQLException {
        // 獲取數據源
        DataSource dataSource = determineCurrentDataSource();
        // 獲取連接
        Connection connection = dataSource.getConnection();
        // 返回連接
        return connection;
    }

    /**
     * 獲取當前數據源
     *
     * @return 當前數據源
     */
    private DataSource determineCurrentDataSource() {
        // 獲取數據源路由
        Object dataSourceKey = determineCurrentDataSourceKey();
        // 如果數據源路由為空,則返回默認數據源
        if (dataSourceKey == null) {
            return getDefaultDataSource();
        }
        // 否則返回指定的數據源
        DataSource dataSource = getTargetDataSources().get(dataSourceKey);
        if (dataSource == null) {
            throw new IllegalStateException("Cannot find data source with key: " + dataSourceKey);
        }
        return dataSource;
    }

}

在上面的抽象類中,我們定義了一個determineCurrentDataSourceKey()方法來獲取當前線程上的數據源路由,以及determineCurrentDataSource()方法來獲取當前數據源。具體的數據源實現需要繼承這個抽象類,並實現這兩個方法。下面是一個示例:

public class DynamicRoutingDataSource extends AbstractRoutingDataSource {

    private final Logger logger = LoggerFactory.getLogger(getClass());

    private final Map<String, DataSource> dataSources = new ConcurrentHashMap();

    private DataSource defaultDataSource;

    @Override
    protected Object determineCurrentDataSourceKey() {
        return DataSourceContextHolder.getDataSourceKey();
    }

    @Override
    public void afterPropertiesSet() {
        Map<Object, Object> targetDataSources = new HashMap();
        targetDataSources.put("default", defaultDataSource);
        for (String key : dataSources.keySet()) {
            targetDataSources.put(key, dataSources.get(key));
        }
        setTargetDataSources(targetDataSources);
        super.afterPropertiesSet();
    }

    /**
     * 添加數據源
     *
     * @param key       數據源標識
     * @param dataSource 數據源
     */
    public void addDataSource(String key, DataSource dataSource) {
        dataSources.put(key, dataSource);
        logger.info("Add data source [{}]", key);
    }

    /**
     * 設置默認數據源
     *
     * @param defaultDataSource 默認數據源
     */
    public void setDefaultDataSource(DataSource defaultDataSource) {
        this.defaultDataSource = defaultDataSource;
    }

}

在上面的示例中,我們繼承了抽象類,並實現了determineCurrentDataSourceKey()方法和afterPropertiesSet()方法。我們還可以添加數據源和設置默認數據源。這樣一來,我們就可以動態地添加和切換數據源了。

2. 使用AOP切面切換數據源

接下來,我們需要通過AOP實現數據源的切換。這裡我們使用@Around註解,實現在訪問資料庫前或者後切換數據源。

@Aspect
@Component
public class DataSourceAspect {

    private final Logger logger = LoggerFactory.getLogger(getClass());

    @Around("execution(* com.example.service.*.*(..))")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        // 獲取方法名
        String methodName = point.getSignature().getName();

        // 獲取目標類
        Class targetClass = point.getTarget().getClass();

        // 獲取數據源註解
        DataSource dataSource = targetClass.getAnnotation(DataSource.class);

        Method method = ((MethodSignature) point.getSignature()).getMethod();
        if (method.isAnnotationPresent(DataSource.class)) {
            dataSource = method.getAnnotation(DataSource.class);
            methodName = method.getName();
        }

        // 如果有數據源註解,則切換數據源
        if (dataSource != null) {
            String dataSourceKey = dataSource.value();
            DataSourceContextHolder.setDataSourceKey(dataSourceKey);
            logger.info("Switch data source to [{}] for method [{}]", dataSourceKey, methodName);
        }

        // 執行目標方法
        Object result = point.proceed();

        // 切換回默認數據源
        if (dataSource != null) {
            DataSourceContextHolder.clearDataSourceKey();
            logger.info("Restore data source to default for method [{}]", methodName);
        }

        return result;
    }

}

在上面的代碼中,我們使用around()方法來攔截類中使用了@DataSource註解的方法,然後切換數據源。注意,我們使用了DataSourceContextHolder來存放當前數據源路由。

三、示例代碼

1. 定義註解

我們需要定義一個@DataSource註解來標記使用哪個數據源:

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
    String value() default "default";
}

2. 數據源切換

我們使用DynamicRoutingDataSource來實現數據源的添加和切換:

@Configuration
public class DataSourceConfig {

    @Bean
    @ConfigurationProperties("spring.datasource.default")
    public DataSource defaultDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties("spring.datasource.test")
    public DataSource testDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties("spring.datasource.demo")
    public DataSource demoDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    public DynamicRoutingDataSource dynamicRoutingDataSource() {
        DynamicRoutingDataSource dataSource = new DynamicRoutingDataSource();
        dataSource.setDefaultDataSource(defaultDataSource());

        dataSource.addDataSource("test", testDataSource());
        dataSource.addDataSource("demo", demoDataSource());

        return dataSource;
    }

    /**
     * 設置動態數據源
     *
     * @param dataSource 動態數據源
     * @return 動態數據源
     */
    @Bean
    public DataSource dataSource(DynamicRoutingDataSource dataSource) {
        return dataSource;
    }

}

注意,我們使用@ConfigurationProperties註解來自動載入數據源配置,然後使用DynamicRoutingDataSource將配置轉換為數據源,並添加到數據源路由中。

3. 測試數據源切換

最後,我們來測試一下數據源切換是否生效。首先,我們需要使用@DataSource註解來指定使用哪個數據源:

@Mapper
public interface UserMapper {

    @Select("select id, name from user limit 1")
    @DataSource("test")
    User getTestUser();

    @Select("select id, name from user limit 1")
    @DataSource("demo")
    User getDemoUser();

}

然後,在UserService中調用UserMapper

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    public User getTestUser() {
        return userMapper.getTestUser();
    }

    public User getDemoUser() {
        return userMapper.getDemoUser();
    }

}

最後,在測試用例中驗證數據源是否正確切換:

@RunWith(SpringRunner.class)
@SpringBootTest
public class DataSourceTest {

    @Autowired
    private UserService userService;

    @Test
    public void testSwitchDataSource() {
        User testUser = userService.getTestUser();
        assertNotNull(testUser);

        User demoUser = userService.getDemoUser();
        assertNotNull(demoUser);
    }

}

如果測試用例可以正常運行,那麼就說明數據源切換成功了。

四、總結

通過本文的介紹,我們學習了如何使用Spring Boot動態數據源,使得程序可以動態地切換不同的資料庫連接。具體實現需要使用AOP切面和自定義數據源來實現。相信大家已經對動態數據源有了一定的了解,並可以根據需要自己實現動態數據源。

原創文章,作者:JXAMT,如若轉載,請註明出處:https://www.506064.com/zh-tw/n/333999.html

(0)
打賞 微信掃一掃 微信掃一掃 支付寶掃一掃 支付寶掃一掃
JXAMT的頭像JXAMT
上一篇 2025-02-05 13:05
下一篇 2025-02-05 13:05

相關推薦

發表回復

登錄後才能評論