在日常开发中,基于springboot的开发已经越来越常见,而有些重复功能的开发对于框架搭建选型的过程是繁琐且麻烦的,而springboot提供的spi机制为我们提供了很大的便利,下面我们以简单的多数据源做一个spring-boot-starter并截出搭建过程。

技术选型:先贴一下pom文件

<?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>
​
    <groupId>com.shiyebushihua</groupId>
    <artifactId>mybatis-dynamicdatasource-boot-starter</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <name>mybatis-dynamicdatasource-boot-starter</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <packaging>jar</packaging>
​
    <parent>
        <artifactId>spring-boot-starter-parent</artifactId>
        <groupId>org.springframework.boot</groupId>
        <version>2.3.5.RELEASE</version>
    </parent>
​
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-autoconfigure</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.mybatis.spring.boot/mybatis-spring-boot-starter -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.4</version>
        </dependency>
​
        <dependency>
            <groupId>commons-beanutils</groupId>
            <artifactId>commons-beanutils</artifactId>
            <version>1.9.4</version>
        </dependency>
        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
            <version>2.6</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.75</version>
        </dependency>
    </dependencies>
​
    <build>
        <finalName>mybatis-dynamicdatasource-boot-starter</finalName>
        <resources>
            <resource>
                <directory>src/main/resources</directory>
                <filtering>true</filtering>
                <includes>
                    <include>**/**</include>
                </includes>
            </resource>
        </resources>
        <testResources>
            <testResource>
                <directory>src/test/resources</directory>
                <filtering>true</filtering>
                <includes>
                    <include>**/**</include>
                </includes>
            </testResource>
        </testResources>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.12.4</version>
                <configuration>
                    <skipTests>true</skipTests>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-resources-plugin</artifactId>
                <version>2.5</version>
                <configuration>
                    <encoding>${project.build.sourceEncoding}</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>2.3.2</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>${project.build.sourceEncoding}</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-source-plugin</artifactId>
                <version>2.1.2</version>
                <executions>
                    <execution>
                        <id>attach-sources</id>
                        <goals>
                            <goal>jar</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
​
</project>
​

这里简单的引用了几个工具包以及springboot依赖,aop。

开始搭建:

考虑用注解的形式进行数据源切换,这样比较灵活。

package com.shiyebushihua.mybatis.dynamicdatasource.boot.annotation;
​
import java.lang.annotation.*;
​
/**
 * @author wangjianhua
 * @Description 用于切换数据源的注解
 * @date 2021/12/31/031 10:21
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD,ElementType.TYPE})
public @interface Datasource {
​
    /**
     * 数据源名字
     * @return
     */
    String name() default "";
}
​

使用常量维护配置文件的key值:

package com.shiyebushihua.mybatis.dynamicdatasource.boot.constants;
​
/**
 * @author wangjianhua
 * @Description 数据源常量维护
 * @date 2021/12/31/031 10:23
 */
public class DatasourceConstants {
​
    /**
     * 统一的前缀 便于文件读取并配置
     */
    public static final String PREFIX= "db.jdbc.datasource.";
​
​
    /**
     * 分割符号
     */
    public static final String LIST_SPLIT= ",";
​
    /**
     * 默认数据源
     */
    public static final String DEFAULT= "default";
​
    /**
     * jdbc所需元素
     */
    public static final String URL = "url";
​
    public static final String USERNAME = "username";
​
    public static final String PASSWORD = "password";
}
​

接下来写一个环境属性工具类,这个工具类是干啥的,简单来说就是屏蔽springboot1代和二代的区别,这样可以做一下兼容,虽然说我们现在基本是2往后了。

package com.shiyebushihua.mybatis.dynamicdatasource.boot.util;
​
import org.springframework.core.env.Environment;
import org.springframework.core.env.PropertyResolver;
​
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
​
/**
 * @author wangjianhua
 * @Description 环境属性工具类
 * @date 2021/12/31/031 10:26
 */
