1. SpringBoot 2.x 的动态数据源
在Spring Boot 2.x中实现动态数据源的原理是通过动态注册和切换DataSource
bean来实现。
1.1 数据源的注册
数据源的注册:在应用启动时,通过配置文件或者代码,创建并注册默认的数据源bean(通常是DataSource
的实现类,如HikariDataSource
)。这个默认数据源将用于应用的初始数据访问。
1.2 动态数据源类
动态数据源类:创建一个动态数据源类,它继承自AbstractRoutingDataSource
。在这个类中,重写determineCurrentLookupKey()
方法,根据当前的数据源标识(如线程上下文中的标识)返回要使用的数据源的名称。
1.3 数据源切换器
数据源切换器:创建一个数据源切换器,它是一个工具类,用于在运行时切换数据源。可以使用ThreadLocal
或者@Scope
注解来实现数据源的线程隔离,将当前线程使用的数据源标识存储在ThreadLocal
中。
1.4 数据源路由配置
数据源路由配置:在Spring配置类中,将动态数据源类和数据源切换器配置为Bean,并使用@Primary
注解将动态数据源类设置为默认数据源。
1.5 动态数据源切换
动态数据源切换:在需要切换数据源的地方,通过数据源切换器设置当前线程使用的数据源标识。在应用访问数据库之前,动态数据源类会调用determineCurrentLookupKey()
方法获取当前数据源的名称,然后根据名称获取对应的数据源bean。
1.6 事务管理器配置
事务管理器配置:为了确保在事务中正确使用动态数据源,需要配置事务管理器。事务管理器需要注入动态数据源作为其数据源。
通过以上的实现方式,当需要切换数据源时,只需在适当的位置调用数据源切换器设置当前线程使用的数据源标识,动态数据源类会根据标识选择相应的数据源。这样就实现了在运行时动态切换数据源的功能。
需要注意的是,在切换数据源后,需要手动重新加载持久层框架的相关配置(如MyBatis的SqlSessionFactory
),以确保它们能够正确地使用新的数据源。
2.业务层实现原理
2.1 定义主数据源配置
在配置文件中定义了主数据源的配置信息,包括驱动类、连接URL、用户名和密码等。
#关系型数据库默认动态数据源配置
dynamic:
datasource:
sourceId: 2000
driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/dynamicdata?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
username: root
password: root
mapperPath: classpath*:mapper/*.xml
mybatisPlus: true
plusVersion: 3.5.2
主数据源配置类:
@ConfigurationProperties(prefix = "dynamic.datasource")
@Component
@Data
public class DynamicDataSourceProperties {
private String sourceId;
/**
* Fully qualified name of the JDBC driver. Auto-detected based on the URL by default.
*/
private String driverClassName;
/**
* JDBC URL of the database.
*/
private String url;
/**
* Login username of the database.
*/
private String username;
/**
* Login password of the database.
*/
private String password;
private String mapperPath;
/**
* data source type
*/
private String dataSourceType;
private boolean ext;
private boolean mybatisPlus;
}
2.2 注册动态数据源及mybatis/mybatis plus
(1)配置mybatis或mybatis-plus
当前案例的数据源来自DB,所以除了主数据源配置外,还需要提供dao的mybatis或mybatis-plus配置。
spring:
profiles:
active: data
application:
name: dynamic
#mybatis-plus配置
mybatis-plus:
#配置Mapper映射文件
mapper-locations: classpath*:mapper/*.xml
global-config:
banner: false
#设置逻辑删除(mybatis-plus的 删除 会成为逻辑删除 查询 方法自动过滤删除数据)
db-config:
#删除值
logic-delete-value: 0
#未删除值
logic-not-delete-value: 1
configuration:
#下划线驼峰命名
map-underscore-to-camel-case: true
#开启日志打印
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
server:
port: 8181
这里把主启动类的配置也一起列出来。
(2)动态数据源的配置类
在DynamicDataSourceConfig
类中,使用DynamicRoutingDataSource
作为数据源,并设置默认数据源为主数据源。
/**
* @author wsp
* @create 2023-04-24 22:51
*/
@Configuration
public class DynamicDataSourceConfig {
@Autowired
private DynamicDataSourceProperties properties;
public static String defaultDbKey;
/**
* 动态数据源
*/
@Bean
public DynamicDataSource dynamicDataSource() {
DynamicDataSource dataSource = new DynamicDataSource();
Map<Object, Object> targetDataSources = new HashMap<>();
dataSource.setTargetDataSources(targetDataSources);
return dataSource;
}
/**
* 会话工厂
*/
@Bean
@Primary
public FactoryBean sqlSessionFactoryBean() throws IOException {
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
if(!properties.isMybatisPlus()) {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dynamicDataSource());
org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
configuration.setMapUnderscoreToCamelCase(true);
sqlSessionFactoryBean.setConfiguration(configuration);
sqlSessionFactoryBean.setMapperLocations(resolver.getResources(properties.getMapperPath())); //"classpath*:/repository/*.xml"
return sqlSessionFactoryBean;
} else {
MybatisSqlSessionFactoryBean factoryBean = new MybatisSqlSessionFactoryBean();
factoryBean.setDataSource(dynamicDataSource());
factoryBean.setMapperLocations(resolver.getResources(properties.getMapperPath()));
GlobalConfig config = new GlobalConfig();
config.setBanner(false);
factoryBean.setGlobalConfig(config);
/*在加载MybatisSqlSessionFactoryBean时,把相应的插件加载进去*/
factoryBean.setPlugins(paginationInterceptor());
return factoryBean;
}
}
/*这里不变,还是用来配置一些外部的插件*/
@Bean
public MybatisPlusInterceptor paginationInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 指定数据库方言为 H2
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
/**
* 事务管理器
*/
@Bean
public PlatformTransactionManager transactionManager() {
return new DataSourceTransactionManager(dynamicDataSource());
}
@Bean
public Object initDefaultDataSource() {
DynamicDataSourceService.createDataSource(properties);
defaultDbKey = properties.getSourceId();
return new Object();
}
}
-
dynamicDataSource()
方法创建了一个DynamicDataSource
对象作为动态数据源,并设置一个空的目标数据源集合。 -
sqlSessionFactoryBean()
方法用于配置会话工厂,根据properties
中的配置判断是否使用MyBatis Plus。如果不使用MyBatis Plus,则创建一个SqlSessionFactoryBean
对象(即Mybatis),设置动态数据源作为数据源,配置MyBatis的相关属性,并指定Mapper文件的路径。如果使用MyBatis Plus,则创建一个MybatisSqlSessionFactoryBean
对象,同样设置动态数据源作为数据源,并加载Mapper文件的路径。此外,还设置了MyBatis Plus的全局配置和插件。 (可以使用Mybatis Plus 或 Mybatis) -
paginationInterceptor()
方法用于创建一个MybatisPlusInterceptor
对象,并添加了一个PaginationInnerInterceptor
作为插件,指定数据库方言为MySQL。 -
transactionManager()
方法创建了一个DataSourceTransactionManager
对象作为事务管理器,设置动态数据源作为数据源。 -
initDefaultDataSource()
方法在容器初始化时调用,通过DynamicDataSourceService
创建默认的数据源,并将其设置为默认数据源。
这些配置通过使用@Bean
注解将相应的对象注册到Spring容器中。整体上,该配置类完成了动态数据源的创建和管理,并配置了会话工厂、事务管理器等相关组件,以支持在应用中使用动态数据源进行数据访问和事务管理。
2.3 初始化主数据源
创建了一个DynamicDataSourceProvider
类,该类负责提供数据源,并在初始化阶段调用provide
方法创建数据源。
/**
* @author wsp
* @create 2023-04-24 22:53
*/
@Component
public class DynamicDataSourceProvider {
@Autowired
private DynamicDataSourceProperties dynamicDataSourceProperties;
@Autowired
private DynamicDataSourceConfig dynamicDataSourceConfig;
//
private List<DataSourceInfo> dataSources;
@Resource
private DataSourceInfoMapper dataSourceInfoMapper;
private DataSource buildDataSource(DynamicDataSourceProperties prop) {
DataSourceBuilder<?> builder = DataSourceBuilder.create();
builder.driverClassName(prop.getDriverClassName());
builder.username(prop.getUsername());
builder.password(prop.getPassword());
builder.url(prop.getUrl());
return builder.build();
}
public List<DataSource> provide() {
List<DataSource> res = new ArrayList<>();
String initTenantId = "";
if (dynamicDataSourceProperties != null) {
DataSource dataSource = buildDataSource(dynamicDataSourceProperties);
dynamicDataSourceConfig.initDefaultDataSource();
res.add(dataSource);
}
//获取数据源信息
dataSources = dataSourceInfoMapper.selectList(new LambdaQueryWrapper<DataSourceInfo>().eq(false, DataSourceInfo :: getDriverClassName, "com.mysql.cj.jdbc.Driver"));
dataSources.forEach(item -> {
DynamicDataSourceProperties properties = new DynamicDataSourceProperties();
properties.setSourceId(item.getSourceId());
properties.setDriverClassName(item.getDriverClassName());
properties.setUrl(item.getUrl());
properties.setUsername(item.getUsername());
properties.setPassword(item.getPassword());
DataSource dataSource = buildDataSource(properties);
res.add(dataSource);
if (!StringUtils.equals(initTenantId, item.getSourceId())) {
DynamicDataSourceService.createDataSource(properties);
}
});
return res;
}
@PostConstruct
public void init() {
provide();
}
}
DynamicDataSourceProvider
类中的provide
方法实现了数据源的动态添加和切换逻辑。首先创建主数据源,然后从数据库中获取其他数据源的配置信息。每个数据源都使用DynamicDataSourceService
的createDataSource
方法创建并添加到数据源列表中。
以下是代码的分析:
(1)在类上使用`@Component`注解,将该类标记为一个Spring组件。
(2)通过`@Autowired`注解注入`DynamicDataSourceProperties`和`DynamicDataSourceConfig`对象。
(3)定义`dataSources`成员变量,用于存储数据源信息。
(4)使用`@Resource`注解注入`DataSourceInfoMapper`对象,用于操作数据源信息。
(5)实现`buildDataSource()`方法,根据传入的`DynamicDataSourceProperties`对象构建并返回数据源。
(6)实现`provide()`方法,用于提供数据源列表。
- 创建一个空的结果列表`res`。
- 如果`dynamicDataSourceProperties`不为null,说明存在初始数据源信息。
- 调用`buildDataSource(dynamicDataSourceProperties)`方法根据初始数据源信息创建数据源。
- 调用`dynamicDataSourceConfig.initDefaultDataSource()`方法初始化默认数据源。
- 将初始数据源添加到结果列表`res`中。
- 获取数据源信息:
- 调用`dataSourceInfoMapper.selectList(...)`方法从数据库中查询数据源信息。
- 对查询结果进行遍历,对每个数据源信息进行处理。
- 创建一个新的`DynamicDataSourceProperties`对象,并设置相应的属性。
- 调用`buildDataSource(properties)`方法根据数据源属性创建数据源。
- 将数据源添加到结果列表`res`中。
- 如果当前数据源的`sourceId`与`initTenantId`不相等,调用`DynamicDataSourceService.createDataSource(properties)`方法创建数据源。
(7)在`@PostConstruct`注解的方法`init()`中调用`provide()`方法,初始化数据源提供者,在初始化过程中会自动加载初始数据源和从数据库中加载的数据源。
总体上,该代码实现了动态数据源的提供功能。在初始化过程中,会根据初始数据源信息创建数据源,并将其添加到结果列表中。然后从数据库中查询数据源信息,对每个数据源信息进行处理,创建相应的数据源,并将其添加到结果列表中。如果某个数据源的`sourceId`与`initTenantId`不相等,会调用`DynamicDataSourceService.createDataSource(properties)`方法创建数据源。最终,返回结果列表包含所有的数据源。
2.4 数据源管理
/**
* @author wsp
* @create 2023-04-24 22:50
*/
@Component
public class DynamicDataSourceService {
private static final Logger log = LoggerFactory.getLogger(DynamicDataSourceService.class);
private static final ConcurrentHashMap<Object, Object> dataSources = new ConcurrentHashMap<>();
/**
* 用于初始化数据源时加锁
*/
private final ConcurrentHashMap<String, Object> dataSourceInitLock = new ConcurrentHashMap<>(100);
private static final ThreadLocal<String> dbKeys = ThreadLocal.withInitial(() -> null);
/**
* 动态激活一个数据源并入缓存
*
* @param name 数据源的key
* @param dataSource 数据源对象
*/
public static void activeDataSourceAndCache(String name, DataSource dataSource) {
dataSources.put(name, dataSource);
DynamicDataSource dynamicDataSource = StaticMethodGetBean.getBean(DynamicDataSource.class);
dynamicDataSource.setTargetDataSources(dataSources);
dynamicDataSource.afterPropertiesSet();
log.info("添加了数据源:{}",name);
}
/**
* 切换数据源
*/
public static void switchDb(String dbKey) {
dbKeys.set(dbKey);
}
/**
* 重置数据源
*/
public static void resetDb() {
dbKeys.remove();
}
/**
* 获取当前数据源
*/
public static String currentDb() {
return dbKeys.get();
}
/**
* 创建数据源
* @param properties
* @return
*/
public static DataSource createDataSource(DynamicDataSourceProperties properties) {
DataSourceBuilder<?> builder = DataSourceBuilder.create();
builder.driverClassName(properties.getDriverClassName());
builder.username(properties.getUsername());
builder.password(properties.getPassword());
builder.url(properties.getUrl());
DataSource dataSource = builder.build();
String sourceId = properties.getSourceId();
activeDataSourceAndCache(sourceId, dataSource);
return dataSource;
}
}
DynamicDataSourceService
类是数据源的管理类,提供了创建数据源、切换数据源和移除数据源等方法。在创建数据源时,使用DataSourceBuilder
创建数据源对象,并根据配置信息设置驱动类、连接URL、用户名和密码等属性。
2.5 新数据源创建入口
@RestController
@RequestMapping("/dynamic")
public class DynamicController {
@Autowired
private DynamicDataSourceService dynamicDataSourceService;
@Autowired
private DataSourceInfoMapper dataSourceInfoMapper;
/**
* 创建数据源
* @param dataSourceInfo
* @return
*/
@PostMapping("/create")
public String create(@RequestBody DataSourceInfo dataSourceInfo) {
DynamicDataSourceProperties properties = new DynamicDataSourceProperties();
//数据库新增数据源信息
dataSourceInfoMapper.insert(dataSourceInfo);
properties.setSourceId(dataSourceInfo.getSourceId());
properties.setUrl(dataSourceInfo.getUrl());
properties.setDriverClassName(dataSourceInfo.getDriverClassName());
properties.setUsername(dataSourceInfo.getUsername());
properties.setPassword(dataSourceInfo.getPassword());
//创建动态数据源
DataSource dataSource = DynamicDataSourceService.createDataSource(properties);
return FastjsonUtil.getBeanToJson("ok");
}
}
DynamicController
类是一个RESTful接口,用于接收请求并创建数据源。在create
方法中,先将新的数据源信息插入数据库,然后调用DynamicDataSourceService
的createDataSource
方法创建数据源,并返回结果。
在DynamicController
中通过@Autowired
注解注入DynamicDataSourceService
和DataSourceInfoMapper
,用于动态数据源的管理和数据库操作。
2.6 实现数据源切换
@NoArgsConstructor
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
String currentDb = DynamicDataSourceService.currentDb();
if (currentDb == null) {
return DynamicDataSourceConfig.defaultDbKey;
}
return currentDb;
}
}
数据源切换是通过使用AbstractRoutingDataSource
实现的。在DynamicRoutingDataSource
类中继承了AbstractRoutingDataSource
,重写了determineCurrentLookupKey
方法。该方法根据线程本地变量中存储的数据源标识,决定当前使用哪个数据源。
2.7 获取bean对象工具类
@Component
public class StaticMethodGetBean<T> implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
StaticMethodGetBean.applicationContext = applicationContext;
}
@Nullable
public static <T> T getBean(Class<T> clazz) {
return applicationContext != null?applicationContext.getBean(clazz):null;
}
}
这段代码定义了一个名为 StaticMethodGetBean
的类,它实现了 ApplicationContextAware
接口,并使用了 @Component
注解,将其标记为一个 Spring 组件。
代码定义了一个静态变量 applicationContext
,用于保存应用程序的上下文。实现了 setApplicationContext
方法,该方法在应用程序上下文被初始化时被调用,并将应用程序上下文赋值给 applicationContext
变量。
实现了一个静态的 getBean
方法,该方法接收一个 Class<T>
类型的参数,用于获取 Spring 容器中的相应类型的 Bean 实例。
-
在方法内部,通过判断
applicationContext
是否为null
,如果不为null
,则调用applicationContext.getBean(clazz)
方法来获取对应的 Bean 实例。 -
如果
applicationContext
为null
,则返回null
。
通过这个工具类,可以在非 Spring 管理的类中通过静态方法调用 getBean
方法来获取 Spring 容器中的 Bean 实例,从而实现非依赖注入的方式获取 Bean 对象。