SpringBoot多數據源以及事務處理

背景

在高併發的項目中,單數據庫已無法承載大數據量的訪問,因此需要使用多個數據庫進行對數據的讀寫分離,此外就是在微服化的今天,我們在項目中可能採用各種不同存儲,因此也需要連接不同的數據庫,居於這樣的背景,這裏簡單分享實現的思路以及實現方案。

如何實現

多數據源實現思路有兩種,一種是通過配置多個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相關的實戰,後半年文章會多一些理論。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章