public class PropertyUtil {
​
    private static int springBootVersion = 1;
​
    static {
        try
        {
            Class.forName("org.springframework.boot.bind.RelaxedPropertyResolver");
        }
        catch (ClassNotFoundException e)
        {
            springBootVersion = 2;
        }
    }
​
    /**
     * Spring Boot 1.x is compatible with Spring Boot 2.x by Using Java Reflect.
     * @param environment : the environment context
     * @param prefix : the prefix part of property key
     * @param targetClass : the target class type of result
     * @param <T> : refer to @param targetClass
     * @return T
     */
    @SuppressWarnings("unchecked")
    public static <T> T handle(final Environment environment, final String prefix, final Class<T> targetClass) {
        switch (springBootVersion) {
            case 1:
                return (T) v1(environment, prefix);
            default:
                return (T) v2(environment, prefix, targetClass);
        }
    }
​
    private static Object v1(final Environment environment, final String prefix) {
        try {
            Class<?> resolverClass = Class.forName("org.springframework.boot.bind.RelaxedPropertyResolver");
            Constructor<?> resolverConstructor = resolverClass.getDeclaredConstructor(PropertyResolver.class);
            Method getSubPropertiesMethod = resolverClass.getDeclaredMethod("getSubProperties", String.class);
            Object resolverObject = resolverConstructor.newInstance(environment);
            String prefixParam = prefix.endsWith(".") ? prefix : prefix + ".";
            return getSubPropertiesMethod.invoke(resolverObject, prefixParam);
        } catch (final ClassNotFoundException | NoSuchMethodException | SecurityException | InstantiationException
                | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
            throw new RuntimeException(ex.getMessage(), ex);
        }
    }
​
    private static Object v2(final Environment environment, final String prefix, final Class<?> targetClass) {
        try {
            Class<?> binderClass = Class.forName("org.springframework.boot.context.properties.bind.Binder");
            Method getMethod = binderClass.getDeclaredMethod("get", Environment.class);
            Method bindMethod = binderClass.getDeclaredMethod("bind", String.class, Class.class);
            Object binderObject = getMethod.invoke(null, environment);
            String prefixParam = prefix.endsWith(".") ? prefix.substring(0, prefix.length() - 1) : prefix;
            Object bindResultObject = bindMethod.invoke(binderObject, prefixParam, targetClass);
            Method resultGetMethod = bindResultObject.getClass().getDeclaredMethod("get");
            return resultGetMethod.invoke(bindResultObject);
        } catch (final ClassNotFoundException | NoSuchMethodException | SecurityException | IllegalAccessException
                | IllegalArgumentException | InvocationTargetException ex) {
            throw new RuntimeException(ex.getMessage(), ex);
        }
    }
}
​

为了保护线程私有的变量,这里使用TheadLocal类进行数据源的动态存取。

package com.shiyebushihua.mybatis.dynamicdatasource.boot.dynamic;
​
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
​
/**
 * @author wangjianhua
 * @Description 动态存取数据源 用于切换
 * @date 2021/12/31/031 10:30
 */
public class DynamicDatasourceContextHolder {
​
    private static final Logger logger= LoggerFactory.getLogger(DynamicDatasourceContextHolder.class);
​
​
    /**
     * 给定一个主数据源进行初始化
     */
    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
​
​
    /**
     * 根据名称设置数据源
     *
     * @param key 数据源名称
     */
    public static void setDataSourceKey(String key) {
        CONTEXT_HOLDER.set(key);
    }
​
​
    /**
     * 获得正在使用的数据源
     * @return  数据源名字
     */
    public static String getDataSourceKey(){
        return CONTEXT_HOLDER.get();
    }
​
    /**
     * 清空数据源
     */
    public static void clearDataSourceKey(){
        logger.info("查询完毕 清空数据源====");
        CONTEXT_HOLDER.remove();
    }
}
​

同时我们要注意,在使用完毕后,一定要进行TheadLocal.remove()防止内存泄漏。

Spring boot提供了AbstractRoutingDataSource 根据用户定义的规则选择当前的数据源,这样我们可以在执行查询之前,设置使用的数据源。实现可动态路由的数据源,在每次数据库查询操作前执行。它的抽象方法 determineCurrentLookupKey() 决定使用哪个数据源。

