6. SpringBoot数据访问

6.1 数据源自动配置源码剖析

1. 数据源配置方式

  1. 选择数据库驱动的库文件
    在maven中配置数据库驱动
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
  1. 配置数据库连接
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql:///springboot_h?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=root
# spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
  1. 配置spring-boot-starter-jdbc
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
  1. 编写测试类
@RunWith(SpringRunner.class)
@SpringBootTest(classes = SpringBootDemoApplication.class)
class SpringBootDemoApplicationTests {
@Autowired
DataSource dataSource;
@Test
public void contextLoads() throws SQLException {
Connection connection = dataSource.getConnection();
}
}

2. 连接池配置方式

  1. 选择数据库连接池的库文件

SpringBoot提供了三种数据库连接池:

  • HikariCP

  • Commons DBCP2

  • Tomcat JDBC Connection Pool

其中spring boot2.x版本默认使用HikariCP,maven中配置如下:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

如果不使用HikariCP,而改用Commons DBCP2,则配置如下:

<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-dbcp2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
<exclusions>
<exclusion>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</exclusion>
</exclusions>
</dependency>

如果不使用HikariCP,而改用Tomcat JDBC Connection Pool,则配置如下:

<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
<exclusions>
<exclusion>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</exclusion>
</exclusions>
</dependency>

思考:为什么说springboot默认使用的连接池类型是HikariCP,在哪指定的?

3. 数据源自动配置

spring.factories中找到数据源的配置类:

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
@EnableConfigurationProperties(DataSourceProperties.class)
@Import({ DataSourcePoolMetadataProvidersConfiguration.class,
DataSourceInitializationConfiguration.class })
public class DataSourceAutoConfiguration {

@Configuration(proxyBeanMethods = false)
@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.Generic.class,
DataSourceJmxConfiguration.class })
protected static class PooledDataSourceConfiguration {
}
//...
}

@Conditional(PooledDataSourceCondition.class) 根据判断条件,实例化这个类,指定了配置文件中,必须有type这个属性。

另外springboot 默认支持 type 类型设置的数据源;

