SpringBoot多資料來源以及事務處理

2023-02-27 12:04:56

背景

在高並行的專案中,單資料庫已無法承載巨量資料量的存取,因此需要使用多個資料庫進行對資料的讀寫分離,此外就是在微服化的今天,我們在專案中可能採用各種不同儲存,因此也需要連線不同的資料庫,居於這樣的背景,這裡簡單分享實現的思路以及實現方案。

如何實現

多資料來源實現思路有兩種,一種是通過設定多個SqlSessionFactory實現多資料來源; image.png 另外一種是通過Spring提供的AbstractRoutingDataSource抽象了一個DynamicDataSource實現動態切換資料來源; image.png

實現方案

準備

採用Spring Boot2.7.8框架,資料庫Mysql,ORM框架採用Mybatis,整個Maven依賴如下:

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <spring-boot.version>2.7.8</spring-boot.version>
        <mysql-connector-java.version>5.1.46</mysql-connector-java.version>
        <mybatis-spring-boot-starter.version>2.0.0</mybatis-spring-boot-starter.version>
        <mybatis.version>3.5.1</mybatis.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>${mysql-connector-java.version}</version>
            </dependency>
            <dependency>
                <groupId>org.mybatis</groupId>
                <artifactId>mybatis</artifactId>
                <version>${mybatis.version}</version>
            </dependency>
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>${mybatis-spring-boot-starter.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

指定資料來源操作指定目錄XML檔案

該種方式需要操作的資料庫的Mapper層和Dao層分別建立一個資料夾,分包放置,整體專案結構如下圖: image.png

Maven依賴如下:
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
        <dependency>
            <groupId>com.zaxxer</groupId>
            <artifactId>HikariCP</artifactId>
            <version>4.0.3</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
Yaml檔案
spring:
  datasource:
    user:
      jdbc-url: jdbc:mysql://127.0.0.1:3306/study_user?useSSL=false&useUnicode=true&characterEncoding=UTF-8
      username: root
      password: 123456
      driver-class-namecom.mysql.jdbc.Driver
      typecom.zaxxer.hikari.HikariDataSource
      #hikari連線池設定
      hikari:
        #pool name
        pool-nameuser
        #最小空閒連線數
        minimum-idle: 5
        #最大連線池
        maximum-pool-size: 20
        #連結超時時間  3秒
        connection-timeout: 3000
        # 連線測試query
        connection-test-querySELECT 1
    soul:
      jdbc-urljdbc:mysql://127.0.0.1:3306/soul?useSSL
=false&useUnicode=true&characterEncoding=UTF-8
      username: root
      password: 123456
      driver-class-namecom.mysql.jdbc.Driver
      typecom.zaxxer.hikari.HikariDataSource
      #hikari連線池設定
      hikari:
        #pool name
        pool-namesoul
        #最小空閒連線數
        minimum-idle: 5
        #最大連線池
        maximum-pool-size: 20
        #連結超時時間  3秒
        connection-timeout: 3000
        # 連線測試query
        connection-test-querySELECT 1
不同庫的Mapper指定不同的SqlSessionFactory

針對不同的庫分別放置對用不同的SqlSessionFactory

@Configuration
@MapperScan(basePackages = "org.datasource.demo1.usermapper",
        sqlSessionFactoryRef = "userSqlSessionFactory")
public class UserDataSourceConfiguration {

    public static final String MAPPER_LOCATION = "classpath:usermapper/*.xml";

    @Primary
    @Bean("userDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.user")
    public DataSource userDataSource() {
        return DataSourceBuilder.create().build();
    }


    @Bean(name = "userTransactionManager")
    @Primary
    public PlatformTransactionManager userTransactionManager(@Qualifier("userDataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }


    @Primary
    @Bean(name = "userSqlSessionFactory")
    public SqlSessionFactory userSqlSessionFactory(@Qualifier("userDataSource") DataSource dataSource) throws Exception {
        final SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean();
        sessionFactoryBean.setDataSource(dataSource);
        sessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(UserDataSourceConfiguration.MAPPER_LOCATION));
        return sessionFactoryBean.getObject();
    }

}
@Configuration
@MapperScan(basePackages = "org.datasource.demo1.soulmapper",
        sqlSessionFactoryRef = "soulSqlSessionFactory")
public class SoulDataSourceConfiguration {

    public static final String MAPPER_LOCATION = "classpath:soulmapper/*.xml";


    @Bean("soulDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.soul")
    public DataSource soulDataSource() {
        return DataSourceBuilder.create().build();
    }


    @Bean(name = "soulTransactionManager")
    public PlatformTransactionManager soulTransactionManager(@Qualifier("soulDataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }


    @Bean(name = "soulSqlSessionFactory")
    public SqlSessionFactory soulSqlSessionFactory(@Qualifier("soulDataSource") DataSource dataSource) throws Exception {
        final SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean();
        sessionFactoryBean.setDataSource(dataSource);
        sessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(SoulDataSourceConfiguration.MAPPER_LOCATION));
        return sessionFactoryBean.getObject();
    }

}
使用
@Service
public class AppAuthService {

    @Autowired
    private AppAuthMapper appAuthMapper;

    @Transactional(rollbackFor = Exception.class)
    public int getCount() 
{
        int a = appAuthMapper.listCount();
        int b = 1 / 0;
        return a;
    }

}

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

    @Autowired
    private AppAuthService appAuthService;

    @Autowired
    private SysUserService sysUserService;

    @Test
    public void test_dataSource1(){
        int b=sysUserService.getCount();
        int a=appAuthService.getCount();
    }
}
總結

此種方式使用起來分層明確,不存在任何冗餘程式碼,不足地方就是每個庫都需要對應一個設定類,該設定類中實現方式都基本類似,該種解決方案每個設定類中都存在事務管理器,因此不需要單獨再去額外的關注。

AOP+自定義註解

關於採用Spring AOP方式實現原理就是把多個資料來源儲存在一個 Map中,當需要使用某個資料來源時,從 Map中獲取此資料來源進行處理。 image.png

AbstractRoutingDataSource

在Spring中提供了AbstractRoutingDataSource來實現此功能,繼承AbstractRoutingDataSource類並覆寫其determineCurrentLookupKey()方法就可以完成資料來源切換,該方法只需要返回資料來源key即可,也就是存放資料來源的Map的key,接下來我們來看一下AbstractRoutingDataSource整體的繼承結構,看他是如何做到的。 image.png 在整體的繼承結構上我們會發現AbstractRoutingDataSource最終是繼承於DataSource,因此當我們繼承AbstractRoutingDataSource是我們自身也是一個資料來源,對於資料來源必然有連線資料庫的動作,如下程式碼:

public Connection getConnection() throws SQLException {
  return this.determineTargetDataSource().getConnection();
}

public Connection getConnection(String username, String password) throws SQLException {
  return this.determineTargetDataSource().getConnection(username, password);
}

只是AbstractRoutingDataSource的getConnection()方法裡實際是呼叫determineTargetDataSource()返回的資料來源的getConnection()方法。

protected DataSource determineTargetDataSource() {
  Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
  Object lookupKey = this.determineCurrentLookupKey();
  DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);
  if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
    dataSource = this.resolvedDefaultDataSource;
  }

  if (dataSource == null) {
    throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
  } else {
    return dataSource;
  }
}