package com.shiyebushihua.mybatis.dynamicdatasource.boot.dynamic;
​
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
​
/**
 * @author wangjianhua
 * @Description
 * @date 2021/12/31/031 10:36
 */
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
​
    private final Logger logger = LoggerFactory.getLogger(DynamicRoutingDataSource.class);
​
    @Override
    protected Object determineCurrentLookupKey() {
        logger.debug("目前使用的数据源为:{}",DynamicDatasourceContextHolder.getDataSourceKey());
        return DynamicDatasourceContextHolder.getDataSourceKey();
    }
}
​

接下来写一个aop

package com.shiyebushihua.mybatis.dynamicdatasource.boot.aop;
​
import com.shiyebushihua.mybatis.dynamicdatasource.boot.annotation.Datasource;
import com.shiyebushihua.mybatis.dynamicdatasource.boot.dynamic.DynamicDatasourceContextHolder;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;
​
import java.lang.reflect.Method;
​
/**
 * @author wangjianhua
 * @Description
 * @date 2021/12/31/031 10:46
 */
@Aspect
public class DatasourceAspect {
​
    private static final Logger logger = LoggerFactory.getLogger(DatasourceAspect.class);
​
​
    @Pointcut("@annotation(com.shiyebushihua.mybatis.dynamicdatasource.boot.annotation.Datasource)")
    public void pointCut(){
    }
​
    @Before("pointCut()")
    public void before(JoinPoint point) throws Throwable{
        logger.info("进入注解面");
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        Datasource dataSource = method.getAnnotation(Datasource.class);
        if(!StringUtils.isEmpty( dataSource.name())){
            logger.info("设置为{}数据源",dataSource.name());
            DynamicDatasourceContextHolder.setDataSourceKey(dataSource.name());
            logger.debug("选择数据源到 [{}] ]",
                    DynamicDatasourceContextHolder.getDataSourceKey());
        }
​
​
    }
​
    @After("pointCut()")
    public void after(JoinPoint point){
        DynamicDatasourceContextHolder.clearDataSourceKey();
        logger.debug("重置数据源 [{}] 在方法 [{}]中",DynamicDatasourceContextHolder.getDataSourceKey(),point.getSignature());
​
    }
}
​

写一个配置类,该配置类会在项目被引入后利用springboot的spi机制自动加载。

package com.shiyebushihua.mybatis.dynamicdatasource.boot.config;
​
import com.shiyebushihua.mybatis.dynamicdatasource.boot.aop.DatasourceAspect;
import com.shiyebushihua.mybatis.dynamicdatasource.boot.constants.DatasourceConstants;
import com.shiyebushihua.mybatis.dynamicdatasource.boot.dynamic.DynamicRoutingDataSource;
import com.shiyebushihua.mybatis.dynamicdatasource.boot.util.PropertyUtil;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.transaction.support.TransactionTemplate;
​
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
​
/**
 * @author wangjianhua
 * @Description 自动配置的数据源
 * @date 2021/12/31/031 10:39
 */