abstract class DataSourceConfiguration {

@SuppressWarnings("unchecked")
// 使用DataSourceBuilder 建造数据源,利用反射创建type数据源,然后绑定相关属性
protected static <T> T createDataSource(DataSourceProperties properties, Class<? extends DataSource> type) {
return (T) properties.initializeDataSourceBuilder().type(type).build();
}

/**
* Tomcat Pool DataSource configuration.
* //2.0 之后默认不是使用 tomcat 连接池,或者使用tomcat 容器
* //如果导入tomcat jdbc连接池 则使用此连接池,在使用tomcat容器时候 或者导入此包时候
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(org.apache.tomcat.jdbc.pool.DataSource.class)
@ConditionalOnMissingBean(DataSource.class)
//并且配置的配置是 org.apache.tomcat.jdbc.pool.DataSource 会采用tomcat 连接池
//name用来从application.properties中读取某个属性值
//缺少该property时是否可以加载。如果为true,没有该property也会正常加载;反之报错
// 不管你配不配置 都以 tomcat 连接池作为连接池
@ConditionalOnProperty(name = "spring.datasource.type", havingValue = "org.apache.tomcat.jdbc.pool.DataSource",
matchIfMissing = true)
static class Tomcat {

@Bean
@ConfigurationProperties(prefix = "spring.datasource.tomcat")
org.apache.tomcat.jdbc.pool.DataSource dataSource(DataSourceProperties properties) {
org.apache.tomcat.jdbc.pool.DataSource dataSource = createDataSource(properties,
org.apache.tomcat.jdbc.pool.DataSource.class);
DatabaseDriver databaseDriver = DatabaseDriver.fromJdbcUrl(properties.determineUrl());
String validationQuery = databaseDriver.getValidationQuery();
if (validationQuery != null) {
dataSource.setTestOnBorrow(true);
dataSource.setValidationQuery(validationQuery);
}
return dataSource;
}

}

/**
* Hikari DataSource configuration.
* //2.0 之后默认默认使用 hikari 连接池
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(HikariDataSource.class)
//注解判断是否执行初始化代码,即如果用户已经创建了bean,则相关的初始化代码不再执行
@ConditionalOnMissingBean(DataSource.class)
//拿配置文件中的type 如果为空返回false
//type 不为空则去havingValue 对比 ,相同则ture 否则为false
// 不管上面文件中是否配置,默认都进行加载 ,matchIfMissing的默值为false
@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 dataSource = createDataSource(properties, HikariDataSource.class);
if (StringUtils.hasText(properties.getName())) {
dataSource.setPoolName(properties.getName());
}
return dataSource;
}

}

/**
* DBCP DataSource configuration.
* //Dbcp2 连接池
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(org.apache.commons.dbcp2.BasicDataSource.class)
@ConditionalOnMissingBean(DataSource.class)
@ConditionalOnProperty(name = "spring.datasource.type", havingValue = "org.apache.commons.dbcp2.BasicDataSource",
matchIfMissing = true)
static class Dbcp2 {

@Bean
@ConfigurationProperties(prefix = "spring.datasource.dbcp2")
org.apache.commons.dbcp2.BasicDataSource dataSource(DataSourceProperties properties) {
return createDataSource(properties, org.apache.commons.dbcp2.BasicDataSource.class);
}

}

/**
* Generic DataSource configuration.
* //自定义连接池 接口 spring.datasource.type 配置
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(DataSource.class)
@ConditionalOnProperty(name = "spring.datasource.type")
static class Generic {

@Bean
DataSource dataSource(DataSourceProperties properties) {
//创建数据源 initializeDataSourceBuilder DataSourceBuilder
return properties.initializeDataSourceBuilder().build();
}

}

}

如果在类路径没有找到 jar包 则会跑出异常:

配置文件中没有指定数据源时候 会根据注解判断然后选择相应的实例化数据源对象!则 type 为空。

/**
* Hikari DataSource configuration.
* //2.0 之后默认默认使用 hikari 连接池
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(HikariDataSource.class)
//注解判断是否执行初始化代码,即如果用户已经创建了bean,则相关的初始化代码不再执行
@ConditionalOnMissingBean(DataSource.class)
//拿配置文件中的type 如果为空返回false
//type 不为空则去havingValue 对比 ,相同则ture 否则为false
// 不管上面文件中是否配置,默认都进行加载 ,matchIfMissing的默值为false
@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 dataSource = createDataSource(properties, HikariDataSource.class);
if (StringUtils.hasText(properties.getName())) {
dataSource.setPoolName(properties.getName());
}
return dataSource;
}

}

createDataSource 方法

@SuppressWarnings("unchecked")
// 使用DataSourceBuilder 建造数据源,利用反射创建type数据源,然后绑定相关属性
protected static <T> T createDataSource(DataSourceProperties properties, Class<? extends DataSource> type) {
return (T) properties.initializeDataSourceBuilder().type(type).build();
}

DataSourceBuilder 类

设置type

public <D extends DataSource> DataSourceBuilder<D> type(Class<D> type) {
this.type = type;
return this;
}

根据设置type的选择类型

private Class<? extends DataSource> getType() {
//如果没有配置type 则为空 默认选择 findType
Class<? extends DataSource> type = this.type != null ? this.type :
findType(this.classLoader);
if (type != null) {
return type;
} else {
throw new IllegalStateException("No supported DataSource type found");
}
}
public static Class<? extends DataSource> findType(ClassLoader classLoader) {
String[] var1 = DATA_SOURCE_TYPE_NAMES;
int var2 = var1.length;
int var3 = 0;
while(var3 < var2) {
String name = var1[var3];
try {
return ClassUtils.forName(name, classLoader);
} catch (Exception var6) {
++var3;
}
}
return null;
}
private static final String[] DATA_SOURCE_TYPE_NAMES = new String[]
{"com.zaxxer.hikari.HikariDataSource",
"org.apache.tomcat.jdbc.pool.DataSource",
"org.apache.commons.dbcp2.BasicDataSource"};

取出来的第一个值就是com.zaxxer.hikari.HikariDataSource,那么证实在没有指定Type的情况下,默认类型为com.zaxxer.hikari.HikariDataSource

4. Druid连接池的配置

<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>

在application.yml中引入druid的相关配置

spring:
datasource:
username: root
password: root
url: jdbc:mysql:///springboot_h?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC
driver-class-name: com.mysql.cj.jdbc.Driver
initialization-mode: always
# 使用druid数据源
type: com.alibaba.druid.pool.DruidDataSource
# 数据源其他配置
initialSize: 5
minIdle: 5
maxActive: 20
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
filters: stat,wall,log4j
maxPoolPreparedStatementPerConnectionSize: 20
useGlobalDataSourceStat: true
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500

但是测试Debug查看DataSource的值,会发现有些属性是没有生效的

这是因为:如果单纯在yml文件中编写如上的配置,SpringBoot肯定是读取不到druid的相关配置的。因为它并不像我们原生的jdbc,系统默认就使用DataSourceProperties与其属性进行了绑定。所以我们应该编写一个类与其属性进行绑定

编写整合druid的配置类DruidConfig

public class DruidConfig {
@ConfigurationProperties(prefix = "spring.datasource")
@Bean
public DataSource druid(){
return new DruidDataSource();
}
}

测试的时候,突然发现控制台报错了。经过查找发现是yml文件里的
filters: stat,wall,log4j
因为我们springBoot2.0以后使用的日志框架已经不再使用log4j了。此时应该引入相应的适配器。 我们可以在pom.xml文件上加入:

<!--引入适配器-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</dependency>

6.2 SpringBoot整合Mybatis

MyBatis 是一款优秀的持久层框架,Spring Boot官方虽然没有对MyBatis进行整合,但是MyBatis团队自行适配了对应的启动器,进一步简化了使用MyBatis进行数据的操作 因为Spring Boot框架开发的便利性,所以实现Spring Boot与数据访问层框架(例如MyBatis)的整合非常简单,主要是引入对应的依赖启动器,并进行数据库相关参数设置即可。

1. 新建springboot项目,并导入mybatis的pom配置

<!-- 配置数据库驱动和mybatis dependency -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>

2. application.yml配置

spring:
datasource:
username: root
password: root
url: jdbc:mysql:///springboot_h?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC
driver-class-name: com.mysql.jdbc.Driver
# 使用druid数据源
type: com.alibaba.druid.pool.DruidDataSource

3. 代码实现

Bean对象

@Data
public class Account {
private String name;
private Integer money;
private String cardNo;
}

mapper接口

@Mapper
public interface UserDao {
@Select("SELECT * FROM USER")
List<User> getUser();
}

service类

@Service
public class UserService {
Logger logger = LoggerFactory.getLogger(UserService.class);
@Autowired
private UserDao userDao;
public List<User> getUser(){
List<User> userList = userDao.getUser();
logger.info("查询出来的用户信息,{}",userList.toString());
return userList;
}
}

测试类

@RunWith(SpringRunner.class)
@SpringBootTest
public class DemoApplicationTests {
@Test
public void contextLoads() {
}
}


public class UserServiceTest extends DemoApplicationTests {
@Autowired
private UserService userService;
@Test
public void getUser() {
userService.getUser();
}
}

6.3 Mybatis自动配置源码分析

  1. springboot项目最核心的就是自动加载配置,该功能则依赖的是一个注解@SpringBootApplication中的@EnableAutoConfiguration
  2. EnableAutoConfiguration主要是通过AutoConfigurationImportSelector类来加载

以mybatis为例,*selector通过反射加载spring.factories中指定的java类,也就是加载MybatisAutoConfiguration类(该类有Configuration注解,属于配置类)

/**
* {@link EnableAutoConfiguration Auto-Configuration} for Mybatis. Contributes a {@link SqlSessionFactory} and a
* {@link SqlSessionTemplate}.
*
* If {@link org.mybatis.spring.annotation.MapperScan} is used, or a configuration file is specified as a property,
* those will be considered, otherwise this auto-configuration will attempt to register mappers based on the interface
* definitions in or under the root auto-configuration package.
*
* @author Eddú Meléndez
* @author Josh Long
* @author Kazuki Shimizu
* @author Eduardo Macarrón
*/
@org.springframework.context.annotation.Configuration
@ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class })
@ConditionalOnSingleCandidate(DataSource.class)
@EnableConfigurationProperties(MybatisProperties.class)
@AutoConfigureAfter({ DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class })
public class MybatisAutoConfiguration implements InitializingBean {

private static final Logger logger = LoggerFactory.getLogger(MybatisAutoConfiguration.class);
// //与mybatis配置文件对应
private final MybatisProperties properties;

private final Interceptor[] interceptors;

private final TypeHandler[] typeHandlers;

private final LanguageDriver[] languageDrivers;

private final ResourceLoader resourceLoader;

private final DatabaseIdProvider databaseIdProvider;

private final List<ConfigurationCustomizer> configurationCustomizers;

public MybatisAutoConfiguration(MybatisProperties properties, ObjectProvider<Interceptor[]> interceptorsProvider,
ObjectProvider<TypeHandler[]> typeHandlersProvider, ObjectProvider<LanguageDriver[]> languageDriversProvider,
ResourceLoader resourceLoader, ObjectProvider<DatabaseIdProvider> databaseIdProvider,
ObjectProvider<List<ConfigurationCustomizer>> configurationCustomizersProvider) {
this.properties = properties;
this.interceptors = interceptorsProvider.getIfAvailable();
this.typeHandlers = typeHandlersProvider.getIfAvailable();
this.languageDrivers = languageDriversProvider.getIfAvailable();
this.resourceLoader = resourceLoader;
this.databaseIdProvider = databaseIdProvider.getIfAvailable();
this.configurationCustomizers = configurationCustomizersProvider.getIfAvailable();
}

@Override
public void afterPropertiesSet() {
checkConfigFileExists();
}

private void checkConfigFileExists() {
if (this.properties.isCheckConfigLocation() && StringUtils.hasText(this.properties.getConfigLocation())) {
Resource resource = this.resourceLoader.getResource(this.properties.getConfigLocation());
Assert.state(resource.exists(),
"Cannot find config location: " + resource + " (please add config file or check your Mybatis configuration)");
}
}

//conditionalOnMissingBean作用:在没有类的时候调用,创建sqlsessionFactorysqlsessionfactory最主要的是创建并保存了Configuration类
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
factory.setDataSource(dataSource);
factory.setVfs(SpringBootVFS.class);
if (StringUtils.hasText(this.properties.getConfigLocation())) {
factory.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation()));
}
applyConfiguration(factory);
if (this.properties.getConfigurationProperties() != null) {
factory.setConfigurationProperties(this.properties.getConfigurationProperties());
}
if (!ObjectUtils.isEmpty(this.interceptors)) {
factory.setPlugins(this.interceptors);
}
if (this.databaseIdProvider != null) {
factory.setDatabaseIdProvider(this.databaseIdProvider);
}
if (StringUtils.hasLength(this.properties.getTypeAliasesPackage())) {
factory.setTypeAliasesPackage(this.properties.getTypeAliasesPackage());
}
if (this.properties.getTypeAliasesSuperType() != null) {
factory.setTypeAliasesSuperType(this.properties.getTypeAliasesSuperType());
}
if (StringUtils.hasLength(this.properties.getTypeHandlersPackage())) {
factory.setTypeHandlersPackage(this.properties.getTypeHandlersPackage());
}
if (!ObjectUtils.isEmpty(this.typeHandlers)) {
factory.setTypeHandlers(this.typeHandlers);
}
if (!ObjectUtils.isEmpty(this.properties.resolveMapperLocations())) {
factory.setMapperLocations(this.properties.resolveMapperLocations());
}
Set<String> factoryPropertyNames = Stream
.of(new BeanWrapperImpl(SqlSessionFactoryBean.class).getPropertyDescriptors()).map(PropertyDescriptor::getName)
.collect(Collectors.toSet());
Class<? extends LanguageDriver> defaultLanguageDriver = this.properties.getDefaultScriptingLanguageDriver();
if (factoryPropertyNames.contains("scriptingLanguageDrivers") && !ObjectUtils.isEmpty(this.languageDrivers)) {
// Need to mybatis-spring 2.0.2+
factory.setScriptingLanguageDrivers(this.languageDrivers);
if (defaultLanguageDriver == null && this.languageDrivers.length == 1) {
defaultLanguageDriver = this.languageDrivers[0].getClass();
}
}
if (factoryPropertyNames.contains("defaultScriptingLanguageDriver")) {
// Need to mybatis-spring 2.0.2+
factory.setDefaultScriptingLanguageDriver(defaultLanguageDriver);
}

