Mybatis SpringBoot DB series 】 【 AbstractRoutingDataSource based multiple source switch with AOP implementation
The previous blog introduces the configuration of multiple data sources in Mybatis. Simply speaking, it is a configuration specified for a data source, and Mapper of different data sources is specified separately. This article introduces another way, with the aid of AbstractRoutingDataSource switch to achieve dynamic data source, and through the way of custom annotations + AOP to achieve the specified data source
I. Environment preparation
1. Database related
Take mysql as an example to illustrate, because multiple data sources are needed, the simplest case is multiple logical libraries on a physical library, this paper is based on the local mysql operation
Create database test and story; create table money; create database test and story;
CREATE TABLE `money` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(20) NOT NULL DEFAULT ' ' COMMENT 'Username',
`money` int(26) NOT NULL DEFAULT '0' COMMENT 'money',
`is_deleted` tinyint(1) NOT NULL DEFAULT '0',
`create_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Creation time',
`update_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Update Time'.PRIMARY KEY (`id`),
KEY `name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
Copy the code
2. Project environment
This project is developed by SpringBoot 2.2.1.RELEASE + Maven 3.5.3 + IDEA
Here is the core POM.xml (source code available at the end of this article)
<dependencies>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
Copy the code
Configuration file information application.yml
Note that this configuration is not consistent with the previous blog post, and the reason will be explained later
spring:
dynamic:
datasource:
story:
driver-class-name: com.mysql.cj.jdbc.Driver
url: JDBC: mysql: / / 127.0.0.1:3306 / story? useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password:
test:
driver-class-name: com.mysql.cj.jdbc.Driver
url: JDBC: mysql: / / 127.0.0.1:3306 / test? useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password:
# log correlation
logging:
level:
root: info
org:
springframework:
jdbc:
core: debug
Copy the code
II. Configure multiple data sources
Mybatis multi – data source configuration and use
Before we begin, it’s worth reviewing what were the main problems with Mybatis multi-data source configuration
- With one more data source, one more configuration is required
- Mapper files require subcontracting, which is a potential pitfall for developers
For the above, then we want to achieve the goal is also very clear, to solve the above two problems
1. AbstractRoutingDataSource
The key to realizing multiple data sources, as can be seen from the name, is to route specific data sources, and its core code is as follows
// Returns the selected data source
protected DataSource determineTargetDataSource(a) {
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 {
returndataSource; }}@Nullable
protected abstract Object determineCurrentLookupKey(a);
Copy the code
Including determineCurrentLookupKey need our own to achieve, whether return to which data source
2. Dynamic data source implementation
We create a DynamicDataSource that inherits from the abstract class above
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey(a) {
String dataBaseType = DSTypeContainer.getDataBaseType();
returndataBaseType; }}Copy the code
Notice the implementation method above. How do you decide which data source to return?
One option is to add an annotation @ds to the Mapper file that specifies the corresponding data source, and then use it to determine which data source to execute.
Since the above implementation does not pass parameters, we consider passing information in a thread context
public class DSTypeContainer {
private static final ThreadLocal<String> TYPE = new ThreadLocal<String>();
public static String defaultType;
/** * Sets the data source type ** to the current thread@param dataBase
*/
public static void setDataBaseType(String dataBase) {
if (StringUtils.isEmpty(dataBase)) {
dataBase = defaultType;
}
TYPE.set(dataBase);
System.err.println("[change current data source to] :" + dataBase);
}
/** * Get the data source type **@return* /
public static String getDataBaseType(a) {
String database = TYPE.get();
System.err.println("[get current data source type] :" + database);
return database;
}
/** * Clear the data type */
public static void clearDataBaseType(a) { TYPE.remove(); }}Copy the code
3. Annotation implementation
Although the policy chosen by the data source was given above to get DataBaseType from the thread context, how do you stuff this data into the thread context?
The solution we need to support is of course to intercept the Sql before it is executed, writing to the DataBaseType, so we can consider defining an annotation on the xxxMapper interface, then intercepting its access execution, getting the data source writing context specified in the annotation before execution, and knowing the context after execution
A very basic data source annotation @ds
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface DS {
String value(a) default "";
}
Copy the code
Annotations to intercept
@Aspect
@Component
public class DsAspect {
// Intercepts method calls with DS annotations on the class
@Around("@within(DS)")
public Object dsAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
DS ds = (DS) proceedingJoinPoint.getSignature().getDeclaringType().getAnnotation(DS.class);
try {
// Write the thread context, which DB should be used
DSTypeContainer.setDataBaseType(ds == null ? null : ds.value());
return proceedingJoinPoint.proceed();
} finally {
// Clear the context informationDSTypeContainer.clearDataBaseType(); }}}Copy the code
4. Register the configuration
Now we need to register the DynamicDataSource and provide it to the SqlSessionFactory. Here we want to solve the problem that we don’t need to change the configuration even if we add more data sources, so we adjust the configuration structure of the data source
spring:
dynamic:
datasource:
story:
driver-class-name: com.mysql.cj.jdbc.Driver
url: JDBC: mysql: / / 127.0.0.1:3306 / story? useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password:
test:
driver-class-name: com.mysql.cj.jdbc.Driver
url: JDBC: mysql: / / 127.0.0.1:3306 / test? useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password:
Copy the code
It then presents a configuration class DSProperties that loads the above configuration
@Data
@ConfigurationProperties(prefix = "spring.dynamic")
public class DSProperties {
private Map<String, DataSourceProperties> datasource;
}
Copy the code
Then the implementation of our AutoConfiguration class is relatively clear (I suggest comparing the configuration classes in the previous post).
@Configuration
@EnableConfigurationProperties(DSProperties.class)
@MapperScan(basePackages = {"com.git.hui.boot.multi.datasource.mapper"}, sqlSessionFactoryRef = "SqlSessionFactory")
public class DynamicDataSourceConfig {
@SuppressWarnings("unchecked")
@Bean(name = "dynamicDataSource")
public DynamicDataSource DataSource(DSProperties dsProperties) {
Map targetDataSource = new HashMap<>(8);
dsProperties.getDatasource().forEach((k, v) -> {
targetDataSource.put(k, v.initializeDataSourceBuilder().build());
});
DynamicDataSource dataSource = new DynamicDataSource();
dataSource.setTargetDataSources(targetDataSource);
// Set the default database. The following assignment is not recommended, just for convenience
DSTypeContainer.defaultType = (String) targetDataSource.keySet().stream().findFirst().get();
dataSource.setDefaultTargetDataSource(targetDataSource.get(DSTypeContainer.defaultType));
return dataSource;
}
@Bean(name = "SqlSessionFactory")
public SqlSessionFactory test1SqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dynamicDataSource)
throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dynamicDataSource);
bean.setMapperLocations(
new PathMatchingResourcePatternResolver().getResources("classpath*:mapping/*/*.xml"));
returnbean.getObject(); }}Copy the code
5. Database entity classes
Project structure drawing
All of the previous ones are generic configuration related, and the next ones are specific database operation-related entity classes, Mapper classes
Database entity class StoryMoneyEntity
@Data
public class StoryMoneyEntity {
private Integer id;
private String name;
private Long money;
private Integer isDeleted;
private Timestamp createAt;
private Timestamp updateAt;
}
Copy the code
Mapper defines the interface storyMoneapper + TestMoneapper
@DS(value = "story")
@Mapper
public interface StoryMoneyMapper {
List<StoryMoneyEntity> findByIds(List<Integer> ids);
}
@DS(value = "test")
@Mapper
public interface TestMoneyMapper {
List<TestMoneyEntity> findByIds(List<Integer> ids);
}
Copy the code
Corresponding XML file
<! DOCTYPEmapper PUBLIC "- / / mybatis.org//DTD Mapper / 3.0 / EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.git.hui.boot.multi.datasource.mapper.StoryMoneyMapper">
<resultMap id="BaseResultMap" type="com.git.hui.boot.multi.datasource.entity.StoryMoneyEntity">
<id column="id" property="id" jdbcType="INTEGER"/>
<result column="name" property="name" jdbcType="VARCHAR"/>
<result column="money" property="money" jdbcType="INTEGER"/>
<result column="is_deleted" property="isDeleted" jdbcType="TINYINT"/>
<result column="create_at" property="createAt" jdbcType="TIMESTAMP"/>
<result column="update_at" property="updateAt" jdbcType="TIMESTAMP"/>
</resultMap>
<sql id="money_po">
id, `name`, money, is_deleted, create_at, update_at
</sql>
<select id="findByIds" parameterType="list" resultMap="BaseResultMap">
select
<include refid="money_po"/>
from money where id in
<foreach item="id" collection="list" separator="," open="(" close=")" index="">
#{id}
</foreach>
</select>
</mapper>
<! -- omit second XML file content is basically the same -->
Copy the code
Database operations encapsulate classes StoryMoneyRepository + TestMoneyRepository
@Repository
public class StoryMoneyRepository {
@Autowired
private StoryMoneyMapper storyMoneyMapper;
public void query(a) {
List<StoryMoneyEntity> list = storyMoneyMapper.findByIds(Arrays.asList(1.1000)); System.out.println(list); }}@Repository
public class TestMoneyRepository {
@Autowired
private TestMoneyMapper testMoneyMapper;
public void query(a) {
List<TestMoneyEntity> list = testMoneyMapper.findByIds(Arrays.asList(1.1000)); System.out.println(list); }}Copy the code
Test 6.
Finally, a simple test is performed to determine whether dynamic data source switching is effective
@SpringBootApplication
public class Application {
public Application(StoryMoneyRepository storyMoneyRepository, TestMoneyRepository testMoneyRepository) {
storyMoneyRepository.query();
testMoneyRepository.query();
}
public static void main(String[] args) { SpringApplication.run(Application.class); }}Copy the code
The following log output is displayed:
6. Summary
This paper presents a dynamic data source based on AbstractRoutingDataSource + AOP implementation of switching implementation approach, using the following three points
AbstractRoutingDataSource
Implement dynamic data source switching- The custom
@DS
Annotation + AOP specifies the Mapper corresponding data source ConfigurationProperties
You can add a data source without modifying the configuration
II. The other
0. Project
Related blog
- 【DB series 】Mybatis multi-data source configuration and use
- 【DB series 】 Multi-data source configuration and use of JdbcTemplate
- 【DB series 】Mybatis-Plus code automatic generation
- 【DB series 】MybatisPlus Integration
- 【DB series 】Mybatis+ annotations integration
- 【DB series 】Mybatis+ XML integration
The source code
- Project: github.com/liuyueyi/sp…
- Source: github.com/liuyueyi/sp…
1. An ashy Blog
As far as the letter is not as good, the above content is purely one’s opinion, due to the limited personal ability, it is inevitable that there are omissions and mistakes, if you find bugs or have better suggestions, welcome criticism and correction, don’t hesitate to appreciate
Below a gray personal blog, record all the study and work of the blog, welcome everyone to go to stroll
- A grey Blog Personal Blog blog.hhui.top
- A Grey Blog-Spring feature Blog Spring.hhui.top
- Wechat official account: One Grey Blog