該方法通過determineCurrentLookupKey()方法獲取一個key,通過key從resolvedDataSources中獲取資料來源DataSource物件。determineCurrentLookupKey()是個抽象方法,需要繼承AbstractRoutingDataSource的類實現;而resolvedDataSources是一個Map<Object, DataSource>,裡面應該儲存當前所有可切換的資料來源,接下來我們來聊聊實現,我們首先來看下目錄,與分包的不同的是將所有的Mapper檔案都放到一起,其他Maven依賴以及組態檔都保持一致。 image.png

DataSourceType

該列舉用來存放資料來源的名稱,

public enum DataSourceType {

    USERDATASOURCE("userDataSource"),

    SOULDATASOURCE("soulDataSource");

    private String name;


    DataSourceType(String name) {
        this.name=name;
    }


    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
DynamicDataSourceConfiguration

通過讀取組態檔中的資料來源設定資訊,建立資料連線,將多個資料來源放入Map中,注入到容器中:

@Configuration
@MapperScan(basePackages = "org.datasource.demo2.mapper")
public class DynamicDataSourceConfiguration {

    @Primary
    @Bean(name = "userDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.user")
    public DataSource userDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean(name = "soulDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.soul")
    public DataSource soulDataSource() {
        return DataSourceBuilder.create().build();
    }