@Configuration
public class DatasourceAutoConfig implements EnvironmentAware {
​
    /**
     * 数据源配置组
     */
    private Map<String,Map<String,Object>> dataSourceMap = new HashMap<>();
​
    /**
     * 默认的数据源配置
     */
    private Map<String,Object> defaultDataSourceConfig;
​
    @Bean(name = "datasourceAspect")
    @ConditionalOnMissingBean
    public DatasourceAspect point(){
        return new DatasourceAspect();
    }
​
​
    @Bean
    public DataSource dataSource(){
        //创建数据源
        Map<Object,Object> targetDataSources = new HashMap<>();
        for (String dbInfo : dataSourceMap.keySet()) {
            Map<String, Object> objMap = dataSourceMap.get(dbInfo);
            targetDataSources.put(dbInfo,new DriverManagerDataSource(
                    objMap.get(DatasourceConstants.URL).toString(),
                    objMap.get(DatasourceConstants.USERNAME).toString()
                    ,objMap.get(DatasourceConstants.PASSWORD).toString()));
        }
​
        //设置数据源
        DynamicRoutingDataSource dynamicDataSource = new DynamicRoutingDataSource();
        dynamicDataSource.setTargetDataSources(targetDataSources);
        dynamicDataSource.setDefaultTargetDataSource(new DriverManagerDataSource(
                defaultDataSourceConfig.get(DatasourceConstants.URL).toString(),
                defaultDataSourceConfig.get(DatasourceConstants.USERNAME).toString()
                ,defaultDataSourceConfig.get(DatasourceConstants.PASSWORD).toString()));
​
        return dynamicDataSource;
    }
​
    @Bean
    public TransactionTemplate transactionTemplate(DataSource dataSource){
        DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
        dataSourceTransactionManager.setDataSource(dataSource);
​
        TransactionTemplate transactionTemplate = new TransactionTemplate();
        transactionTemplate.setTransactionManager(dataSourceTransactionManager);
        transactionTemplate.setPropagationBehaviorName("PROPAGATION_REQUIRED");
        return transactionTemplate;
    }
​
    @Override
    public void setEnvironment(Environment environment) {
        //多数据源
        String dataSources = environment.getProperty(DatasourceConstants.PREFIX+DatasourceConstants.LIST);
        assert dataSources != null;
        for (String dbInfo : dataSources.split(DatasourceConstants.LIST_SPLIT)) {
            Map<String,Object> dataSourceProps = PropertyUtil.handle(environment,DatasourceConstants.PREFIX+dbInfo,Map.class);
            dataSourceMap.put(dbInfo,dataSourceProps);
        }
​
        //默认数据源
        String defaultData = environment.getProperty(DatasourceConstants.PREFIX+DatasourceConstants.DEFAULT);
        defaultDataSourceConfig = PropertyUtil.handle(environment,DatasourceConstants.PREFIX+defaultData,Map.class);
    }
}
​

在resources目录下新建目录META-INF,该目录下新建spring.factories文件

添加以下内容:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.shiyebushihua.mybatis.dynamicdatasource.boot.config.DatasourceAutoConfig

接下来使用maven的install插件,将自己写的starter工具安装到你的maven仓库里面。

这样就可以测试使用了。

附测试使用的yml配置文件与单元测试类以及dao

server:
  port: 8111
db:
  jdbc:
    datasource:
      default: mysql
      list: oracle
      mysql:
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3306/mybatistest?useUnicode=true&characterEncoding=utf8
        username: root
        password: 123456
      oracle:
        driver-class-name: oracle.jdbc.driver.OracleDriver
        url: jdbc:oracle:thin:@localhost:1521:test
        username: root
        password: 123456
mybatis:
  mapper-locations: classpath:/mybatis/mapper/*.xml
package com.shiyebushihua.testdynamic.test;
​
import com.shiyebushihua.mybatis.dynamicdatasource.boot.annotation.Datasource;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
​
import java.util.List;
import java.util.Map;
​
/**
 * @author wangjianhua
 * @Description
 * @date 2021/12/31/031 11:08
 */
@Mapper
public interface TestDao {
​
​
    @Select("select * from user")
    List<Map<String,Object>> getUserfromMysql();
​
    @Datasource(name = "oracle")
    @Select("SELECT * FROM ELE_USER")
    List<Map<String,Object>> getUserFromOracle();
}
​
package com.shiyebushihua.testdynamic;
​
import com.alibaba.fastjson.JSON;
import com.shiyebushihua.testdynamic.test.TestDao;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.test.context.SpringBootTest;
​
import javax.annotation.Resource;
​
@SpringBootTest
class TestdynamicApplicationTests {
​
​
    private Logger logger = LoggerFactory.getLogger(TestdynamicApplicationTests.class);
    @Resource
    private TestDao testDao;
​
    @Test
    void contextLoads() {
        logger.info("mysql user表信息:{}",JSON.toJSONString(testDao.getUserfromMysql()));
        logger.info("oracle USER:{}",JSON.toJSONString(testDao.getUserFromOracle()));
    }
​
}