java会自动配置一个默认数据源:HikariDataSource连接:HikariDataSource

一、简介

说到多数据源,一般用在以下两种场景:

多数据源的实现,从简单到复杂,有多种解决方案。

本文将以SpringBoot(2.5.X)+Mybatis+H2为例网络切换代码用不了,演示简单可靠的多数据源实现。

看完这篇文章,你会收获:

SpringBoot如何自动配置数据源? SpringBoot中Mybatis如何自动配置多数据源下的事务?如何使用它获得可靠的多数据源示例项目二、自动配置数据源

p>

SpringBoot的自动配置已经为我们做了几乎所有的工作,只需要引入相关的依赖就可以完成所有的工作


    com.h2database
    h2
    runtime


    org.mybatis.spring.boot
    mybatis-spring-boot-starter
    2.2.0

当H2数据库引入依赖时,DataSourceAutoConfiguration.java会自动配置一个默认数据源:HikariDataSource,先贴上源码:

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
@ConditionalOnMissingBean(type = "io.r2dbc.spi.ConnectionFactory")
// 1、加载数据源配置
@EnableConfigurationProperties(DataSourceProperties.class)
@Import({ DataSourcePoolMetadataProvidersConfiguration.class,
      DataSourceInitializationConfiguration.InitializationSpecificCredentialsDataSourceInitializationConfiguration.class,
      DataSourceInitializationConfiguration.SharedCredentialsDataSourceInitializationConfiguration.class })
public class DataSourceAutoConfiguration {
   @Configuration(proxyBeanMethods = false)
   // 内嵌数据库依赖条件,默认存在 HikariDataSource 所以不会生效,详见下文
   @Conditional(EmbeddedDatabaseCondition.class)
   @ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
   @Import(EmbeddedDataSourceConfiguration.class)
   protected static class EmbeddedDatabaseConfiguration {
   }
   @Configuration(proxyBeanMethods = false)
   @Conditional(PooledDataSourceCondition.class)
   @ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
   @Import({ DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class,
         DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.OracleUcp.class,
         DataSourceConfiguration.Generic.class, DataSourceJmxConfiguration.class })
   protected static class PooledDataSourceConfiguration {
   //2、初始化带池化的数据源:Hikari、Tomcat、Dbcp2等
   }
   // 省略其他
}

原理如下:

1、加载数据源配置

通过@EnableConfigurationProperties(DataSourceProperties.class)加载配置信息,观察DataSourceProperties的类定义:

@ConfigurationProperties(prefix = "spring.datasource")
public class DataSourceProperties implements BeanClassLoaderAware, InitializingBean

可以得到两条信息:

配置的前缀是spring.datasource;实现了InitializingBean接口,具有初始化操作。

其实默认的嵌入式数据库连接是根据用户配置初始化的:

    @Override
    public void afterPropertiesSet() throws Exception {
        if (this.embeddedDatabaseConnection == null) {
            this.embeddedDatabaseConnection = EmbeddedDatabaseConnection.get(this.classLoader);
        }
    }

通过EmbeddedDatabaseConnection.get方法遍历内置数据库枚举,找到最适合当前环境的嵌入式数据库连接。由于我们引入了H2,所以返回值也是H2数据库的枚举信息:

public static EmbeddedDatabaseConnection get(ClassLoader classLoader) {
        for (EmbeddedDatabaseConnection candidate : EmbeddedDatabaseConnection.values()) {
            if (candidate != NONE && ClassUtils.isPresent(candidate.getDriverClassName(), classLoader)) {
                return candidate;
            }
        }
        return NONE;
    }

这就是SpringBoot的约定优于配置(convention over configuration)的思想。 SpringBoot发现我们引入了H2数据库,立马准备了默认的连接信息。

2、创建数据源

默认情况下,由于SpringBoot内置了池化数据源HikariDataSource,不会加载@Import(EmbeddedDataSourceConfiguration.class),只会初始化一个HikariDataSource,因为@Conditional(EmbeddedDatabaseCondition.class)在当前不持有环境。这在源代码的注释中有解释:

/**
 * {@link Condition} to detect when an embedded {@link DataSource} type can be used.
 
 * If a pooled {@link DataSource} is available, it will always be preferred to an
 * {@code EmbeddedDatabase}.
 * 如果存在池化 DataSource,其优先级将高于 EmbeddedDatabase
 */
