在日常开发中,基于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()));
}
}