// //获取SqlSessionFactoryBean的getObject()中的对象注入Spring容器,也就是SqlSessionFactory对象
return factory.getObject();
}

private void applyConfiguration(SqlSessionFactoryBean factory) {
Configuration configuration = this.properties.getConfiguration();
if (configuration == null && !StringUtils.hasText(this.properties.getConfigLocation())) {
configuration = new Configuration();
}
if (configuration != null && !CollectionUtils.isEmpty(this.configurationCustomizers)) {
for (ConfigurationCustomizer customizer : this.configurationCustomizers) {
customizer.customize(configuration);
}
}
factory.setConfiguration(configuration);
}

// 往Spring容器中注入SqlSessionTemplate对象
@Bean
@ConditionalOnMissingBean
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.
*/
public static class AutoConfiguredMapperScannerRegistrar implements BeanFactoryAware, ImportBeanDefinitionRegistrar {

private BeanFactory beanFactory;

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

if (!AutoConfigurationPackages.has(this.beanFactory)) {
logger.debug("Could not determine auto-configuration package, automatic mapper scanning disabled.");
return;
}

logger.debug("Searching for mappers annotated with @Mapper");

List<String> packages = AutoConfigurationPackages.get(this.beanFactory);
if (logger.isDebugEnabled()) {
packages.forEach(pkg -> logger.debug("Using auto-configuration base package '{}'", pkg));
}

BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);
builder.addPropertyValue("processPropertyPlaceHolders", true);
builder.addPropertyValue("annotationClass", Mapper.class);
builder.addPropertyValue("basePackage", StringUtils.collectionToCommaDelimitedString(packages));
BeanWrapper beanWrapper = new BeanWrapperImpl(MapperScannerConfigurer.class);
Set<String> propertyNames = Stream.of(beanWrapper.getPropertyDescriptors()).map(PropertyDescriptor::getName)
.collect(Collectors.toSet());
if (propertyNames.contains("lazyInitialization")) {
// Need to mybatis-spring 2.0.2+
builder.addPropertyValue("lazyInitialization", "${mybatis.lazy-initialization:false}");
}
if (propertyNames.contains("defaultScope")) {
// Need to mybatis-spring 2.0.6+
builder.addPropertyValue("defaultScope", "${mybatis.mapper-default-scope:}");
}
registry.registerBeanDefinition(MapperScannerConfigurer.class.getName(), builder.getBeanDefinition());
}