    @Bean(name = "dynamicDataSource")
    public DynamicDataSource DataSource(@Qualifier("userDataSource") DataSource userDataSource,
                                        @Qualifier("soulDataSource") DataSource soulDataSource) 
{
        //targetDataSource 集合是我們資料庫和名字之間的對映
        Map<Object, Object> targetDataSource = new HashMap<>();
        targetDataSource.put(DataSourceType.USERDATASOURCE.getName(), userDataSource);
        targetDataSource.put(DataSourceType.SOULDATASOURCE.getName(), soulDataSource);
        DynamicDataSource dataSource = new DynamicDataSource();
        dataSource.setTargetDataSources(targetDataSource);
        //設定預設物件
        dataSource.setDefaultTargetDataSource(userDataSource);
        return dataSource;
    }


    @Bean(name = "sqlSessionFactory")
    public SqlSessionFactory sqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dynamicDataSource)
            throws Exception 
{
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setTransactionFactory(new MultiDataSourceTransactionFactory());
        bean.setDataSource(dynamicDataSource);
        //設定我們的xml檔案路徑
        bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(
                        "classpath*:mapper/*.xml"));
        return bean.getObject();
    }
}
DataSourceContext

DataSourceContext使用ThreadLocal存放當前執行緒使用的資料來源型別資訊;

public class DataSourceContext {

    private final static ThreadLocal<String> LOCAL_DATASOURCE =
            new ThreadLocal<>();

    public static void set(String name) {
        LOCAL_DATASOURCE.set(name);
    }

    public static String get() {
        return LOCAL_DATASOURCE.get();
    }

    public static void remove() {
        LOCAL_DATASOURCE.remove();
    }
}
DynamicDataSource

DynamicDataSource繼承AbstractRoutingDataSource,重寫determineCurrentLookupKey()方法,可以選擇對應Key;

public class DynamicDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContext.get();
    }

}
CurrentDataSource

定義資料來源的註解;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface CurrentDataSource {
    DataSourceType value() default DataSourceType.USERDATASOURCE;
}
DataSourceAspect

定義切面切點,用來切換資料來源,

