项目要求从多个源库(oracle,haha,pg)里面读取schema,table,字段等信息,spring提供了AbstractRoutingDataSource类实现动态数据源,下面就简单介绍一下如何实现的。
首先给出数据源信息表如下所示:
ID | NAME | DATABASE_TYPE | CON_STR | ACCOUNT | PASSWORD | CREATOR | CREATE_TIME | MODIFIER | MODIFY_TIME | DEL_FLAG |
2 | xxx2 | HANA | jdbc:sap://1.1.1.1:30015 | xxx | xxx | xxx | 2021/11/10 20:35 | xxx | 2021/11/9 20:35 | 0 |
3 | xxx3 | oracle | jdbc:oracle:thin:@1.1.1.1:1521:xxx | xxx | xxx | xxx | 2021/11/10 20:35 | xxx | 2021/11/10 20:35 | 0 |
4 | xxx4 | oracle | jdbc:oracle:thin:@1.1.1.1:1521:xxx | xxx | xxx | xxx | 2021/11/11 20:35 | xxx | 2021/11/11 20:35 | 0 |
5 | xxx5 | HANA | jdbc:sap://1.1.1.1:30015 | xxx | xxx | xxx | 2021/11/12 20:35 | xxx | 2021/11/12 20:35 | 0 |
6 | xxx6 | oracle | jdbc:oracle:thin:@1.1.1.1:1521:xxx | xxx | xxx | xxx | 2021/11/13 20:35 | xxx | 2021/11/13 20:35 | 0 |
7 | xxx7 | HANA | jdbc:sap://1.1.1.1:30015 | xxx | xxx | xxx | 2021/11/14 20:35 | xxx | 2021/11/14 20:35 | 0 |
在项目中可以通过上表读取数据源信息进行数据源的实时切换,对于的entity对象为:
package com.xxx.entity;
import lombok.Data;
import lombok.ToString;
/**
* @Author : lgq
* @CreateTime : 2021/11/16
* @Description :
**/
@Data
@ToString
public class DataSource {
String datasourceId;
String url;
String userName;
String passWord;
String dataSourceName;
String databaseType;
}
DynamicDataSource继承AbstractRoutingDataSource用来创建和维护数据源。
package com.xxx.datasource;
import java.sql.Connection;
import java.sql.DriverManager;
import java.util.Map;
import java.util.Set;
import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.stat.DruidDataSourceStatManager;
import com.xxx.common.errorcode.ErrorCode;
import com.xxx.common.exception.BusinessException;
import com.xxx.entity.DataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.util.StringUtils;
public class DynamicDataSource extends AbstractRoutingDataSource {
private boolean debug = true;
public static final Logger log = LoggerFactory.getLogger(DynamicDataSource.class);
private Map
DruidDBConfig类用来注入动态数据源对象并创建主数据源。
package com.xxx.config;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.support.http.StatViewServlet;
import com.alibaba.druid.support.http.WebStatFilter;
import com.xxx.datasource.DynamicDataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.transaction.annotation.EnableTransactionManagement;
/**
* @Author : lgq
* @CreateTime : 2021/11/15
* @Description :
**/
@Configuration
@EnableTransactionManagement
public class DruidDBConfig {
private final Logger log = LoggerFactory.getLogger(getClass());
// 数据库连接信息
@Value("${spring.datasource.url}")
private String dbUrl;
@Value("${spring.datasource.username}")
private String username;
@Value("${spring.datasource.password}")
private String password;
@Value("${spring.datasource.driverClassName}")
private String driverClassName;
// 连接池连接信息
@Value("${spring.datasource.initialSize}")
private int initialSize;
@Value("${spring.datasource.minIdle}")
private int minIdle;
@Value("${spring.datasource.maxActive}")
private int maxActive;
@Value("${spring.datasource.maxWait}")
private int maxWait;
@Bean // 声明其为Bean实例
@Primary // 在同样的DataSource中,首先使用被标注的DataSource
@Qualifier("mainDataSource")
public DataSource dataSource() throws SQLException {
DruidDataSource datasource = new DruidDataSource();
// 基础连接信息
datasource.setUrl(this.dbUrl);
datasource.setUsername(username);
datasource.setPassword(password);
datasource.setDriverClassName(driverClassName);
// 连接池连接信息
datasource.setInitialSize(initialSize);
datasource.setMinIdle(minIdle);
datasource.setMaxActive(maxActive);
datasource.setMaxWait(maxWait);
//是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说oracle。在mysql下建议关闭。
datasource.setPoolPreparedStatements(true);
datasource.setMaxPoolPreparedStatementPerConnectionSize(20);
// 对于耗时长的查询sql,会受限于ReadTimeout的控制,单位毫秒
// datasource.setConnectionProperties("oracle.net.CONNECT_TIMEOUT=6000;oracle.jdbc.ReadTimeout=60000");
//对于耗时长的查询sql,会受限于ReadTimeout的控制,单位毫秒
datasource.setConnectionProperties("druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000");
//申请连接时执行validationQuery检测连接是否有效,这里建议配置为TRUE,防止取到的连接不可用
datasource.setTestOnBorrow(true);
//建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
datasource.setTestWhileIdle(true);
String validationQuery = "select 1 from dual";
//用来检测连接是否有效的sql,要求是一个查询语句。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。
datasource.setValidationQuery(validationQuery);
//属性类型是字符串,通过别名的方式配置扩展插件,常用的插件有:监控统计用的filter:stat日志用的filter:log4j防御sql注入的filter:wall
datasource.setFilters("stat,wall");
//配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
datasource.setTimeBetweenEvictionRunsMillis(60000);
//配置一个连接在池中最小生存的时间,单位是毫秒,这里配置为3分钟180000
datasource.setMinEvictableIdleTimeMillis(180000);
//打开druid.keepAlive之后,当连接池空闲时,池中的minIdle数量以内的连接,空闲时间超过minEvictableIdleTimeMillis,则会执行keepAlive操作,
// 即执行druid.validationQuery指定的查询SQL,一般为select * from dual,只要minEvictableIdleTimeMillis设置的小于防火墙切断连接时间,
// 就可以保证当连接空闲时自动做保活检测,不会被防火墙切断
datasource.setKeepAlive(true);
//是否移除泄露的连接/超过时间限制是否回收。
datasource.setRemoveAbandoned(true);
//泄露连接的定义时间(要超过最大事务的处理时间);单位为秒。这里配置为1小时
datasource.setRemoveAbandonedTimeout(3600);
//移除泄露连接发生是是否记录日志
datasource.setLogAbandoned(true);
return datasource;
}
/**
* 注册一个StatViewServlet druid监控页面配置1-帐号密码配置
*
* @return servlet registration bean
*/
@Bean
public ServletRegistrationBean druidStatViewServlet() {
ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(
new StatViewServlet(), "/druid/*");
servletRegistrationBean.addInitParameter("loginUsername", "admin");
servletRegistrationBean.addInitParameter("loginPassword", "123456");
servletRegistrationBean.addInitParameter("resetEnable", "false");
return servletRegistrationBean;
}
/**
* 注册一个:filterRegistrationBean druid监控页面配置2-允许页面正常浏览
*
* @return filter registration bean
*/
@Bean
public FilterRegistrationBean druidStatFilter() {
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(
new WebStatFilter());
// 添加过滤规则.
filterRegistrationBean.addUrlPatterns("/*");
// 添加不需要忽略的格式信息.
filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");
return filterRegistrationBean;
}
@Bean(name = "dynamicDataSource")
@Qualifier("dynamicDataSource")
public DynamicDataSource dynamicDataSource() throws SQLException {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
dynamicDataSource.setDebug(false);
//配置缺省的数据源
// 默认数据源配置 DefaultTargetDataSource
dynamicDataSource.setDefaultTargetDataSource(dataSource());
Map targetDataSources = new HashMap();
//额外数据源配置 TargetDataSources
targetDataSources.put("mainDataSource", dataSource());
dynamicDataSource.setTargetDataSources(targetDataSources);
return dynamicDataSource;
}
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dynamicDataSource());
//解决手动创建数据源后字段到bean属性名驼峰命名转换失效的问题
sqlSessionFactoryBean.setConfiguration(configuration());
// 设置mybatis的主配置文件
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
// 设置别名包
sqlSessionFactoryBean.setTypeAliasesPackage("com.xxx.mapper");
//手动配置mybatis的mapper.xml资源路径,如果单纯使用注解方式,不需要配置该行
sqlSessionFactoryBean.setMapperLocations(resolver.getResources("classpath:mapper/*.xml"));
return sqlSessionFactoryBean.getObject();
}
/**
* 读取驼峰命名设置
*
* @return
*/
@Bean
@ConfigurationProperties(prefix = "mybatis.configuration")
public org.apache.ibatis.session.Configuration configuration() {
return new org.apache.ibatis.session.Configuration();
}
}
配置文件application.properties内容如下:
server.port = xxxx
server.address = 0.0.0.0
mybatis.mapper-locations = classpath:mapper/*.xml,classpath:mapper/*/*.xml,classpath:mapper/*/*/*.xml
spring.datasource.druid.url = jdbc:oracle:thin:@1.1.1.1:1521:xxx
spring.datasource.druid.username = xxx
spring.datasource.druid.password = xxx
spring.datasource.druid.driver-class-name = oracle.jdbc.driver.OracleDriver
spring.datasource.druid.type = com.alibaba.druid.pool.DruidDataSource
spring.datasource.druid.initial-size = 20
spring.datasource.druid.max-active = 20
spring.datasource.druid.min-idle = 10
spring.datasource.druid.max-wait = 100
#redis配置
purist.redis.enable = true
purist.redis.database = 0
purist.redis.host = 127.0.0.1
purist.redis.port = 6379
purist.redis.password =
purist.redis.timeout = 60000
purist.redis.pool.maxActive = 8
purist.redis.pool.maxIdle = 8
purist.redis.pool.maxWait = -1
purist.redis.pool.minIdle = 0
#采用驼峰标识,解决 Mybatis resultType返回结果为null的问题
mybatis.configuration.map-underscore-to-camel-case = true
DBContextHolder使用ThreadLocal将数据源连接存储在当前线程的threadlocals(ThreadLocalMap)中,在连接数据库时自动获取当前线程对于的数据源。
package com.xxx.datasource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @Author : lgq
* @CreateTime : 2021/11/16
* @Description :
**/
public class DBContextHolder {
private final static Logger log = LoggerFactory.getLogger(DBContextHolder.class);
// 对当前线程的操作-线程安全的
private static final ThreadLocal contextHolder = new ThreadLocal();
// 调用此方法,切换数据源
public static void setDataSource(String dataSource) {
contextHolder.set(dataSource);
log.info("已切换到数据源:{}",dataSource);
}
// 获取数据源
public static String getDataSource() {
return contextHolder.get();
}
// 删除数据源
public static void clearDataSource() {
contextHolder.remove();
log.info("已切换到主数据源");
}
}
DBChangeService提供方法手动实现数据库切换。
package com.xxx.service.sourcesystem;
import java.util.List;
import com.xxx.entity.DataSource;
/**
* @Author : lgq
* @CreateTime : 2021/11/15
* @Description :
**/
public interface DBChangeService {
List get();
boolean changeDb(String datasourceId) throws Exception;
}
package com.xxx.service.sourcesystem.impl;
import java.util.List;
import javax.annotation.Resource;
import com.xxx.datasource.DBContextHolder;
import com.xxx.datasource.DynamicDataSource;
import com.xxx.entity.DataSource;
import com.xxx.mapper.DataSourceMapper;
import com.xxx.service.sourcesystem.DBChangeService;
import org.springframework.stereotype.Service;
/**
* @Author : lgq
* @CreateTime : 2021/11/16
* @Description :
**/
@Service
public class DBChangeServiceImpl implements DBChangeService {
@Resource
DataSourceMapper dataSourceMapper;
@Resource
private DynamicDataSource dynamicDataSource;
@Override
public List get() {
return dataSourceMapper.getAllDataSources();
}
@Override
public boolean changeDb(String datasourceName) throws Exception {
//默认切换到主数据源,进行整体资源的查找
DBContextHolder.clearDataSource();
List dataSourcesList = dataSourceMapper.getAllDataSources();
for (DataSource dataSource : dataSourcesList) {
if (dataSource.getDataSourceName().equals(datasourceName)) {
DynamicDataSource.log.info("需要使用的的数据源已经找到,datasourceName是:" + dataSource.getDataSourceName());
//创建数据源连接&检查 若存在则不需重新创建
dynamicDataSource.createDataSourceWithCheck(dataSource);
//切换到该数据源
DBContextHolder.setDataSource(dataSource.getDataSourceName());
return true;
}
}
return false;
}
}
在impl类里面的查询操作前后只要切换数据源即可完成查询。
dbChangeService.changeDb(sourceSystemName);
/*
* 各种逻辑处理和查询操作
*/
//切回主数据源
DBContextHolder.clearDataSource();
最后,给出再贴出hana,gp,oracle查询schema和table等相关信息的语句。