1.项目依赖
<packaging>jar</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.5.RELEASE</version>
<relativePath/>
</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>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.34</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-test</artifactId>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-test -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<scope>test</scope>
</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>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<finalName>db-router-spring-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>
2. 创建一个路由的配置类
分库分表字段
/**
* 分库数量
*/
private int dbCount;
/**
* 分表数量
*/
private int tbCount;
/**
* 路由字段
*/
private String routerKey;
然后用策略模式,可拓展后续
接下来我应该还需要一种策略,这个策略关系到了数据会分到哪个库哪个表里面,先来一个接口,用来规范所有的策略里应该有哪些方法。
/**
* 路由计算
* @param dbKeyAttr 路由字段
*/
void doRouter(String dbKeyAttr);
/**
* 手动设置分库路由
* @param dbIdx 路由库,需要在配置范围内
*/
void setDBKey(int dbIdx);
/**
* 手动设置分表路由
*
* @param tbIdx 路由表,需要在配置范围内
*/
void setTBKey(int tbIdx);
/**
* 获取分库数
*
* @return 数量
*/
int dbCount();
/**
* 获取分表数
*
* @return 数量
*/
int tbCount();
/**
* 清除路由
*/
void clear();
## 实现默认一种策略(哈希路由策略)
public class DBRouterStrategyHashCode implements IDBRouterStrategy {
private Logger logger = LoggerFactory.getLogger(DBRouterStrategyHashCode.class);
private DBRouterConfig dbRouterConfig;
public DBRouterStrategyHashCode(DBRouterConfig dbRouterConfig) {
this.dbRouterConfig = dbRouterConfig;
}
@Override
public void doRouter(String dbKeyAttr) {
int size = dbRouterConfig.getDbCount() * dbRouterConfig.getTbCount();
//扰动函数
int idx = (size - 1) & (dbKeyAttr.hashCode() ^ (dbKeyAttr.hashCode() >>> 16));
// 库表索引;相当于是把一个长条的桶,切割成段,对应分库分表中的库编号和表编号
int dbIdx = idx / dbRouterConfig.getTbCount() + 1;
int tbIdx = idx - dbRouterConfig.getTbCount() * (dbIdx - 1);
DBContextHolder.setDbKey(String.format("%02d",dbIdx));
DBContextHolder.setTbKey(String.format("%02d",tbIdx));
logger.debug("数据库路由 dbIdx:{} tbIdx:{}", dbIdx, tbIdx);
}
@Override
public void setDBKey(int dbIdx) {
DBContextHolder.setDbKey(String.format("%02d",dbIdx));
}
@Override
public void setTBKey(int tbIdx) {
DBContextHolder.setTbKey(String.format("%02d",tbIdx));
}
@Override
public int dbCount() {
return dbRouterConfig.getDbCount();
}
@Override
public int tbCount() {
return dbRouterConfig.getTbCount();
}
@Override
public void clear() {
DBContextHolder.clearDBKey();
DBContextHolder.clearTBKey();
}
}
其中有一个DBhandel 里面是ThreadLocal<String> 以及一些方法
package com.monica.db.router.routeBase;
/**
* @Author monica
* @Date 2023/3/13 17:44
* @description 1.0
*/
public class DBContextHolder {
private static final ThreadLocal<String> dbKey = new ThreadLocal<String>();
private static final ThreadLocal<String> tbkey = new ThreadLocal<String>();
public static void setDbKey(String dbKeyIdx){
dbKey.set(dbKeyIdx);
}
public static String getDBKey(){
return dbKey.get();
}
public static void setTbKey(String tbKeyIdx){
dbKey.set(tbKeyIdx);
}
public static String getTBKey(){
return tbkey.get();
}
public static void clearDBKey(){
dbKey.remove();
}
public static void clearTBKey(){
tbkey.remove();
}
}
如何让注解生效呢?
我在上面单纯的创建了一个注解类,把他标记到了方法,这样运行而来并没有什么用。如何让他生效呢?这里就需要引入Spring中AOP相关的模块。
package com.monica.db.router.routeBase;
import com.monica.db.router.annotatiom.DBRouter;
import com.monica.db.router.strategy.impl.IDBRouterStrategy;
import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.lang.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* @Author monica
* @Date 2023/3/13 18:03
* @description 1.0 据路由切面,通过自定义注解的方式,拦截被切面的方法,进行数据库路由
*/
@Aspect
public class DBRouterJoinPoint {
private Logger logger = LoggerFactory.getLogger(DBRouterJoinPoint.class);
private DBRouterConfig dbRouterConfig;
private IDBRouterStrategy idbRouterStrategy;
public DBRouterJoinPoint(DBRouterConfig dbRouterConfig, IDBRouterStrategy idbRouterStrategy) {
this.dbRouterConfig = dbRouterConfig;
this.idbRouterStrategy = idbRouterStrategy;
}
@Pointcut("@annotation(com.monica.db.router.annotatiom.DBRouter)")
public void aopPoint(){
}
/**
* 所有需要分库分表的操作,都需要使用自定义注解进行拦截,拦截后读取方法中的入参字段,根据字段进行路由操作。
* 1. dbRouter.key() 确定根据哪个字段进行路由
* 2. getAttrValue 根据数据库路由字段,从入参中读取出对应的值。比如路由 key 是 uId,那么就从入参对象 Obj 中获取到 uId 的值。
* 3. dbRouterStrategy.doRouter(dbKeyAttr) 路由策略根据具体的路由值进行处理
* 4. 路由处理完成比,就是放行。 jp.proceed();
* 5. 最后 dbRouterStrategy 需要执行 clear 因为这里用到了 ThreadLocal 需要手动清空。关于 ThreadLocal 内存泄漏介绍 https://t.zsxq.com/027QF2fae
*/
@Around("aopPoint() && @annotation(dbRouter)")
public Object doRouter(ProceedingJoinPoint point , DBRouter dbRouter){
String dbKey = dbRouter.key();
if (StringUtils.isBlank(dbKey) && StringUtils.isBlank(dbRouterConfig.getRouterKey())){
throw new RuntimeException("annotation DBRouter key is null");
}
dbKey = StringUtils.isNotBlank(dbKey) ? dbKey : dbRouterConfig.getRouterKey();
// 路由属性
String dbKeyAttr = getAttrValue(dbKey,point.getArgs());
//路由策略
idbRouterStrategy.doRouter(dbKeyAttr);
}
public Method getMethod(JoinPoint jp) throws NoSuchMethodException {
Signature sig = jp.getSignature();
MethodSignature methodSignature = (MethodSignature) sig;
return jp.getTarget().getClass().getMethod(methodSignature.getName(),methodSignature.getParameterTypes());
}
private String getAttrValue(String dbKey, Object[] args) {
if (1 == args.length){
Object arg = args[0];
if (arg instanceof String){
return args.toString();
}
}
String filedValue = null;
for (Object arg : args) {
try {
if (StringUtils.isNotBlank(filedValue)){
break;
}
filedValue = BeanUtils.getProperty(arg,dbKey);
} catch (Exception e) {
logger.error("获取路由属性失败 attr :{}",dbKey,e);
}
}
return filedValue;
}
}
加载配置文件中的配置
现在我们这个组件的进度已经完成了核心的代码,在这些核心代码使用的前提是需要将配置文件中的配置加载到内存中,这样在计算出哪个库哪个表后,我们才可以把数据插入到数据库里面。
Tip:配置文件的格式已经在上面列了出来。在看这节的时候大家可以参考着配置文件的格式来看这节内容。
到了这里加载配置文件的时候,我现在遇到了一个问题,如何在SpringBoot项目启动成功时将配置文件中组件的配置加载到内存呢?
解决这个问题,我们可以通过SpringBoot给我们提供的扩展点来实现。在SpringBoot中有一个EnvironmentAware接口,当一个让Spring来管理的Bean实现了这个接口的时候,在SpringBoot启动成功时,便会回调这个类中的setEnvironment方法,在这个方法的environment参数中便可以获取到配置文件的信息。所以,遇到的这个问题可以通过这种方式解决。
package com.monica.db.router.config;
import com.monica.db.router.dynamic.DynamicDataSource;
import com.monica.db.router.dynamic.DynamicMybatisPlugin;
import com.monica.db.router.routeBase.DBRouterConfig;
import com.monica.db.router.routeBase.DBRouterJoinPoint;
import com.monica.db.router.strategy.DBRouterStrategyHashCode;
import com.monica.db.router.strategy.impl.IDBRouterStrategy;
import com.monica.db.router.util.PropertyUtil;
import org.apache.ibatis.plugin.Interceptor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.OnPropertyListCondition;
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 org.springframework.util.Assert;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
/**
* @Author monica
* @Date 2023/3/13 18:32
* @description 1.0
*/
@Configuration
public class DataSourceAutoConfig implements EnvironmentAware {
/**
* 数据源配置组
*/
private Map<String, Map<String, Object>> dataSourceMap = new HashMap<>();
/**
* 默认数据源配置
*/
private Map<String, Object> defaultDataSourceConfig;
/**
* 分库数量
*/
private int dbCount;
/**
* 分表数量
*/
private int tbCount;
/**
* 路由字段
*/
private String routerKey;
@Bean(name = "db-router-point")
@ConditionalOnMissingBean
public DBRouterJoinPoint point(DBRouterConfig dbRouterConfig, IDBRouterStrategy routerStrategy){
return new DBRouterJoinPoint(dbRouterConfig,routerStrategy);
}
@Bean
public DBRouterConfig dbRouterConfig(){
return new DBRouterConfig(dbCount,tbCount,routerKey);
}
// 动态mybatis拦截
@Bean
public Interceptor plugin(){return new DynamicMybatisPlugin();}
// 配置数据源
@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("url").toString(), objMap.get("username").toString(), objMap.get("password").toString()));
}
//设置数据源
DynamicDataSource dynamicDataSource = new DynamicDataSource();
dynamicDataSource.setTargetDataSources(targetDataSources);
dynamicDataSource.setDefaultTargetDataSource(new DriverManagerDataSource(defaultDataSourceConfig.get("url").toString(), defaultDataSourceConfig.get("username").toString(), defaultDataSourceConfig.get("password").toString()));
return dynamicDataSource;
}
@Bean
public IDBRouterStrategy idbRouterStrategy(DBRouterConfig dbRouterConfig){
return new DBRouterStrategyHashCode(dbRouterConfig);
}
@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 prefix = "mini-db-router.jdbc.datasource.";
dbCount = Integer.valueOf(environment.getProperty(prefix + "dbCount"));
tbCount = Integer.valueOf(environment.getProperty(prefix + "tbCount"));
routerKey = environment.getProperty(prefix + "routerKey");
//分库分表数据源
String dataSources = environment.getProperty(prefix + "list");
assert dataSources != null ;
for (String dbInfo : dataSources.split(",")) {
Map<String, Object> dataSourceProps = PropertyUtil.handle(environment, prefix + dbInfo, Map.class);
dataSourceMap.put(dbInfo,dataSourceProps);
}
//默认数据源
String defaultData = environment.getProperty(prefix + "default");
defaultDataSourceConfig = PropertyUtil.handle(environment,prefix + defaultData, Map.class);
}
}
里面需要配置下数据源
在DynamicDataSource对象中需要设置默认的数据源和目标数据源(你可以将这个理解成除了默认数据源外其它有哪些数据源)
这些代码中用到一个类DynamicDataSource,这个类是由我自定义的他主要的作用就是用来数据源的切换。而这个自定义的类继承自AbstractRoutingDataSource类,这个是spring.jdbc中提供的一个,他的作用在于在执行DML操作之前可以根据规则来使用哪一个数据源。在执行DML之前会回调这个类中的determineCurrentLookupKey方法来切换数据源。
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return "db" + DBContextHolder.getDBKey();
}
}
动态Batista拦截
// 动态mybatis拦截
@Bean
public Interceptor plugin(){return new DynamicMybatisPlugin();}
所需要的类为下面
package com.monica.db.router.dynamic;
import com.monica.db.router.annotatiom.DBRouterStrategy;
import com.monica.db.router.routeBase.DBContextHolder;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.DefaultReflectorFactory;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import java.sql.Connection;
import java.sql.Statement;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @Author monica
* @Date 2023/3/14 10:57
* @description 1.0
*/
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class DynamicMybatisPlugin implements Interceptor {
private Pattern pattern = Pattern.compile("(from|into|update)[\\s]{1,}(\\w{1,})", Pattern.CASE_INSENSITIVE);
private String tableName;
@Override
public Object intercept(Invocation invocation) throws Throwable {
//获取StatementHandler
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
MetaObject metaObject= MetaObject.forObject(statementHandler, SystemMetaObject.DEFAULT_OBJECT_FACTORY,SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY, new DefaultReflectorFactory());
MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
//获取自定义注解判断是否进行分表操作
String id = mappedStatement.getId();
String className = id.substring(0, id.lastIndexOf("."));
Class<?> clazz = Class.forName(className);
DBRouterStrategy dbRouterStrategy = clazz.getAnnotation(DBRouterStrategy.class);
if (null == dbRouterStrategy || !dbRouterStrategy.splitTable()){
return invocation.proceed();
}
//获取SQL
BoundSql boundSql = statementHandler.getBoundSql();
String sql = boundSql.getSql();
//替换SQL表名 USER 为USER_03
Matcher matcher = pattern.matcher(sql);
String tableName = null;
if (matcher.find()){
tableName = matcher.group().trim();
}
assert null != tableName;
String replaceSql = matcher.replaceAll(tableName + "_" + DBContextHolder.getTBKey());
return null;
}
}