@Aspect
@Order(-1
@Component
public class DataSourceAspect {

    @Pointcut("@annotation(org.datasource.demo2.constant.CurrentDataSource)")
    public void dsPointCut() {

    }

    @Around("dsPointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        MethodSignature signature = (MethodSignature) point.getSignature();

        Method method = signature.getMethod();

        CurrentDataSource dataSource = method.getAnnotation(CurrentDataSource.class);

        if (Objects.nonNull(dataSource)) {
            System.out.println("切換資料來源為" + dataSource.value().getName());
            DataSourceContext.set(dataSource.value().getName());
        }

        try {
            return point.proceed();
        } finally {
            // 銷燬資料來源 在執行方法之後
            System.out.println("銷燬資料來源" + dataSource.value().getName());
            DataSourceContext.remove();
        }
    }

}
多資料來源切換以後事務問題

Spring使用事務的方式有兩種,一種是宣告式事務,一種是程式設計式事務,我們討論的都是關於宣告式事務,這種方式很方便,也是大家常用的,這裡為什麼討論這個問題,當我們想將不同庫的表放在同一個事務使用的時候,這個是時候我們會報錯,如下圖: image.png 這部分也就是其他技術貼沒講解的部分,因此這裡我們來補充一下這個話題,背過八股們的小夥伴都知道Spring事務是居於AOP實現,從這個角度很容易會理解到這個問題,當我們將兩個Service方法放在同一個Transactional下的時候,這個代理物件就是當前類,因此導致資料來源物件也是當前類下的DataSource,導致就出現表不存在問題,當Transactional分別放在不同Service的時候沒有這種情況。

    @Transactional(rollbackFor = Exception.class)
    public void update()
{
        sysUserMapper.updateSysUser("111");
        appAuthService.update("111");
    }

有沒有辦法解決這個問題呢,當然是有的,這裡我就不一步一步去探討原始碼問題,我就直接直搗黃龍,把問題本質說一下,在Spring事務管理中有一個核心類DataSourceTransactionManager,該類是Spring事務核心的預設實現,AbstractPlatformTransactionManager是整體的Spring事務實現模板類,整體的繼承結構如下圖, image.png 在方案一中,我們針對每個DataSourece都建立對應的DataSourceTransactionManager實現,也可以看出DataSourceTransactionManager就是管理我們整體的事務的,當我們設定了事物管理器以及攔截Service中的方法後,每次執行Service中方法前會開啟一個事務,並且同時會快取DataSource、SqlSessionFactory、Connection,因為DataSource、Conneciton都是從快取中拿的,因此我們怎麼切換資料來源也沒用,因此就出現表不存在的報錯,具體原始碼可參考下面截圖部分: image.png image.png 看到這裡我們大致明白了為什麼會報錯,那麼我們該如何做才能實現這種情況呢?其實我們要做的事就是動態的根據DataSourceType獲取不同的Connection,不從快取中獲取Connection。

解決方案

我們來自定義一個MultiDataSourceTransaction實現Mybatis的事務介面,使用Map儲存Connection相關連線,所有事務都採用手動提交,之後將MultiDataSourceTransaction交給SpringManagedTransactionFactory處理。

public class MultiDataSourceTransaction implements Transaction {

    private final DataSource dataSource;

    private ConcurrentMap<String, Connection> concurrentMap;

    private boolean autoCommit;


    public MultiDataSourceTransaction(DataSource dataSource) {
        this.dataSource = dataSource;
        concurrentMap = new ConcurrentHashMap<>();
    }


    @Override
    public Connection getConnection() throws SQLException {
        String databaseIdentification = DataSourceContext.get();
        if (StringUtils.isEmpty(databaseIdentification)) {
            databaseIdentification = DataSourceType.USERDATASOURCE.getName();
        }
        //獲取資料來源
        if (!this.concurrentMap.containsKey(databaseIdentification)) {
            try {
                Connection conn = this.dataSource.getConnection();
                autoCommit=false;
                conn.setAutoCommit(false);
                this.concurrentMap.put(databaseIdentification, conn);
            } catch (SQLException ex) {
                throw new CannotGetJdbcConnectionException("Could bot get JDBC otherConnection", ex);
            }
        }
        return this.concurrentMap.get(databaseIdentification);
    }


    @Override
    public void commit() throws SQLException {
        for (Connection connection : concurrentMap.values()) {
            if (!autoCommit) {
                connection.commit();
            }
        }
    }

    @Override
    public void rollback() throws SQLException {
        for (Connection connection : concurrentMap.values()) {
            connection.rollback();
        }
    }

    @Override
    public void close() throws SQLException {
        for (Connection connection : concurrentMap.values()) {
            DataSourceUtils.releaseConnection(connection, this.dataSource);
        }
    }

    @Override
    public Integer getTimeout() throws SQLException {
        return null;
    }
}

public class MultiDataSourceTransactionFactory extends SpringManagedTransactionFactory {
    @Override
    public Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit) {
        return new MultiDataSourceTransaction(dataSource);
    }
}
為什麼可以這麼做

在Mybatis自動裝配式會將組態檔裝配為Configuration物件,也就是在方案一種SqlSessionFactory設定的過程,其中SqlSessionFactoryBean類實現了InitializingBean介面,初始化後執行afterPropertiesSet()方法,在afterPropertiesSet()方法中會執行 BuildSqlSessionFactory() 方法生成一個SqlSessionFactory物件。在BuildSqlSessionFactory中,會建立SpringManagedTransactionFactory物件,該物件就是MyBatis跟 Spring的橋樑。