static class EmbeddedDatabaseCondition extends SpringBootCondition {
// 省略源码
}

所以默认数据源的初始化是通过:@Import({DataSourceConfiguration.Hikari.class,//Omit others}实现的。代码也比较简单:

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(HikariDataSource.class)
@ConditionalOnMissingBean(DataSource.class)
@ConditionalOnProperty(name = "spring.datasource.type", havingValue = "com.zaxxer.hikari.HikariDataSource",
      matchIfMissing = true)
static class Hikari {
   @Bean
   @ConfigurationProperties(prefix = "spring.datasource.hikari")
   HikariDataSource dataSource(DataSourceProperties properties) {
   //创建 HikariDataSource 实例 
      HikariDataSource dataSource = createDataSource(properties, HikariDataSource.class);
      if (StringUtils.hasText(properties.getName())) {
         dataSource.setPoolName(properties.getName());
      }
      return dataSource;
   }
}

protected static  T createDataSource(DataSourceProperties properties, Class type) {
// 在 initializeDataSourceBuilder 里面会用到默认的连接信息
return (T) properties.initializeDataSourceBuilder().type(type).build();
}

public DataSourceBuilder initializeDataSourceBuilder() {
   return DataSourceBuilder.create(getClassLoader()).type(getType()).driverClassName(determineDriverClassName())
         .url(determineUrl()).username(determineUsername()).password(determinePassword());
}

默认连接信息的使用思路是一样的:先使用用户指定的配置。如果用户不写,则使用默认值。以determineDriverClassName()为例:

public String determineDriverClassName() {
    // 如果配置了 driverClassName 则返回
		if (StringUtils.hasText(this.driverClassName)) {
			Assert.state(driverClassIsLoadable(), () -> "Cannot load driver class: " + this.driverClassName);
			return this.driverClassName;
		}
		String driverClassName = null;
    // 如果配置了 url 则根据 url推导出 driverClassName
		if (StringUtils.hasText(this.url)) {
			driverClassName = DatabaseDriver.fromJdbcUrl(this.url).getDriverClassName();
		}
    // 还没有的话就用数据源配置类初始化时获取的枚举信息填充
		if (!StringUtils.hasText(driverClassName)) {
			driverClassName = this.embeddedDatabaseConnection.getDriverClassName();
		}
		if (!StringUtils.hasText(driverClassName)) {
			throw new DataSourceBeanCreationException("Failed to determine a suitable driver class", this,
					this.embeddedDatabaseConnection);
		}
		return driverClassName;
	}

determineUrl()、determineUsername()、determinePassword()等其他都是一样的,不再赘述。

至此,默认的HikariDataSource就自动配置好了!

看看SpringBoot中Mybatis是如何自动配置的

三、自动配置Mybatis

要在Spring中使用Mybatis,至少需要一个SqlSessionFactory和一个mapper接口,所以MyBatis-Spring-Boot-Starter为我们做了这些事情:

自动发现已有的DataSource,并将DataSource传递给SqlSessionFactoryBean,创建并注册一个SqlSessionFactory实例。使用sqlSessionFactory创建并注册SqlSessionTemplate实例,自动扫描mapper,与SqlSessionTemplate链接,注册到Spring容器中供其他bean注入

p>

结合源码加深印象:

public class MybatisAutoConfiguration implements InitializingBean {
    @Bean
    @ConditionalOnMissingBean
    //1.自动发现已有的`DataSource`
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
        //2.将 DataSource 传递给 SqlSessionFactoryBean 从而创建并注册一个 SqlSessionFactory 实例
        factory.setDataSource(dataSource);
       // 省略其他...
        return factory.getObject();
    }
    @Bean
    @ConditionalOnMissingBean
    //3.利用 sqlSessionFactory 创建并注册 SqlSessionTemplate 实例
    public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
        ExecutorType executorType = this.properties.getExecutorType();
        if (executorType != null) {
            return new SqlSessionTemplate(sqlSessionFactory, executorType);
        } else {
            return new SqlSessionTemplate(sqlSessionFactory);
        }
    }
    /**
     * This will just scan the same base package as Spring Boot does. If you want more power, you can explicitly use
     * {@link org.mybatis.spring.annotation.MapperScan} but this will get typed mappers working correctly, out-of-the-box,
     * similar to using Spring Data JPA repositories.
     */
     //4.自动扫描`mapper`,将他们与`SqlSessionTemplate` 链接起来并注册到`Spring` 容器中供其他`Bean`注入
    public static class AutoConfiguredMapperScannerRegistrar implements BeanFactoryAware, ImportBeanDefinitionRegistrar {
	// 省略其他...
    }
}

