多数据源
回顾
通过后面文章的介绍,目前曾经反对支流数据库,包含 MySql,PostgreSql,Oracle,Microsoft SQL Server 等,通过配置零代码实现了 CRUD 增删改查 RESTful API。采纳形象工厂设计模式,能够无缝切换不同类型的数据库。
然而如果须要同时反对不同类型的数据库,如何通过配置进行治理呢?这时候引入多数据源性能就很有必要了。
简介
利用 spring boot 多数据源性能,能够同时反对不同类型数据库 mysql,oracle,postsql,sql server 等,以及雷同类型数据库不同的 schema。零代码同时生成不同类型数据库增删改查 RESTful api,且反对同一接口中跨库数据拜访二次开发。
UI 界面
配置一个数据源,多个从数据源,每一个数据源互相独立配置和拜访。
外围原理
配置数据库连贯串
配置 application.properties,spring.datasource 为默认主数据源,spring.datasource.hikari.data-sources[] 数组为从数据源
#primary
spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/crudapi?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true
spring.datasource.username=root
spring.datasource.password=root
#postgresql
spring.datasource.hikari.data-sources[0].postgresql.driverClassName=org.postgresql.Driver
spring.datasource.hikari.data-sources[0].postgresql.url=jdbc:postgresql://localhost:5432/crudapi
spring.datasource.hikari.data-sources[0].postgresql.username=postgres
spring.datasource.hikari.data-sources[0].postgresql.password=postgres
#sqlserver
spring.datasource.hikari.data-sources[1].sqlserver.driverClassName=com.microsoft.sqlserver.jdbc.SQLServerDriver
spring.datasource.hikari.data-sources[1].sqlserver.url=jdbc:sqlserver://localhost:1433;SelectMethod=cursor;DatabaseName=crudapi
spring.datasource.hikari.data-sources[1].sqlserver.username=sa
spring.datasource.hikari.data-sources[1].sqlserver.password=Mssql1433
#oracle
spring.datasource.hikari.data-sources[2].oracle.url=jdbc:oracle:thin:@//localhost:1521/XEPDB1
spring.datasource.hikari.data-sources[2].oracle.driverClassName=oracle.jdbc.OracleDriver
spring.datasource.hikari.data-sources[2].oracle.username=crudapi
spring.datasource.hikari.data-sources[2].oracle.password=crudapi
#mysql
spring.datasource.hikari.data-sources[3].mysql.driverClassName=com.mysql.cj.jdbc.Driver
spring.datasource.hikari.data-sources[3].mysql.url=jdbc:mysql://localhost:3306/crudapi2?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true
spring.datasource.hikari.data-sources[3].mysql.username=root
spring.datasource.hikari.data-sources[3].mysql.password=root
动静数据源——DynamicDataSource
Spring boot 提供了抽象类 AbstractRoutingDataSource,复写接口 determineCurrentLookupKey,能够在执行查问之前,设置应用的数据源,从而实现动静切换数据源。
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {return DataSourceContextHolder.getDataSource();
}
}
数据源 Context——DataSourceContextHolder
默认主数据源名称为 datasource,从数据源名称保留在 ThreadLocal 变量 CONTEXT_HOLDER 外面,ThreadLocal 叫做线程变量, 意思是 ThreadLocal 中填充的变量属于以后线程, 该变量对其余线程而言是隔离的, 也就是说该变量是以后线程独有的变量。
在 RestController 外面依据须要提前设置好以后须要拜访的数据源 key,即调用 setDataSource 办法,拜访数据的时候调用 getDataSource 办法获取到数据源 key,最终传递给 DynamicDataSource。
public class DataSourceContextHolder {
// 默认数据源 primary=dataSource
private static final String DEFAULT_DATASOURCE = "dataSource";
// 保留线程连贯的数据源
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
private static final ThreadLocal<String> HEADER_HOLDER = new ThreadLocal<>();
public static String getDataSource() {String dataSoure = CONTEXT_HOLDER.get();
if (dataSoure != null) {return dataSoure;} else {return DEFAULT_DATASOURCE;}
}
public static void setDataSource(String key) {if ("primary".equals(key)) {key = DEFAULT_DATASOURCE;}
CONTEXT_HOLDER.set(key);
}
public static void cleanDataSource() {CONTEXT_HOLDER.remove();
}
public static void setHeaderDataSource(String key) {HEADER_HOLDER.set(key);
}
public static String getHeaderDataSource() {String dataSoure = HEADER_HOLDER.get();
if (dataSoure != null) {return dataSoure;} else {return DEFAULT_DATASOURCE;}
}
}
动静数据库提供者——DynamicDataSourceProvider
程序启动时候,读取配置文件 application.properties 中数据源信息,构建 DataSource 并通过接口 setTargetDataSources 设置从数据源。数据源的 key 和 DataSourceContextHolder 中 key 一一对应
@Component
@EnableConfigurationProperties(DataSourceProperties.class)
@ConfigurationProperties(prefix = "spring.datasource.hikari")
public class DynamicDataSourceProvider implements DataSourceProvider {
@Autowired
private DynamicDataSource dynamicDataSource;
private List<Map<String, DataSourceProperties>> dataSources;
private Map<Object,Object> targetDataSourcesMap;
@Resource
private DataSourceProperties dataSourceProperties;
private DataSource buildDataSource(DataSourceProperties prop) {DataSourceBuilder<?> builder = DataSourceBuilder.create();
builder.driverClassName(prop.getDriverClassName());
builder.username(prop.getUsername());
builder.password(prop.getPassword());
builder.url(prop.getUrl());
return builder.build();}
@Override
public List<DataSource> provide() {Map<Object,Object> targetDataSourcesMap = new HashMap<>();
List<DataSource> res = new ArrayList<>();
if (dataSources != null) {
dataSources.forEach(map -> {Set<String> keys = map.keySet();
keys.forEach(key -> {DataSourceProperties properties = map.get(key);
DataSource dataSource = buildDataSource(properties);
targetDataSourcesMap.put(key, dataSource);
});
});
// 更新 dynamicDataSource
this.targetDataSourcesMap = targetDataSourcesMap;
dynamicDataSource.setTargetDataSources(targetDataSourcesMap);
dynamicDataSource.afterPropertiesSet();}
return res;
}
@PostConstruct
public void init() {provide();
}
public List<Map<String, DataSourceProperties>> getDataSources() {return dataSources;}
public void setDataSources(List<Map<String, DataSourceProperties>> dataSources) {this.dataSources = dataSources;}
public List<Map<String, String>> getDataSourceNames() {List<Map<String, String>> dataSourceNames = new ArrayList<Map<String, String>>();
Map<String, String> dataSourceNameMap = new HashMap<String, String>();
dataSourceNameMap.put("name", "primary");
dataSourceNameMap.put("caption", "主数据源");
dataSourceNameMap.put("database", parseDatabaseName(dataSourceProperties));
dataSourceNames.add(dataSourceNameMap);
if (dataSources != null) {
dataSources.forEach(map -> {Set<Map.Entry<String, DataSourceProperties>> entrySet = map.entrySet();
for (Map.Entry<String, DataSourceProperties> entry : entrySet) {Map<String, String> t = new HashMap<String, String>();
t.put("name", entry.getKey());
t.put("caption", entry.getKey());
DataSourceProperties p = entry.getValue();
t.put("database", parseDatabaseName(p));
dataSourceNames.add(t);
}
});
}
return dataSourceNames;
}
public String getDatabaseName() {List<Map<String, String>> dataSourceNames = this.getDataSourceNames();
String dataSource = DataSourceContextHolder.getDataSource();
Optional<Map<String, String>> op = dataSourceNames.stream()
.filter(t -> t.get("name").toString().equals(dataSource))
.findFirst();
if (op.isPresent()) {return op.get().get("database");
} else {return dataSourceNames.stream()
.filter(t -> t.get("name").toString().equals("primary"))
.findFirst().get().get("database");
}
}
private String parseDatabaseName(DataSourceProperties p) {String url = p.getUrl();
String databaseName = "";
if (url.toLowerCase().indexOf("databasename") >= 0) {String[] urlArr = p.getUrl().split(";");
for (String u : urlArr) {if (u.toLowerCase().indexOf("databasename") >= 0) {String[] uArr = u.split("=");
databaseName = uArr[uArr.length - 1];
}
}
} else {String[] urlArr = p.getUrl().split("\\?")[0].split("/");
databaseName = urlArr[urlArr.length - 1];
}
return databaseName;
}
public Map<Object,Object> getTargetDataSourcesMap() {return targetDataSourcesMap;}
}
动静数据源配置——DynamicDataSourceConfig
首先勾销零碎主动数据库配置,设置 exclude = {DataSourceAutoConfiguration.class}
@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class})
public class ServiceApplication {public static void main(String[] args) {SpringApplication.run(ServiceApplication.class, args);
}
}
而后自定义 Bean,别离定义主数据源 dataSource 和动静数据源 dynamicDataSource,并且注入到 JdbcTemplate,NamedParameterJdbcTemplate,和 DataSourceTransactionManager 中,在拜访数据时候自动识别对应的数据源。
// 数据源配置类
@Configuration
@EnableConfigurationProperties(DataSourceProperties.class)
public class DynamicDataSourceConfig {private static final Logger log = LoggerFactory.getLogger(DynamicDataSourceConfig.class);
@Resource
private DataSourceProperties dataSourceProperties;
@Bean(name = "dataSource")
public DataSource getDataSource(){DataSourceBuilder<?> builder = DataSourceBuilder.create();
builder.driverClassName(dataSourceProperties.getDriverClassName());
builder.username(dataSourceProperties.getUsername());
builder.password(dataSourceProperties.getPassword());
builder.url(dataSourceProperties.getUrl());
return builder.build();}
@Primary // 当雷同类型的实现类存在时,抉择该注解标记的类
@Bean("dynamicDataSource")
public DynamicDataSource dynamicDataSource(){DynamicDataSource dynamicDataSource = new DynamicDataSource();
// 默认数据源
dynamicDataSource.setDefaultTargetDataSource(getDataSource());
Map<Object,Object> targetDataSourcesMap = new HashMap<>();
dynamicDataSource.setTargetDataSources(targetDataSourcesMap);
return dynamicDataSource;
}
// 事务管理器 DataSourceTransactionManager 结构参数须要 DataSource
// 这里能够看到咱们给的是 dynamicDS 这个 bean
@Bean
public PlatformTransactionManager transactionManager(){return new DataSourceTransactionManager(dynamicDataSource());
}
// 这里的 JdbcTemplate 结构参数同样须要一个 DataSource, 为了实现数据源切换查问,// 这里应用的也是 dynamicDS 这个 bean
@Bean(name = "jdbcTemplate")
public JdbcTemplate getJdbc(){return new JdbcTemplate(dynamicDataSource());
}
// 这里的 JdbcTemplate 结构参数同样须要一个 DataSource, 为了实现数据源切换查问,// 这里应用的也是 dynamicDS 这个 bean
@Bean(name = "namedParameterJdbcTemplate")
public NamedParameterJdbcTemplate getNamedJdbc(){return new NamedParameterJdbcTemplate(dynamicDataSource());
}
}
申请头过滤器——HeadFilter
拦挡所有 http 申请,从 header 外面解析出以后须要拜访的数据源,而后设置到线程变量 HEADER_HOLDER 中。
@WebFilter(filterName = "headFilter", urlPatterns = "/*")
public class HeadFilter extends OncePerRequestFilter {private static final Logger log = LoggerFactory.getLogger(HeadFilter.class);
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {if (!"/api/auth/login".equals(request.getRequestURI())
&& !"/api/auth/jwt/login".equals(request.getRequestURI())
&& !"/api/auth/logout".equals(request.getRequestURI())
&& !"/api/metadata/dataSources".equals(request.getRequestURI())) {String dataSource = request.getParameter("dataSource");
HeadRequestWrapper headRequestWrapper = new HeadRequestWrapper(request);
if (StringUtils.isEmpty(dataSource)) {dataSource = headRequestWrapper.getHeader("dataSource");
if (StringUtils.isEmpty(dataSource)) {
dataSource = "primary";
headRequestWrapper.addHead("dataSource", dataSource);
}
}
DataSourceContextHolder.setHeaderDataSource(dataSource);
// finish
filterChain.doFilter(headRequestWrapper, response);
} else {filterChain.doFilter(request, response);
}
}
}
理论利用
后面动静数据源配置筹备工作曾经实现,最初咱们定义切面 DataSourceAspect
@Aspect
public class DataSourceAspect {private static final Logger log = LoggerFactory.getLogger(DataSourceAspect.class);
@Pointcut("within(cn.crudapi.api.controller..*)")
public void applicationPackagePointcut() {}
@Around("applicationPackagePointcut()")
public Object dataSourceAround(ProceedingJoinPoint joinPoint) throws Throwable {String dataSource = DataSourceContextHolder.getHeaderDataSource();
DataSourceContextHolder.setDataSource(dataSource);
try {return joinPoint.proceed();
} finally {DataSourceContextHolder.cleanDataSource();
}
}
}
在 API 对应的 controller 中拦挡,获取以后的申请头数据源 key,而后执行 joinPoint.proceed(),最初再复原数据源。当然在 service 外部还能够屡次切换数据源,只须要调用 DataSourceContextHolder.setDataSource() 即可。比方能够从 mysql 数据库读取数据,而后保留到 oracle 数据库中。
前端集成
在申请头外面设置 dataSource 为对应的数据源,比方 primary 示意主数据源,postgresql 示意从数据源 postgresql,具体能够名称和 application.properties 配置保持一致。
首先调用的中央配置 dataSource
const table = {list: function(dataSource, tableName, page, rowsPerPage, search, query, filter) {
return axiosInstance.get("/api/business/" + tableName,
{
params: {offset: (page - 1) * rowsPerPage,
limit: rowsPerPage,
search: search,
...query,
filter: filter
},
dataSource: dataSource
}
);
},
}
而后在 axios 外面对立拦挡配置
axiosInstance.interceptors.request.use(function(config) {if (config.dataSource) {console.log("config.dataSource =" + config.dataSource);
config.headers["dataSource"] = config.dataSource;
}
return config;
},
function(error) {return Promise.reject(error);
}
);
成果如下
小结
本文次要介绍了多数据源性能,在同一个 Java 程序中,通过多数据源性能,不须要一行代码,咱们就能够失去不同数据库的根本 crud 性能,包含 API 和 UI。
crudapi 简介
crudapi 是 crud+api 组合,示意增删改查接口,是一款零代码可配置的产品。应用 crudapi 能够辞别枯燥无味的增删改查代码,让您更加专一业务,节约大量老本,从而进步工作效率。
crudapi 的指标是让解决数据变得更简略,所有人都能够收费应用!
无需编程,通过配置主动生成 crud 增删改查 RESTful API,提供后盾 UI 治理业务数据。基于支流的开源框架,领有自主知识产权,反对二次开发。
demo 演示
crudapi 属于产品级的零代码平台,不同于主动代码生成器,不须要生成 Controller、Service、Repository、Entity 等业务代码,程序运行起来就能够应用,真正 0 代码,能够笼罩根本的和业务无关的 CRUD RESTful API。
官网地址:https://crudapi.cn
测试地址:https://demo.crudapi.cn/crudapi/login
附源码地址
GitHub 地址
https://github.com/crudapi/crudapi-admin-web
Gitee 地址
https://gitee.com/crudapi/crudapi-admin-web
因为网络起因,GitHub 可能速度慢,改成拜访 Gitee 即可,代码同步更新。