image.png 在MapperScan自動掃描Mapper過程中,會通過ClassPathMapperScanner掃描器找到Mapper介面,封裝成各自的BeanDefinition,然後迴圈遍歷對Mapper的BeanDefinition修改beanClass為MapperFactoryBean。 image.png 由於MapperFactoryBean實現了FactoryBean,在Bean生命週期管理時會呼叫getObject方法,通過JDK動態代理生成代理物件MapperProxy,Mapper介面請求的時候,執行MapperProxy代理類的invoke方法,執行的過程中通過SqlSessionFactory建立的SqlSession去呼叫Executor執行器,進行資料庫操作。下圖是SqlSession建立的整個過程: image.png openSession方法是將Spring事務管理關聯起來的核心程式碼,首先這裡將通過 getTransactionFactoryFromEnvironment()方法獲取TransactionFactory。這個操作會得到初始化時候注入的 SpringManagedTransactionFactory物件。然後將執行TransactionFactory#newTransaction() 方法,初始化 MyBatis的Transaction。 image.png這裡通過Configuration.newExecutor()建立一個Executor,Configuration指定在Executor預設為Simple,因此這裡會建立一個SimpleExecutor,並初始化Transaction屬性。接下來我們來看下SimpleExecutor執行執行update方法時候執行prepareStatement方法,在prepareStatement方法中執行了getConnection方法, image.png image.png 這裡我們可以看到Connection獲取過程,是通過Transaction獲取的getConnection(),也就是通過之前注入的Transaction來獲取Connection,這個Transaction就是SpringManagedTransaction,整體的時序圖如下: image.png image.png 在整個呼叫鏈過程中,我們看到在DataSourceUtils有我們熟悉的TransactionSynchronizationManager,在上面Spring事務的時候我們也提到這個類,在開始Spring事務以後就會把Connetion繫結到當前執行緒,在DataSourceUtils獲取到的Connection物件就是Srping開啟事務時候建立的物件,這樣就保證了Spring Transaction中的Connection跟MyBatis中執行SQL語句用的Connection為同一個 Connection,也就可以通過Spring事務管理機制進行事務管理了。 image.png 明白了整個流程,我們要做的事也就很簡單,也就是每次切換DataSoure的同時獲取最新的Connection,然後用一個Map物件來記錄整個過程中的Connection,出現回滾這個Map物件裡面Connection物件都回滾就可以了,然後將我們自定義的Transaction,委託給Spring在進行管理。

總結

採用AOP的方式是切換資料來源已經非常好了,唯一不太好的地方就在於依然要手動去建立DataSource,每次增加都需要增加一個Bean,那有沒有辦法解決呢?當然是有的,讓我們來更上一層樓,解放雙手。

更上一層樓

ImportBeanDefinitionRegistrar

ImportBeanDefinitionRegistrar介面是Spring提供一個擴充套件點,主要用來註冊BeanDefinition,常見的第三方框架在整合Spring的時候,都會通過該介面,實現掃描指定的類,然後註冊到Spring容器中。比如 Mybatis中的Mapper介面,SpringCloud中的Feignlient介面,都是通過該介面實現的自定義註冊邏輯。 我們要做的事情就是通過ImportBeanDefinitionRegistrar幫助我們動態的將DataSource掃描的到容器中去,不在採用增加Bean的方式,整體程式碼如下:

public class DynamicDataSourceBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrarEnvironmentAware {


    /**
     * 預設dataSource
     */

    private DataSource defaultDataSource;

    /**
     * 資料來源map
     */

    private Map<String, DataSource> dataSourcesMap = new HashMap<>();


    @Override
    public void setEnvironment(Environment environment) {
        initConfig(environment);
    }