@Override
public void setBeanFactory(BeanFactory beanFactory) {
this.beanFactory = beanFactory;
}

}

/**
* If mapper registering configuration or mapper scanning configuration not present, this configuration allow to scan
* mappers based on the same component-scanning path as Spring Boot itself.
*/
@org.springframework.context.annotation.Configuration
@Import(AutoConfiguredMapperScannerRegistrar.class)
@ConditionalOnMissingBean({ MapperFactoryBean.class, MapperScannerConfigurer.class })
public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean {

@Override
public void afterPropertiesSet() {
logger.debug(
"Not found configuration for registering mapper bean using @MapperScan, MapperFactoryBean and MapperScannerConfigurer.");
}

}

}

MybatisAutoConfiguration:

①类中有个MybatisProperties类,该类对应的是mybatis的配置文件

②类中有个sqlSessionFactory方法,作用是创建SqlSessionFactory类、Configuration类(mybatis最主要的类,保存着与mybatis相关的东西)

③SelSessionTemplate,作用是与mapperProoxy代理类有关sqlSessionFactory主要是通过创建了一个SqlSessionFactoryBean,这个类实现了FactoryBean接口,所以在Spring容器就会注入这个类中定义的getObject方法返回的对象。

看一下getObject()方法做了什么?

@Override
public SqlSessionFactory getObject() throws Exception {
if (this.sqlSessionFactory == null) {
afterPropertiesSet();
}
return this.sqlSessionFactory;
}

@Override
public void afterPropertiesSet() throws Exception {
notNull(dataSource, "Property 'dataSource' is required");
notNull(sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required");
state((configuration == null && configLocation == null) || !(configuration != null && configLocation != null),
"Property 'configuration' and 'configLocation' can not specified with together");

this.sqlSessionFactory = buildSqlSessionFactory();
}

protected SqlSessionFactory buildSqlSessionFactory() throws IOException {

Configuration configuration;

XMLConfigBuilder xmlConfigBuilder = null;
if (this.configuration != null) {
configuration = this.configuration;
if (configuration.getVariables() == null) {
configuration.setVariables(this.configurationProperties);
} else if (this.configurationProperties != null) {
configuration.getVariables().putAll(this.configurationProperties);
}
} else if (this.configLocation != null) {
xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties);
configuration = xmlConfigBuilder.getConfiguration();
} else {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Property 'configuration' or 'configLocation' not specified, using default MyBatis Configuration");
}
configuration = new Configuration();
if (this.configurationProperties != null) {
configuration.setVariables(this.configurationProperties);
}
}

if (this.objectFactory != null) {
configuration.setObjectFactory(this.objectFactory);
}

if (this.objectWrapperFactory != null) {
configuration.setObjectWrapperFactory(this.objectWrapperFactory);
}

if (this.vfs != null) {
configuration.setVfsImpl(this.vfs);
}

if (hasLength(this.typeAliasesPackage)) {
String[] typeAliasPackageArray = tokenizeToStringArray(this.typeAliasesPackage,
ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
for (String packageToScan : typeAliasPackageArray) {
configuration.getTypeAliasRegistry().registerAliases(packageToScan,
typeAliasesSuperType == null ? Object.class : typeAliasesSuperType);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Scanned package: '" + packageToScan + "' for aliases");
}
}
}

if (!isEmpty(this.typeAliases)) {
for (Class<?> typeAlias : this.typeAliases) {
configuration.getTypeAliasRegistry().registerAlias(typeAlias);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Registered type alias: '" + typeAlias + "'");
}
}
}

if (!isEmpty(this.plugins)) {
for (Interceptor plugin : this.plugins) {
configuration.addInterceptor(plugin);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Registered plugin: '" + plugin + "'");
}
}
}

if (hasLength(this.typeHandlersPackage)) {
String[] typeHandlersPackageArray = tokenizeToStringArray(this.typeHandlersPackage,
ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
for (String packageToScan : typeHandlersPackageArray) {
configuration.getTypeHandlerRegistry().register(packageToScan);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Scanned package: '" + packageToScan + "' for type handlers");
}
}
}

if (!isEmpty(this.typeHandlers)) {
for (TypeHandler<?> typeHandler : this.typeHandlers) {
configuration.getTypeHandlerRegistry().register(typeHandler);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Registered type handler: '" + typeHandler + "'");
}
}
}

