springboot integrates the configuration of multiple data sources and dynamically switches data sources, and annotates switching data sources

In many applications, it may be necessary to use multiple databases or data sources to handle different business needs. Spring Boot provides an easy way to configure and use multiple data sources, enabling developers to easily handle multiple database connections. If you may need to switch data sources at any time in your project, then my article may help you

: I won’t say much about the introduction of coordinates in the pom file here

Configuration file

1: in the properties file

# data source configuration
spring.datasource.mysql.primary.url=jdbc:mysql://127.0.0.1:3351/tally_book?characterEncoding=utf8 &serverTimezone=UTC
spring.datasource.mysql.primary.username=root
spring.datasource.mysql.primary.password=123456
spring.datasource.mysql.primary.driver-class-name=com.mysql.cj.jdbc.Driver

# Data source configuration
spring.datasource.mysql.slave1.url=jdbc:mysql://127.0.0.1:3351/dingding_mid?characterEncoding=utf8 &serverTimezone=UTC
spring.datasource.mysql.slave1.username=root
spring.datasource.mysql.slave1.password=123456
spring.datasource.mysql.slave1.driver-class-name=com.mysql.cj.jdbc.Driver

In the above configuration file, I only wrote two sources, and they are both mysql, primary and slave1 are the distinction

2: The configuration class implements multi-data source configuration

package com.todoitbo.tallybookdasmart.config;

import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import com.todoitbo.tallybookdasmart.multiDataSource.DataSourceType;
import com.todoitbo.tallybookdasmart.multiDataSource.DynamicDataSource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.*;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

/**
 * @author xiaobo
 * @date 2023/5/19
 */
@Configuration
@Slf4j
public class MultiDataSourceConfig {<!-- -->

    @Bean
    public PlatformTransactionManager platformTransactionManager(DataSource dynamicDataSource) {<!-- -->
        return new DataSourceTransactionManager(dynamicDataSource);
    }

    @Bean
    @Primary
    @DependsOn("primaryDataSource")
    public DataSource dynamicDataSource(@Qualifier(DataSourceType. PRIMARY) DataSource primaryDataSource,
                                        @Qualifier(DataSourceType. SECOND) DataSource secondDataSource) {<!-- -->
        DynamicDataSource dynamicDataSource = new DynamicDataSource();

        // 1. Set the default data source
        dynamicDataSource.setDefaultTargetDataSource(primaryDataSource);
        // 2. Configure multiple data sources
        Map<Object, Object> map = new HashMap<>();
        map.put(DataSourceType.PRIMARY, primaryDataSource);
        map.put(DataSourceType.SECOND, secondDataSource);
        // 3. Store the data source set
        dynamicDataSource.setTargetDataSources(map);
        return dynamicDataSource;
    }

    @Bean(name = DataSourceType. PRIMARY)
    @ConfigurationProperties(prefix = "spring.datasource.mysql.primary")
    public DataSource primaryDataSource() {<!-- -->
        log.info("The main database connection pool is being created......");
        return DruidDataSourceBuilder.create().build();
    }

    @Bean(name = DataSourceType. SECOND)
    @ConfigurationProperties(prefix = "spring.datasource.mysql.slave1")
    public DataSource secondDataSource() {<!-- -->
        log.info("second database connection pool creation......");
        return DruidDataSourceBuilder.create().build();
    }

}

3: Custom annotation implementation, you can use custom annotations to switch data sources

package com.todoitbo.tallybookdasmart.multiDataSource;

import java.lang.annotation.*;

/**
 * description: custom annotation, mark data source
 *
 * @author bo
 * @version 1.0
 * @date 2023/5/19 08:45
 */
@Retention(RetentionPolicy. RUNTIME)
@Target(ElementType. METHOD)
@Documented
public @interface DataSource {<!-- -->
    String value() default DataSourceType.PRIMARY;
}

4: Define an aspect class

This code is an aspect class DataSourceAspect, which is used to switch the data source before and after the method call. Here is an explanation of the code:

  1. @Aspect: Specify this class as an aspect class, which is used to define the entry point of the aspect and enhance the logic.
  2. @Order(value=1): Specify the execution order of the aspect, the smaller the value, the higher the priority.
  3. @Component: Declare the aspect class as a Spring component, so that it can be automatically scanned and assembled into the Spring container.
  4. @Pointcut(value = "execution(* com.todoitbo.tallybookdasmart.service.*.*(..)) || execution(* com.todoitbo.tallybookdasmart.*.*(..))" ): Define the pointcut expression and specify the target method that needs to be cut in.
  5. @Around("dataSourcePointCut()"): defines the surround advice, which means that the aspect logic is executed before and after the execution of the target method.
  6. public Object around(ProceedingJoinPoint joinPoint) throws Throwable: surround notification method, including aspect logic.
  7. Obtain the annotation information of the target method through reflection in the method, determine whether there is a @DataSource annotation, and obtain the data source name set in the annotation.
  8. Call the DataSourceContextHolder.setDataSource(dataSource) method to set the obtained data source name to the context of the current thread.
  9. Call the joinPoint.proceed() method to continue executing the target method.
  10. Call the DataSourceContextHolder.clearDataSourceType() method in the finally block to clear the data source information stored in the current thread.
package com.todoitbo.tallybookdasmart.multiDataSource;

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;


/**
 * @author xiaobo
 */
