Implementing Multiple Data Sources in Spring Boot with AOP

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:

  1. Circular Dependencies: When using @Primary annotation, ensure it's applied to the default data source bean, not the dynamic data source bean.
  2. ORM Configuration: Make sure to use the correct SqlSessionFactory implementation. For MybatisPlus, use MybatisSqlSessionFactoryBean to ensure all configurations are properly applied.
  3. 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.

Tags: spring-boot aop HikariCP mybatisplus MySQL

Posted on Sat, 16 May 2026 17:23:36 +0000 by mk_silence