if (this.databaseIdProvider != null) {//fix #64 set databaseId before parse mapper xmls
try {
configuration.setDatabaseId(this.databaseIdProvider.getDatabaseId(this.dataSource));
} catch (SQLException e) {
throw new NestedIOException("Failed getting a databaseId", e);
}
}

if (this.cache != null) {
configuration.addCache(this.cache);
}

if (xmlConfigBuilder != null) {
try {
xmlConfigBuilder.parse();

if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Parsed configuration file: '" + this.configLocation + "'");
}
} catch (Exception ex) {
throw new NestedIOException("Failed to parse config resource: " + this.configLocation, ex);
} finally {
ErrorContext.instance().reset();
}
}

if (this.transactionFactory == null) {
this.transactionFactory = new SpringManagedTransactionFactory();
}

configuration.setEnvironment(new Environment(this.environment, this.transactionFactory, this.dataSource));

if (!isEmpty(this.mapperLocations)) {
for (Resource mapperLocation : this.mapperLocations) {
if (mapperLocation == null) {
continue;
}

try {
XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
configuration, mapperLocation.toString(), configuration.getSqlFragments());
//这个方法已经是mybatis的源码,初始化流程
xmlMapperBuilder.parse();
} catch (Exception e) {
throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e);
} finally {
ErrorContext.instance().reset();
}

if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Parsed mapper file: '" + mapperLocation + "'");
}
}
} else {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Property 'mapperLocations' was not specified or no matching resources found");
}
}
//这个方法已经是mybatis的源码,初始化流程
return this.sqlSessionFactoryBuilder.build(configuration);
}

这个已经很明显了,实际上就是调用了MyBatis的初始化流程现在已经得到了SqlSessionFactory了,接下来就是如何扫描到相关的Mapper接口了。这个需要看这个注解@MapperScan(basePackages = “com.mybatis.mapper”)

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(MapperScannerRegistrar.class)
@Repeatable(MapperScans.class)
public @interface MapperScan {
}

通过@Import的方式会扫描到MapperScannerRegistrar类。

MapperScannerRegistrar实现了ImportBeanDefinitionRegistrar接口,那么在spring实例化之前就会调用到registerBeanDefinitions方法

public class MapperScannerRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware {}
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
//拿到MapperScan注解,并解析注解中定义的属性封装成AnnotationAttributes对象
AnnotationAttributes mapperScanAttrs = AnnotationAttributes
.fromMap(importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName()));
if (mapperScanAttrs != null) {
registerBeanDefinitions(importingClassMetadata, mapperScanAttrs, registry,
generateBaseBeanName(importingClassMetadata, 0));
}
}
 void registerBeanDefinitions(AnnotationMetadata annoMeta, AnnotationAttributes annoAttrs,
BeanDefinitionRegistry registry, String beanName) {

BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);
builder.addPropertyValue("processPropertyPlaceHolders", true);

Class<? extends Annotation> annotationClass = annoAttrs.getClass("annotationClass");
if (!Annotation.class.equals(annotationClass)) {
builder.addPropertyValue("annotationClass", annotationClass);
}

Class<?> markerInterface = annoAttrs.getClass("markerInterface");
if (!Class.class.equals(markerInterface)) {
builder.addPropertyValue("markerInterface", markerInterface);
}

Class<? extends BeanNameGenerator> generatorClass = annoAttrs.getClass("nameGenerator");
if (!BeanNameGenerator.class.equals(generatorClass)) {
builder.addPropertyValue("nameGenerator", BeanUtils.instantiateClass(generatorClass));
}

Class<? extends MapperFactoryBean> mapperFactoryBeanClass = annoAttrs.getClass("factoryBean");
if (!MapperFactoryBean.class.equals(mapperFactoryBeanClass)) {
builder.addPropertyValue("mapperFactoryBeanClass", mapperFactoryBeanClass);
}

String sqlSessionTemplateRef = annoAttrs.getString("sqlSessionTemplateRef");
if (StringUtils.hasText(sqlSessionTemplateRef)) {
builder.addPropertyValue("sqlSessionTemplateBeanName", annoAttrs.getString("sqlSessionTemplateRef"));
}

String sqlSessionFactoryRef = annoAttrs.getString("sqlSessionFactoryRef");
if (StringUtils.hasText(sqlSessionFactoryRef)) {
builder.addPropertyValue("sqlSessionFactoryBeanName", annoAttrs.getString("sqlSessionFactoryRef"));
}

List<String> basePackages = new ArrayList<>();
basePackages.addAll(
Arrays.stream(annoAttrs.getStringArray("value")).filter(StringUtils::hasText).collect(Collectors.toList()));

basePackages.addAll(Arrays.stream(annoAttrs.getStringArray("basePackages")).filter(StringUtils::hasText)
.collect(Collectors.toList()));

basePackages.addAll(Arrays.stream(annoAttrs.getClassArray("basePackageClasses")).map(ClassUtils::getPackageName)
.collect(Collectors.toList()));

if (basePackages.isEmpty()) {
basePackages.add(getDefaultBasePackage(annoMeta));
}

String lazyInitialization = annoAttrs.getString("lazyInitialization");
if (StringUtils.hasText(lazyInitialization)) {
builder.addPropertyValue("lazyInitialization", lazyInitialization);
}

String defaultScope = annoAttrs.getString("defaultScope");
if (!AbstractBeanDefinition.SCOPE_DEFAULT.equals(defaultScope)) {
builder.addPropertyValue("defaultScope", defaultScope);
}

builder.addPropertyValue("basePackage", StringUtils.collectionToCommaDelimitedString(basePackages));
//把类型为MapperScannerConfigurer的注册到spring容器中
registry.registerBeanDefinition(beanName, builder.getBeanDefinition());

}