一张图抵千言,其本质是层层注入:

四、从单个到多个

有了二、的三个总结的知识储备,创建多数据源的理论基础有了:两组DataSource和两组逐层注入,如图:

接下来,我们将按照自动配置单个数据源的套路来配置多个数据源,顺序如下:

首先,设计配置信息。当有单一数据源时,配置前缀为spring.datasource。为了支持多数据源,我们在后面加了一层。 yml如下:

spring:
  datasource:
    first:
      driver-class-name: org.h2.Driver
      jdbc-url: jdbc:h2:mem:db1
      username: sa
      password:
    second:
      driver-class-name: org.h2.Driver
      jdbc-url: jdbc:h2:mem:db2
      username: sa
      password:

第一个数据源的配置

这里的每个@Bean 添加@Primary 使其成为默认Bean。使用@MapperScan时,指定SqlSessionTemplate,将mapper与firstSqlSessionTemplate关联起来。

提示:

最后,为数据源创建一个DataSourceTransactionManager,用于事务管理。在多数据源场景中使用事务时,@Transactional(transactionManager = “firstTransactionManager”) 用于指定事务使用哪种事务管理。

至此网络切换代码用不了,第一个数据源就配置好了,第二个数据源也配置了这些项。因为配置的bean是同一类型的,所以需要使用@Qualifier来限定加载的bean,例如:

@Bean
// 创建 SqlSessionTemplate
public SqlSessionTemplate secondSqlSessionTemplate(@Qualifier("secondSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
    return new SqlSessionTemplate(sqlSessionFactory);
}

完整的代码可以在班级代表的GitHub上查看

五、多数据源下的事务

Spring为我们提供了一个简单易用的声明式事务,让我们可以更加专注于业务开发,但要正确使用却并不容易。本文仅关注多个数据源。请点击交易补课:如何学习Spring声明式交易?

前面的小技巧中提到过,由于开启声明式事务的时候有多个事务管理器,所以需要指定使用哪个事务管理器,比如下面的例子:

// 不显式指定参数 transactionManager 则会使用设置为 Primary 的 firstTransactionManager
// 如下代码只会回滚 firstUserMapper.insert, secondUserMapper.insert(user2);会正常插入
@Transactional(rollbackFor = Throwable.class)
public void insertTwoDBWithTX(String name) {
    User user = new User();
    user.setName(name);
    // 回滚
    firstUserMapper.insert(user);
    // 不回滚
    secondUserMapper.insert(user);
    // 主动触发回滚
    int i = 1/0;
}

事务默认使用firstTransactionManager作为事务管理器,只控制FristDataSource的事务,所以当我们手动从内部抛出异常回滚事务时,firstUserMapper.insert(user);回滚,secondUserMapper.insert(user);不要回滚。

框架代码已上传,小伙伴们可以根据自己的想法设计用例验证。

六、回顾

至此,SpringBoot+Mybatis+H2的多数据源示例已经演示完毕。这应该是最基本的多数据源配置了。事实上,它很少在网上使用,除非它是一项极其简单的一次性业务。 .

因为这种方法的缺点非常明显:代码侵入性太强!需要实现的组件集就有多少个数据源,代码量呈指数级增长。

写这个案例更多的是总结和复习SpringBoot的自动配置、带注释的声明式bean、Spring声明式事务等基础知识,为后续多数据源推进铺路。

Spring官方为我们提供了一个AbstractRoutingDataSource类,通过路由DataSource来实现多个数据源的切换。这也是目前大多数轻量级多数据源实现的底层支持。

跟随类代表,下一个demo将基于AbstractRoutingDataSource+AOP多数据源实现!

参考

mybatis-spring()

mybatis-spring-boot-autoconfigure (#)

该类代表 GitHub()

© 版权声明
THE END
喜欢就支持一下吧
点赞0
分享
评论 抢沙发

请登录后发表评论