@Aspect
@Order(value=1)
@Component
@Slf4j
public class DataSourceAspect {<!-- -->

    /** Define pointcut expression */
    @Pointcut(value = "execution(* com.todoitbo.tallybookdasmart.service.*.*(..)) || execution(* com.todoitbo.tallybookdasmart.*.*(..))")
    public void dataSourcePointCut() {<!-- -->
    }
    @Around("dataSourcePointCut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {<!-- -->
        Object target = joinPoint. getTarget();
        String method = joinPoint. getSignature(). getName();
        Class<?> classz = target. getClass();
        Class<?>[] parameterTypes = ((MethodSignature) joinPoint. getSignature()). getMethod(). getParameterTypes();
        try {<!-- -->
            // Use reflection to get the method object of the specified method name and parameter type in the target class
            Method m = classz. getMethod(method, parameterTypes);
            // Set the default data source name
            String dataSource = DataSourceType. PRIMARY;
            // Determine whether the method is marked with the @DataSource annotation.
            if (m.isAnnotationPresent(DataSource.class)) {<!-- -->
                // Obtain the @DataSource annotation object on the method through the getAnnotation() method.
                DataSource ds = m. getAnnotation(DataSource. class);
                // Get the data source name set in the annotation object
                dataSource = ds. value();
            }
            // Set the obtained data source name to the context of the current thread
            DataSourceContextHolder.setDataSource(dataSource);
            // Continue to execute the target method
            return joinPoint. proceed();
        } finally {<!-- -->
            DataSourceContextHolder. clearDataSourceType();
        }
    }

}

5: The context tool class for storing and obtaining the data source of the current thread

This code is a context tool class for storing and getting the data source of the current thread. It uses Netty’s FastThreadLocal to achieve thread-local fast access.

  1. Create FastThreadLocal object: A FastThreadLocal object named CONTEXT_HOLDER is defined in the class to store the data source information of the current thread.
  2. Set data source: The setDataSource method is used to set the data source name to the context of the current thread. Store the data source name in the current thread by calling CONTEXT_HOLDER.set(dataSource).
  3. Get Data Source: The getDataSource method is used to get the data source name from the context of the current thread. By calling CONTEXT_HOLDER.get(), you can get the data source name of the current thread.
  4. Clear data source: The clearDataSourceType method is used to clear the data source information stored in the current thread. By calling CONTEXT_HOLDER.remove(), the data source information in the current thread can be cleared.
package com.todoitbo.tallybookdasmart.multiDataSource;

import io.netty.util.concurrent.FastThreadLocal;

/**
 * description: The context tool class for storing and obtaining the data source of the current thread
 *
 * @author bo
 * @version 1.0
 * @date 2023/5/19 08:44
 */
public class DataSourceContextHolder {<!-- -->

    /**
     * Create a FastThreadLocal object to store the data source information of the current thread
     */
    private static final FastThreadLocal<String> CONTEXT_HOLDER = new FastThreadLocal<String>();

    /**
     * Set the data source
     */
    public static void setDataSource(String dataSource) {<!-- -->
        CONTEXT_HOLDER.set(dataSource);
    }

    /**
     * Get data source
     */
    public static String getDataSource() {<!-- -->
        return CONTEXT_HOLDER. get();
    }

    /**
     * Clear data source
     */
    public static void clearDataSourceType() {<!-- -->
        CONTEXT_HOLDER. remove();
    }

}

6: data source type

package com.todoitbo.tallybookdasmart.multiDataSource;


/**
 * @author xiaobo
 */
public class DataSourceType {<!-- -->
    public static final String PRIMARY = "primaryDataSource";
    public static final String SECOND = "secondDataSource";
}

7: Get the corresponding data source according to the data source context in the current thread

package com.todoitbo.tallybookdasmart.multiDataSource;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

/**
 * description: Get the corresponding data source according to the data source context in the current thread.
 *
 * @author bo
 * @version 1.0
 * @date 2023/5/19 08:46
 */
public class DynamicDataSource extends AbstractRoutingDataSource {<!-- -->

    @Override
    protected Object determineCurrentLookupKey() {<!-- -->
        return DataSourceContextHolder. getDataSource();
    }

}

Concrete implementation

Just add annotations to the method of the service implementation class

package com.todoitbo.tallybookdasmart.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.todoitbo.tallybookdasmart.entity.TbConfig;
import com.todoitbo.tallybookdasmart.mapper.TbConfigMapper;
import com.todoitbo.tallybookdasmart.multiDataSource.DataSource;
import com.todoitbo.tallybookdasmart.multiDataSource.DataSourceType;
import com.todoitbo.tallybookdasmart.service.ITbConfigService;
import com.todoitbo.tallybookdasmart.service.base.BaseServiceImpl;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.List;


/**
 * (TbConfig) service
 *
 * @author bo
 * @since 2023-04-18 21:13:14
 */
@Service
public class TbConfigServiceImpl extends BaseServiceImpl<TbConfigMapper,TbConfig> implements ITbConfigService {<!-- -->

    @Resource
    protected TbConfigMapper mapper;

    @Override
    @DataSource(DataSourceType. SECOND)
    public List<TbConfig> testList() {<!-- -->
        return mapper. selectList(new QueryWrapper<>());
    }
}

Renderings:

image-20230522084706414

: This is just to show that he is indeed gone from the data source