MapperScannerConfigurer实现了BeanDefinitionRegistryPostProcessor接口,所以接着又会扫描并调用到postProcessBeanDefinitionRegistry方法。

public class MapperScannerConfigurer
implements BeanDefinitionRegistryPostProcessor, InitializingBean, ApplicationContextAware, BeanNameAware
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
if (this.processPropertyPlaceHolders) {
processPropertyPlaceHolders();
}

ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
scanner.setAddToConfig(this.addToConfig);
scanner.setAnnotationClass(this.annotationClass);
scanner.setMarkerInterface(this.markerInterface);
scanner.setSqlSessionFactory(this.sqlSessionFactory);
scanner.setSqlSessionTemplate(this.sqlSessionTemplate);
scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName);
scanner.setSqlSessionTemplateBeanName(this.sqlSessionTemplateBeanName);
scanner.setResourceLoader(this.applicationContext);
scanner.setBeanNameGenerator(this.nameGenerator);
scanner.setMapperFactoryBeanClass(this.mapperFactoryBeanClass);
if (StringUtils.hasText(lazyInitialization)) {
scanner.setLazyInitialization(Boolean.valueOf(lazyInitialization));
}
if (StringUtils.hasText(defaultScope)) {
scanner.setDefaultScope(defaultScope);
}
scanner.registerFilters();
scanner.scan(
StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
}

org.springframework.context.annotation.ClassPathBeanDefinitionScanner#scan

/**
* Perform a scan within the specified base packages.
* @param basePackages the packages to check for annotated classes
* @return number of beans registered
*/
public int scan(String... basePackages) {
int beanCountAtScanStart = this.registry.getBeanDefinitionCount();

doScan(basePackages);

// Register annotation config processors, if necessary.
if (this.includeAnnotationConfig) {
AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);
}

return (this.registry.getBeanDefinitionCount() - beanCountAtScanStart);
}

org.mybatis.spring.mapper.ClassPathMapperScanner#doScan

/**
* Calls the parent search that will search and register all the candidates. Then the registered objects are post
* processed to set them as MapperFactoryBeans
*/
@Override
public Set<BeanDefinitionHolder> doScan(String... basePackages) {
//这个方法主要就注册扫描basePackages路径下的mapper接口,然后封装成一个BeanDefinition后加入到spring容器中
Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);

if (beanDefinitions.isEmpty()) {
LOGGER.warn(() -> "No MyBatis mapper was found in '" + Arrays.toString(basePackages)
+ "' package. Please check your configuration.");
} else {
processBeanDefinitions(beanDefinitions);
}

return beanDefinitions;
}

修改了mapper的beanClass类型为MapperFactoryBean

private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
AbstractBeanDefinition definition;
BeanDefinitionRegistry registry = getRegistry();
for (BeanDefinitionHolder holder : beanDefinitions) {
definition = (AbstractBeanDefinition) holder.getBeanDefinition();
boolean scopedProxy = false;
if (ScopedProxyFactoryBean.class.getName().equals(definition.getBeanClassName())) {
definition = (AbstractBeanDefinition) Optional
.ofNullable(((RootBeanDefinition) definition).getDecoratedDefinition())
.map(BeanDefinitionHolder::getBeanDefinition).orElseThrow(() -> new IllegalStateException(
"The target bean definition of scoped proxy bean not found. Root bean definition[" + holder + "]"));
scopedProxy = true;
}
String beanClassName = definition.getBeanClassName();
LOGGER.debug(() -> "Creating MapperFactoryBean with name '" + holder.getBeanName() + "' and '" + beanClassName
+ "' mapperInterface");

// the mapper interface is the original class of the bean
// but, the actual class of the bean is MapperFactoryBean
definition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName); // issue #59
definition.setBeanClass(this.mapperFactoryBeanClass);

definition.getPropertyValues().add("addToConfig", this.addToConfig);

// Attribute for MockitoPostProcessor
// https://github.com/mybatis/spring-boot-starter/issues/475
definition.setAttribute(FACTORY_BEAN_OBJECT_TYPE, beanClassName);

boolean explicitFactoryUsed = false;
if (StringUtils.hasText(this.sqlSessionFactoryBeanName)) {
definition.getPropertyValues().add("sqlSessionFactory",
new RuntimeBeanReference(this.sqlSessionFactoryBeanName));
explicitFactoryUsed = true;
} else if (this.sqlSessionFactory != null) {
definition.getPropertyValues().add("sqlSessionFactory", this.sqlSessionFactory);
explicitFactoryUsed = true;
}

if (StringUtils.hasText(this.sqlSessionTemplateBeanName)) {
if (explicitFactoryUsed) {
LOGGER.warn(
() -> "Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
}
definition.getPropertyValues().add("sqlSessionTemplate",
new RuntimeBeanReference(this.sqlSessionTemplateBeanName));
explicitFactoryUsed = true;
} else if (this.sqlSessionTemplate != null) {
if (explicitFactoryUsed) {
LOGGER.warn(
() -> "Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
}
definition.getPropertyValues().add("sqlSessionTemplate", this.sqlSessionTemplate);
explicitFactoryUsed = true;
}

if (!explicitFactoryUsed) {
LOGGER.debug(() -> "Enabling autowire by type for MapperFactoryBean with name '" + holder.getBeanName() + "'.");
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
}

definition.setLazyInit(lazyInitialization);

if (scopedProxy) {
continue;
}

if (ConfigurableBeanFactory.SCOPE_SINGLETON.equals(definition.getScope()) && defaultScope != null) {
definition.setScope(defaultScope);
}

if (!definition.isSingleton()) {
BeanDefinitionHolder proxyHolder = ScopedProxyUtils.createScopedProxy(holder, registry, true);
if (registry.containsBeanDefinition(proxyHolder.getBeanName())) {
registry.removeBeanDefinition(proxyHolder.getBeanName());
}
registry.registerBeanDefinition(proxyHolder.getBeanName(), proxyHolder.getBeanDefinition());
}

}
}