    private void initConfig(Environment env) {
        //讀取組態檔獲取更多資料來源
        String dsNames = env.getProperty("spring.datasource.names");
        for (String dsName : dsNames.split(",")) {
            HikariConfig hikariConfig = new HikariConfig();
            hikariConfig.setPoolName(dsName);
            hikariConfig.setDriverClassName(env.getProperty("spring.datasource." + dsName.trim() + ".driver-class-name"));
            hikariConfig.setJdbcUrl(env.getProperty("spring.datasource." + dsName.trim() + ".jdbc-url"));
            hikariConfig.setUsername(env.getProperty("spring.datasource." + dsName.trim() + ".username"));
            hikariConfig.setPassword(env.getProperty("spring.datasource." + dsName.trim() + ".password"));
            hikariConfig.setConnectionTimeout(Long.parseLong(Objects.requireNonNull(env.getProperty("spring.datasource." + dsName.trim() + ".hikari.connection-timeout"))));
            hikariConfig.setMinimumIdle(Integer.parseInt(Objects.requireNonNull(env.getProperty("spring.datasource." + dsName.trim() + ".hikari.minimum-idle"))));
            hikariConfig.setMaximumPoolSize(Integer.parseInt(Objects.requireNonNull(env.getProperty("spring.datasource." + dsName.trim() + ".hikari.maximum-pool-size"))));
            hikariConfig.setConnectionInitSql("SELECT 1");
            HikariDataSource dataSource = new HikariDataSource(hikariConfig);
            if (dataSourcesMap.size() == 0) {
                defaultDataSource = dataSource;
            }
            dataSourcesMap.put(dsName, dataSource);
        }
    }

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {

        Map<Object, Object> targetDataSources = new HashMap<Object, Object>();
        //新增其他資料來源
        targetDataSources.putAll(dataSourcesMap);
        //建立DynamicDataSource
        GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
        beanDefinition.setBeanClass(DynamicDataSource.class);
        beanDefinition.setSynthetic(true);
        MutablePropertyValues mpv = beanDefinition.getPropertyValues();
        //defaultTargetDataSource 和 targetDataSources屬性是 AbstractRoutingDataSource的兩個屬性Map
        mpv.addPropertyValue("defaultTargetDataSource", defaultDataSource);
        mpv.addPropertyValue("targetDataSources", targetDataSources);
        //註冊
        registry.registerBeanDefinition("dataSource", beanDefinition);
    }

}
@Import

@Import模式是向容器匯入Bean是一種非常重要的方式,在註解驅動的Spring專案中,@Enablexxx的設計模式中有大量的使用,我們通過ImportBeanDefinitionRegistrar完成Bean的掃描,通過@Import匯入到容器中,然後將EnableDynamicDataSource放入SpringBoot的啟動項之上,到這裡有沒有感覺到茅塞頓開的感覺。

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Import({DynamicDataSourceBeanDefinitionRegistrar.class})
public @interface EnableDynamicDataSource 
{
}
@SpringBootApplication
@EnableAspectJAutoProxy
@EnableDynamicDataSource
public class DataSourceApplication {

    public static void main(String[] args) {
        SpringApplication.run(DataSourceApplication.classargs);
    }
}
DynamicDataSourceConfig

該類負責將Mapper掃描以及SpringFactory定義;

@Configuration
@MapperScan(basePackages = "org.datasource.demo3.mapper")
public class DynamicDataSourceConfig {


    @Autowired
    private DataSource dynamicDataSource;

    @Bean(name = "sqlSessionFactory")
    public SqlSessionFactory sqlSessionFactory()
            throws Exception 
{
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setTransactionFactory(new MultiDataSourceTransactionFactory());
        bean.setDataSource(dynamicDataSource);
        //設定我們的xml檔案路徑
        bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(
                "classpath*:mapper/*.xml"));
        return bean.getObject();
    }
}
yaml

關於yaml部分我們增加了names定義,方便識別出來設定了幾個DataSource,剩下的部分與AOP保持一致。

spring:
  datasource:
    names: user,soul
    user:
      jdbc-url: jdbc:mysql://127.0.0.1:3306/study_user?useSSL=false&useUnicode=true&characterEncoding=UTF-8
      username: root
      password: 123456
      driver-class-namecom.mysql.jdbc.Driver
      typecom.zaxxer.hikari.HikariDataSource
      #hikari連線池設定
      hikari:
        #最小空閒連線數
        minimum-idle: 5
        #最大連線池
        maximum-pool-size: 20
        #連結超時時間  3秒
        connection-timeout: 3000
    soul:
      jdbc-urljdbc:mysql://127.0.0.1:3306/soul?useSSL
=false&useUnicode=true&characterEncoding=UTF-8
      username: root
      password: 123456
      driver-class-namecom.mysql.jdbc.Driver
      typecom.zaxxer.hikari.HikariDataSource
      #hikari連線池設定
      hikari:
        #最小空閒連線數
        minimum-idle: 5
        #最大連線池
        maximum-pool-size: 20
        #連結超時時間  3秒
        connection-timeout: 3000

結束

歡迎大家點點關注,點點贊! 今年前半年文章會偏Spring、SpringCloud相關的實戰,後半年文章會多一些理論。