Implementing Multiple Data Sources in Spring Boot with AOP
In this article, we'll explore how to configure a Spring Boot application with multiple database connections using Aspect-Oriented Programming (AOP). We'll use HikariCP as the connection pool, MybatisPlus as the ORM framework, and demonstrate connections to both MySQL and SQL Server databases.
Project Setup
First, let's configure the Maven dependencies in our pom.xml file:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example</groupId>
<artifactId>springboot-multi-datasource</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>application-core</artifactId>
<dependencies>
<!-- Spring Boot Web Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring AOP Support -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- MySQL Driver -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- SQL Server Driver -->
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Mybatis Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.0.5</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Application Configuration
Next, let's configure the application.yml file with our database connections:
server:
port: 9587
spring:
datasource:
hikari:
primary:
driverClassName: com.mysql.cj.jdbc.Driver
jdbcUrl: jdbc:mysql://127.0.0.1:3306/credit_db?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf8
username: root
password: password123
secondary:
driverClassName: com.microsoft.sqlserver.jdbc.SQLServerDriver
jdbcUrl: jdbc:sqlserver://127.0.0.1:1433;DatabaseName=TestDB
username: SA
password: YourStrong@Password123
# Mybatis Plus Configuration
mybatis-plus:
type-aliases-package: com.example.model
mapper-locations: classpath:/mapper/**/*.xml
configuration:
jdbc-type-for-null: null
map-underscore-to-camel-case: true
cache-enabled: false
global-config:
db-config:
id-type: auto
field-strategy: not_empty
Data Source Configuration
Now, let's create the configuration classes for our data sources. First, we'll define constants for our data source names:
package com.example.common.datasource;
public class DataSourceKeys {
public static final String PRIMARY = "primary";
public static final String SECONDARY = "secondary";
}
Next, we'll configure the data sources:
package com.example.common.config;
import com.example.common.datasource.DataSourceKeys;
import com.example.common.datasource.DynamicRoutingDataSource;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
@Configuration
@EnableTransactionManagement
@MapperScan("com.example.**.mapper")
public class DataSourceConfiguration {
@Primary
@Bean(name = "primaryDataSource")
@ConfigurationProperties(prefix = "spring.datasource.hikari.primary")
public DataSource primaryDataSource() {
return DataSourceBuilder.create().build();
}
@Bean(name = "secondaryDataSource")
@ConfigurationProperties(prefix = "spring.datasource.hikari.secondary")
public DataSource secondaryDataSource() {
return DataSourceBuilder.create().build();
}
@Bean(name = "dynamicDataSource")
public DataSource dynamicDataSource() {
DynamicRoutingDataSource dynamicDataSource = new DynamicRoutingDataSource();
Map<Object, Object> dataSourceMap = new HashMap<>(2);
dataSourceMap.put(DataSourceKeys.PRIMARY, primaryDataSource());
dataSourceMap.put(DataSourceKeys.SECONDARY, secondaryDataSource());
// Set primary as default
dynamicDataSource.setDefaultDataSource(primaryDataSource());
dynamicDataSource.setDataSources(dataSourceMap);
return dynamicDataSource;
}
@Bean("sqlSessionFactory")
public SqlSessionFactory sqlSessionFactory() throws Exception {
MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dynamicDataSource());
sqlSessionFactoryBean.setTypeAliasesPackage("com.example.model");
sqlSessionFactoryBean.setMapperLocations(
new PathMatchingResourcePatternResolver()
.getResources("classpath:/mapper/**/*Mapper.xml"));
return sqlSessionFactoryBean.getObject();
}
}
Dynamic Data Source Implementation
Let's create the dynamic data source implementation that extends Spring's AbstractRoutingDataSource:
package com.example.common.datasource;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import java.util.Map;
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getCurrentDataSource();
}
public void setDefaultDataSource(Object defaultDataSource) {
super.setDefaultTargetDataSource(defaultDataSource);
}
public void setDataSources(Map<Object, Object> dataSources) {
super.setTargetDataSources(dataSources);
}
}
Data Source Context
Now, let's create the context holder for managing the current data source:
package com.example.common.datasource;
public class DataSourceContextHolder {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>() {
@Override
protected String initialValue() {
return DataSourceKeys.PRIMARY;
}
};
public static String getCurrentDataSource() {
return contextHolder.get();
}
public static void setCurrentDataSource(String key) {
contextHolder.set(key);
}
public static void clearDataSource() {
contextHolder.remove();
}
}
Custom Annotation for Data Source Selection
Let's create a custom annotation to mark methods that should use a specific data source:
package com.example.common.datasource.annotation;
import java.lang.annotation.*;
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TargetDataSource {
String value() default "";
}
AOP Aspect for Data Source Switching
Now, let's create the AOP aspect that will handle switching data sources based on our custom annotation:
package com.example.common.datasource.aspect;
import com.example.common.datasource.DataSourceKeys;
import com.example.common.datasource.DataSourceContextHolder;
import com.example.common.datasource.annotation.TargetDataSource;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
@Aspect
@Component
@Slf4j
@Order(-1)
public class DataSourceAspect {
@Pointcut("@annotation(com.example.common.datasource.annotation.TargetDataSource)")
public void dataSourcePointCut() {}
@Around("dataSourcePointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
TargetDataSource targetDataSource = method.getAnnotation(TargetDataSource.class);
if (targetDataSource == null) {
DataSourceContextHolder.setCurrentDataSource(DataSourceKeys.PRIMARY);
log.info("Using default data source: {}", DataSourceKeys.PRIMARY);
} else {
String dataSourceKey = targetDataSource.value();
DataSourceContextHolder.setCurrentDataSource(dataSourceKey);
log.info("Switched to data source: {} in method: {}", dataSourceKey, method.getName());
}
try {
return point.proceed();
} finally {
DataSourceContextHolder.clearDataSource();
log.info("Restored to default data source after method: {}", method.getName());
}
}
}
Service Implementation Examples
Let's look at examples of how to use these configurations in our service layer. First, a service that uses the default data source (MySQL):
package com.example.service.impl;
import com.example.model.AddressBook;
import com.example.persistence.mapper.AddressBookMapper;
import com.example.service.AddressBookService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class AddressBookServiceImpl implements AddressBookService {
@Autowired
private AddressBookMapper addressBookMapper;
@Override
public AddressBook queryById(Integer bookId) {
return addressBookMapper.queryById(bookId);
}
}
And now a service that explicitly uses the secondary data source (SQL Server):
package com.example.service.impl;
import com.example.common.datasource.DataSourceKeys;
import com.example.common.datasource.annotation.TargetDataSource;
import com.example.model.Inventory;
import com.example.persistence.mapper.InventoryMapper;
import com.example.service.InventoryService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class InventoryServiceImpl implements InventoryService {
@Autowired
private InventoryMapper inventoryMapper;
@Override
@TargetDataSource(value = DataSourceKeys.SECONDARY)
public Inventory queryById(Integer id) {
return inventoryMapper.queryById(id);
}
}
Common Issues and Solutions
When implementing multiple data sources in Spring Boot, you might encounter several challenges:
- Circular Dependencies: When using @Primary annotation, ensure it's applied to the default data source bean, not the dynamic data source bean.
- ORM Configuration: Make sure to use the correct SqlSessionFactory implementation. For MybatisPlus, use MybatisSqlSessionFactoryBean to ensure all configurations are properly applied.
- Transaction Management: Ensure your AOP aspect has a lower order value then @Transactional to guarantee proper execution sequence.
By following these patterns, you can effectively implement a multi-datasource architecture in your Spring Boot application, allowing seamless switching between different database connections as needed.