上述几步主要是完成通过

@MapperScan(basePackages = “com.mybatis.mapper”)这个定义,扫描指定包下的mapper接口,然后设置每个mapper接口的beanClass属性为MapperFactoryBean类型并加入到spring的bean容器中。

MapperFactoryBean实现了FactoryBean接口,所以当spring从待实例化的bean容器中遍历到这个bean并开始执行实例化时返回的对象实际上是getObject方法中返回的对象。

public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> 

最后看一下MapperFactoryBean的getObject方法,实际上返回的就是mybatis中通过getMapper拿到的对象,熟悉mybatis源码的就应该清楚,这个就是mybatis通过动态代理生成的mapper接口实现类:

/**
* {@inheritDoc}
*/
@Override
public T getObject() throws Exception {
return getSqlSession().getMapper(this.mapperInterface);
}

到此,mapper接口现在也通过动态代理生成了实现类,并且注入到spring的bean容器中了,之后使用者就可以通过@Autowired或者getBean等方式,从spring容器中获取到了。

6.4 SpringBoot + Mybatis实现动态数据源切换

1. 动态数据源介绍

**业务背景 **:

电商订单项目分正向和逆向两个部分:其中正向数据库记录了订单的基本信息,包括订单基本信息、订单商品信息、优惠卷信息、发票信息、账期信息、结算信息、订单备注信息、收货人信息等;逆向数据库主要包含了商品的退货信息和维修信息。数据量超过500万行就要考虑分库分表和读写分离,那么我们在正向操作和逆向操作的时候,就需要动态的切换到相应的数据库,进行相关的操作。

**解决思路 **:

现在项目的结构设计基本上是基于MVC的,那么数据库的操作集中在dao层完成,主要业务逻辑在service层处理,controller层处理请求。假设在执行dao层代码之前能够将数据源(DataSource)换成我们想要执行操作的数据源,那么这个问题就解决了

原理图

Spring内置了一个AbstractRoutingDataSource,它可以把多个数据源配置成一个Map,然后,根据不同的key返回不同的数据源。因为AbstractRoutingDataSource也是一个DataSource接口,因此,应用程序可以先设置好key, 访问数据库的代码就可以从AbstractRoutingDataSource拿到对应的一个真实的数据源,从而访问指定的数据库。

查看AbstractRoutingDataSource类:

/**
* Abstract {@link javax.sql.DataSource} implementation that routes {@link#getConnection()}
* calls to one of various target DataSources based on a lookup key. The latter is usually
* (but not necessarily) determined through some thread-bound transaction context.
*
* 抽象 {@link javax.sql.DataSource} 路由 {@link #getConnection ()} 的实现
* 根据查找键调用不同的目标数据之一。后者通常是
* (但不一定) 通过某些线程绑定事务上下文来确定。
*/
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
//.......
/**
* Specify the map of target DataSources, with the lookup key as key.
* The mapped value can either be a corresponding {@link javax.sql.DataSource}
* instance or a data source name String (to be resolved via a
* {@link #setDataSourceLookup DataSourceLookup}).
* <p>The key can be of arbitrary type; this class implements the
* generic lookup process only. The concrete key representation will
* be handled by {@link #resolveSpecifiedLookupKey(Object)} and
* {@link #determineCurrentLookupKey()}.
*
* 指定目标数据源的映射,查找键为键。
* 映射的值可以是相应的{@link javax.sql.DataSource}
* 实例或数据源名称字符串(要通过
* {@link #setDataSourceLookup DataSourceLookup})。
* 键可以是任意类型的; 这个类只实现了
* 通用查找过程。 具体的关键表示将
* 由{@link #resolveSpecifiedLookupKey(Object)}和
* {@link #determineCurrentLookupKey()}处理。
*/
public void setTargetDataSources(Map<Object, Object> targetDataSources)
{
this.targetDataSources = targetDataSources;
}
//......
/**
* Determine the current lookup key. This will typically be
* implemented to check a thread-bound transaction context.
* <p>Allows for arbitrary keys. The returned key needs
* to match the stored lookup key type, as resolved by the
* {@link #resolveSpecifiedLookupKey} method.
*
* 确定当前的查找键。这通常会
* 实现以检查线程绑定的事务上下文。
* <p> 允许任意键。返回的密钥需要
* 与存储的查找密钥类型匹配, 如
* {@link #resolveSpecifiedLookupKey} 方法。
*/
protected abstract Object determineCurrentLookupKey();
}

上面源码中还有另外一个核心的方法setTargetDataSources(Map<Object, Object>targetDataSources) ,它需要一个Map,在方法注释中我们可以得知,这个Map存储的就是我们配置的多个数据源的键值对。我们整理一下这个类切换数据源的运作方式,这个类在连接数据库之前会执行determineCurrentLookupKey()方法,这个方法返回的数据将作为key去targetDataSources中查找相应的值,如果查找到相对应的DataSource,那么就使用此DataSource获取数据库连接。

它是一个abstract类,所以我们使用的话,推荐的方式是创建一个类来继承它并且实现它的determineCurrentLookupKey() 方法,这个方法介绍上面也进行了说明,就是通过这个方法进行数据源的切换。

2. 代码实现

  1. 实体类
@Data
public class Product {
private Integer id;
private String name;
private Double price;
}
  1. ProductMapper
public interface ProductMapper {
@Select("select * from product")
public List<Product> findAllProductM();
@Select("select * from product")
public List<Product> findAllProductS();
}
  1. ProductService
@Service
public class ProductService {
@Autowired
private ProductMapper productMapper;
public void findAllProductM(){
// 查询Master
List<Product> allProductM = productMapper.findAllProductM();
System.out.println(allProductM);
}
public void findAllProductS(){
// 查询Slave
List<Product> allProductS = productMapper.findAllProductS();
System.out.println(allProductS);
}
}
  1. ProductController
