spring boot项目整合mybatis-plus适配多数据源+反射获取注解数据
很多时候我们的项目用到的可能不是一个数据库或者一种类型的数据库,根据业务我们分有以下类型:单数据库单数据源,多数据库单数据源,多数据库多数据源。面对这样的情况我们可以采用dynamic-datasource-spring-boot-starter ,这是一个基于springboot的快速集成多数据源的启动器。
特性
- 支持 数据源分组 ,适用于多种场景 纯粹多库 读写分离 一主多从 混合模式。
- 支持数据库敏感配置信息 加密 ENC()。
- 支持每个数据库独立初始化表结构schema和数据库database。
- 支持无数据源启动,支持懒加载数据源(需要的时候再创建连接)。
- 支持 自定义注解 ,需继承DS(3.2.0+)。
- 提供并简化对Druid,HikariCp,Mybatis-Plus,Quartz,ShardingJdbc,P6sy,Jndi组件的快速集成。
- 提供 自定义数据源来源 方案(如全从数据库加载)。
- 提供项目启动后 动态增加移除数据源 方案。
- 提供Mybatis环境下的 纯读写分离 方案。
- 提供使用 spel动态参数 解析数据源方案。内置spel,session,header,支持自定义。
- 支持 多层数据源嵌套切换 。(ServiceA >>> ServiceB >>> ServiceC)。
- 提供 基于seata的分布式事务方案。 附:不支持原生spring事务。
- 提供 本地多数据源事务方案。 附:不支持原生spring事务。
约定
- 本框架只做 切换数据源 这件核心的事情,并不限制你的具体操作,切换了数据源可以做任何CRUD。
- 配置文件所有以下划线
_
分割的数据源 首部 即为组的名称,相同组名称的数据源会放在一个组下。 - 切换数据源可以是组名,也可以是具体数据源名称。组名则切换时采用负载均衡算法切换。
- 默认的数据源名称为 master ,你可以通过
spring.datasource.dynamic.primary
修改。 - 方法上的注解优先于类上注解。
- DS支持继承抽象类上的DS,暂不支持继承接口上的DS。
使用方法
引入依赖
// orm框架 mybatis-plus
implementation group: 'com.baomidou', name: 'mybatis-plus-boot-starter', version: '3.4.3.1'
// 多数据源核心依赖 dynamic-datasource-spring-boot-starter
implementation group: 'com.baomidou', name: 'dynamic-datasource-spring-boot-starter', version: '3.4.0'
// 各类数据库
// dm
compile group: 'com.dameng', name: 'Dm8JdbcDriver17', version: '8.1.1.49'
// compile('com.dameng:Dm7JdbcDriver17:7.6.0.77')
// 人大金仓
implementation 'com.kingbase8:kingbase8:8.2.0'
// 神通数据库
implementation 'com.shentong:stoscar:1.0'
//mysql
implementation group: 'mysql', name: 'mysql-connector-java', version: '8.0.25'
配置数据源
如果使用多数据源模式的话,原来在spring.datasource下的连接池配置需要转移到spring.datasource.dynamic下才生效。
hikari在spring.datasource下的配置和spring.datasource.dynamic下略有不同,这一点需要注意。
# 配置文件
spring:
datasource:
# 多数据源配置
dynamic:
# 链接池配置
hikari:
#最小空闲连接数量
min-idle: 5
#从池返回的连接默认自动提交
is-auto-commit: true
#空闲连接最大时间,10秒
idle-timeout: 10000
#连接池名字
pool-name: CsseHikariCP
#池中连接的最长生命周期
max-lifetime: 1800000
#数据库连接的超时时间
connection-timeout: 30000
#测试SQL
connection-test-query: SELECT 1
primary: dm #设置默认的数据源或者数据源组,默认值即为master
strict: true #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
datasource:
dm:
url: jdbc:dm://192.168.40.205
username: xxxxx
password: xxxxx
driver-class-name: dm.jdbc.driver.DmDriver
kingbases:
url: jdbc:kingbase8://192.168.17.151:54321/PLATFORM_APP_STORE
username: xxxxx
password: xxxxx
driver-class-name: com.kingbase8.Driver
oscar:
url: jdbc:oscar://192.168.17.40:2003/OSRDB?serverTimezone=UTC&useSSL=FALSE
username: xxxxx
password: xxxxx
driver-class-name: com.oscar.Driver
mysql:
url: jdbc:mysql://192.168.40.206:3306/am?characterEncoding=UTF-8&serverTimezone=UTC
username: xxxxx
password: xxxxx
driver-class-name: com.mysql.cj.jdbc.Driver
#......省略
#以上会配置一个默认库master,一个组slave下有两个子库slave_1,slave_2
# 多主多从 纯粹多库(记得设置primary) 混合配置
spring: spring: spring:
datasource: datasource: datasource:
dynamic: dynamic: dynamic:
datasource: datasource: datasource:
master_1: mysql: master:
master_2: oracle: slave_1:
slave_1: sqlserver: slave_2:
slave_2: postgresql: oracle_1:
slave_3: h2: oracle_2:
具体使用方式
若是没有多数据库多模式的支持的场景,直接更改数据源配置即可,如果场景中需要用到多库多模式的情况,又希望通过注解或者自定义配置进行操作的话可以采用如下两种模式
使用 @DS 切换数据源
@DS 可以注解在方法上或类上,同时存在就近原则 方法上注解 优先于 类上注解。
注解 | 结果 |
---|---|
没有@DS | 默认数据源 |
@DS(“dsName”) | dsName可以为组名也可以为具体某个库的名称 |
@Service
@DS("slave")
public class UserServiceImpl implements UserService {
@Autowired
private JdbcTemplate jdbcTemplate;
public List selectAll() {
return jdbcTemplate.queryForList("select * from user");
}
@Override
@DS("slave_1")
public List selectByCondition() {
return jdbcTemplate.queryForList("select * from user where age >10");
}
}
推荐使用自定义MybatisPlusConfig
通过自定义MybatisPlusConfig,我们可以实现对数据库模式的动态拼接,从而更好的实现多租户和多数据源等场景
package com.csse.platform.appstore.api.config.mybatisplus;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.core.config.GlobalConfig;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.handler.TableNameHandler;
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
import com.baomidou.mybatisplus.extension.plugins.inner.DynamicTableNameInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import com.csse.platform.appstore.api.context.ApiContext;
import com.google.common.collect.Lists;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import org.apache.commons.io.FilenameUtils;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.util.ClassUtils;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
/**
* @Author : 写代码的徐师傅
* @Company : 中软信息系统工程有限公司
* @Date : Created in 14:32 2020/4/22
*/
@Configuration
@MapperScan("com.csse.platform.appstore.api.mapper")
public class MybatisPlusConfig {
private static final String SYSTEM_TENANT_ID = "tenant_id";
private static final List<String> IGNORE_TENANT_TABLES = Lists.newArrayList("tenant");
static final String DEFAULT_RESOURCE_PATTERN = "*.class";
@Value("${spring.datasource.dynamic.primary}")
private String primaryDbType;
@Value("${database.databaseSchema}")
private String databaseSchema;
@Autowired
private ApiContext apiContext;
/**
* 自定义MybatisPlusInterceptor
* <p>
* 实现下上文注入租户id 拦截修改sql 多种数据源分页模式支持
*
* @return MybatisPlusInterceptor
*/
@Bean
public MybatisPlusInterceptor paginationInterceptor() {
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
// 多租户
mybatisPlusInterceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {
@Override
public Expression getTenantId() {
// 从当前系统上下文中取出当前请求的服务商ID,通过解析器注入到SQL中。
Long currentTenantId = apiContext.getCurrentTenantId();
if (null == currentTenantId) {
throw new RuntimeException("#1129 getCurrentTenantId error.");
}
return new LongValue(currentTenantId);
}
@Override
public String getTenantIdColumn() {
return SYSTEM_TENANT_ID;
}
// 这是 default 方法,默认返回 false 表示所有表都需要拼多租户条件
@Override
public boolean ignoreTable(String tableName) {
// 忽略掉一些表:如租户表(Tenant)本身不需要执行这样的处理。
return IGNORE_TENANT_TABLES.stream().anyMatch((e) -> e.equalsIgnoreCase(tableName));
}
}));
// 动态拼接
DynamicTableNameInnerInterceptor dynamicTableNameInnerInterceptor = new DynamicTableNameInnerInterceptor();
HashMap<String, TableNameHandler> map = new HashMap<String, TableNameHandler>(2) {{
// 操作单张表
// put("platform_app_classify", (sql, tableName) -> { return "PLATFORM_APP_STORE." + tableName; });
// 遍历操作所有表
List<String> tables = new ArrayList<>();
tables.addAll(listByClassAnnotation("com.csse.platform.appstore.api.entity"));
tables.addAll(listByClassAnnotation("com.csse.platform.appstore.api.entity.view"));
tables.forEach(table -> put(table, (sql, tableName) -> {
return databaseSchema + tableName;
}));
}};
dynamicTableNameInnerInterceptor.setTableNameHandlerMap(map);
mybatisPlusInterceptor.addInnerInterceptor(dynamicTableNameInnerInterceptor);
// 分页
// 如果用了分页插件注意先 add TenantLineInnerInterceptor 再 add PaginationInnerInterceptor
// 用了分页插件必须设置 MybatisConfiguration#useDeprecatedExecutor = false
if (primaryDbType.equals(DbType.DM.getDb())) {
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.DM));
} else if (primaryDbType.equals(DbType.KINGBASE_ES.getDb())) {
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.KINGBASE_ES));
} else if (primaryDbType.equals(DbType.OSCAR.getDb())) {
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.OSCAR));
} else if (primaryDbType.equals(DbType.MYSQL.getDb())) {
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
}
return mybatisPlusInterceptor;
}
/**
* 自动填充功能
*
* @return globalConfig
*/
@Bean
public GlobalConfig globalConfig() {
GlobalConfig globalConfig = new GlobalConfig();
globalConfig.setMetaObjectHandler(new MetaHandler());
return globalConfig;
}
/**
* 扫描所有包,通过反射获取到类中@TableName标注的表名称
*
* @param packageName 包名称
* @return List<String> listByClassAnnotation
*/
public static List<String> listByClassAnnotation(String packageName) {
List<String> tableNameList = new ArrayList<>();
try {
String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
resolveBasePackage(packageName) + File.separator + DEFAULT_RESOURCE_PATTERN;
Resource[] resources = new PathMatchingResourcePatternResolver().getResources(packageSearchPath);
for (Resource resource : resources) {
URL url = resource.getURL();
File file = new File(url.getPath());
String cname = packageName + "." + FilenameUtils.getBaseName(file.getName());
Class<?> aClass = Class.forName(cname);
if (aClass.isAnnotationPresent(TableName.class)) {
TableName declaredAnnotation = aClass.getDeclaredAnnotation(TableName.class);
tableNameList.add(declaredAnnotation.value());
}
}
} catch (ClassNotFoundException | IOException e) {
e.printStackTrace();
}
return tableNameList;
}
public static String resolveBasePackage(String basePackage) {
return ClassUtils.convertClassNameToResourcePath(new StandardEnvironment().resolveRequiredPlaceholders(basePackage));
}
}