@RestController
public class ProductController {
@Autowired
private ProductService productService;
@GetMapping("/findAllProductM")
public String findAllProductM() {
productService.findAllProductM();
return "master";
}
@GetMapping("/findAllProductS")
public String findAllProductS() {
productService.findAllProductS();
return "slave";
}
}
  1. 配置多数据源
    首先,我们在application.properties中配置两个数据源
spring.druid.datasource.master.password=root
spring.druid.datasource.master.username=root
spring.druid.datasource.master.jdbcurl=
jdbc:mysql://localhost:3306/product_master?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC
spring.druid.datasource.master.driver-class-name=com.mysql.cj.jdbc.Driver

spring.druid.datasource.slave.password=root
spring.druid.datasource.slave.username=root
spring.druid.datasource.slave.jdbcurl=
jdbc:mysql://localhost:3306/product_slave?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC
spring.druid.datasource.slave.driver-class-name=com.mysql.cj.jdbc.Driver

在SpringBoot的配置代码中,我们初始化两个数据源:

@Configuration
public class MyDataSourceConfiguratioin {
Logger logger = LoggerFactory.getLogger(MyDataSourceConfiguratioin.class);
/**
* Master data source.
*/
@Bean("masterDataSource")
@ConfigurationProperties(prefix = "spring.druid.datasource.master")
DataSource masterDataSource() {
logger.info("create master datasource...");
return DataSourceBuilder.create().build();
}
/**
* Slave data source.
*/
@Bean("slaveDataSource")
@ConfigurationProperties(prefix = "spring.druid.datasource.slave")
DataSource slaveDataSource() {
logger.info("create slave datasource...");
return DataSourceBuilder.create().build();
}
}
  1. 编写RoutingDataSource
    然后,我们用Spring内置的RoutingDataSource,把两个真实的数据源代理为一个动态数据源:
public class RoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return "masterDataSource";
}
}

对这个RoutingDataSource ,需要在SpringBoot中配置好并设置为主数据源:

@Bean
@Primary
DataSource primaryDataSource(
@Autowired @Qualifier("masterDataSource") DataSource masterDataSource,
@Autowired @Qualifier("slaveDataSource") DataSource slaveDataSource) {
logger.info("create routing datasource...");
Map<Object, Object> map = new HashMap<>();
map.put("masterDataSource", masterDataSource);
map.put("slaveDataSource", slaveDataSource);
RoutingDataSource routing = new RoutingDataSource();
routing.setTargetDataSources(map);
routing.setDefaultTargetDataSource(masterDataSource);
return routing;
}

现在, RoutingDataSource 配置好了,但是,路由的选择是写死的,即永远返回”masterDataSource”, 那么问题来了:如何存储动态选择的key以及在哪设置key? 在Servlet的线程模型中,使用ThreadLocal存储key最合适,因此,我们编写一个 RoutingDataSourceContext ,来设置并动态存储key:

public class RoutingDataSourceContext {
// holds data source key in thread local:
static final ThreadLocal<String> threadLocalDataSourceKey = new ThreadLocal<>();

public static String getDataSourceRoutingKey() {
String key = threadLocalDataSourceKey.get();
return key == null ? "masterDataSource" : key;
}

public RoutingDataSourceContext(String key) {
threadLocalDataSourceKey.set(key);
}

public void close() {
threadLocalDataSourceKey.remove();
}
}

然后,修改RoutingDataSource ,获取key的代码如下

public class RoutingDataSource extends AbstractRoutingDataSource {
protected Object determineCurrentLookupKey() {
return RoutingDataSourceContext.getDataSourceRoutingKey();
}
}

这样,在某个地方,例如一个Controller的方法内部,就可以动态设置DataSource的Key:

@GetMapping("/findAllProductM")
public String findAllProductM() {
String key = "masterDataSource";
RoutingDataSourceContext routingDataSourceContext = new RoutingDataSourceContext(key);
productService.findAllProductM();
return "master";
}
@GetMapping("/findAllProductS")
public String findAllProductS() {
String key = "slaveDataSource";
RoutingDataSourceContext routingDataSourceContext = new RoutingDataSourceContext(key);
productService.findAllProductS();
return "slave";
}

到此为止,我们已经成功实现了数据库的动态路由访问。

3. 优化

以上代码是可行的,但是,需要读数据库的地方,就需要加上一大段RoutingDataSourceContext

Spring提供的声明式事务管理,就只需要一个@Transactional() 注解,放在某个Java方法上,这个方法就自动具有了事务。

我们也可以编写一个类似的@RoutingWith(“slaveDataSource”) 注解,放到某个Controller的方法上,这个方法内部就自动选择了对应的数据源。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RoutingWith {
String value() default "master";
}

编译前需要添加一个Maven依赖:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

切面类:

@Aspect
@Component
public class RoutingAspect {
@Around("@annotation(routingWith)")
public Object routingWithDataSource(ProceedingJoinPoint joinPoint,
RoutingWith routingWith) throws Throwable {
String key = routingWith.value();
RoutingDataSourceContext ctx = new RoutingDataSourceContext(key);
return joinPoint.proceed();
}
}

注意方法的第二个参数RoutingWith是Spring传入的注解实例,我们根据注解的value()获取配置的key。

改造方法:

@RoutingWith("masterDataSource")
@GetMapping("/findAllProductM")
public String findAllProductM() {
/* String key = "masterDataSource";
* RoutingDataSourceContext routingDataSourceContext = new RoutingDataSourceContext(key);
*/
productService.findAllProductM();
return "lagou";
}
@RoutingWith("slaveDataSource")
@GetMapping("/findAllProductS")
public String findAllProductS() {
/*String key = "slaveDataSource";
*RoutingDataSourceContext routingDataSourceContext = new RoutingDataSourceContext(key);
*/
productService.findAllProductS();
return "lagou";
}

到此为止,我们就实现了用注解动态选择数据源的功能。