上两遍已经描述了动态多数据源的原理和基础实现了,前面的数据源配置都是从application.yml中配置多数据源的,这里再拓展补充一下其他场景,如何读取数据源不从application.yml中配置,实现从数据库中读取数据源配置并动态切换数据源。
上篇:springboot动态多数据源配置和使用(二)
/**
* 多数据源
*
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DynamicContextHolder.peek();
}
}
@Bean
public DynamicDataSource dynamicDataSource(DataSourceProperties dataSourceProperties) {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
//设置多个数据源的map
dynamicDataSource.setTargetDataSources(getDynamicDataSource())
//默认数据源
DruidDataSource defaultDataSource = DynamicDataSourceFactory.buildDruidDataSource(dataSourceProperties);
dynamicDataSource.setDefaultTargetDataSource(defaultDataSource);
return dynamicDataSource;
}
private Map<Object, Object> getDynamicDataSource(){
Map<String, DataSourceProperties> dataSourcePropertiesMap = properties.getDatasource();
Map<Object, Object> targetDataSources = new HashMap<>(dataSourcePropertiesMap.size());
dataSourcePropertiesMap.forEach((k, v) -> {
DruidDataSource druidDataSource = DynamicDataSourceFactory.buildDruidDataSource(v);
targetDataSources.put(k, druidDataSource);
});
return targetDataSources;
}
从上面分析可以知道,重要的是targetDataSources这个存放多数据源的map属性。
那么我们只要把targetDataSources这个map由配置文件获取创建dataSource然后放入map改写成由数据读读取出来的配置,再创建dataSource再放入targetDataSources这个map变量就可以实现我们想要的功能了。
这一步说难也不难,就把数据库的配置保存在数据库的表里面,在切面类切换数据源时读取数据库的配置,然后创建数据源,把创建的数据源通过put方法放入targetDataSources这个map即可,最后在切面类DynamicContextHolder.push(key)改变数据源
但是这样子就很没效率,每次都从数据库读取配置,然后创建dataSource数据源。所以实际上我们是懒加载的模式,再用一个数据源缓存池pool来保存dataSource,如果缓存有了dataSource就不再从数据库读取了,直接从数据源缓存池的pool来获取数据源。
CREATE TABLE `oa_quick_knife_datasource` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`datasource_name` varchar(20) DEFAULT '' COMMENT '数据源名称',
`datasource_url` varchar(200) DEFAULT '' COMMENT '数据源url',
`datasource_account` varchar(50) DEFAULT '' COMMENT '数据源帐号',
`datasource_password` varchar(64) DEFAULT '' COMMENT '数据源密码',
`remark` varchar(200) DEFAULT NULL COMMENT '备注',
`is_show_type` tinyint(1) DEFAULT NULL COMMENT '数据源可见类型(1-全部人可见,2-部分人可见)',
`datasource_type` tinyint(1) DEFAULT NULL COMMENT '默认mysql,暂时只支持mysql',
`update_name` varchar(20) DEFAULT '' COMMENT '更新人',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`create_code` int(11) unsigned DEFAULT '0' COMMENT '创建人工号',
`create_name` varchar(20) DEFAULT '' COMMENT '创建人',
`update_code` int(11) unsigned DEFAULT '0' COMMENT '更新人工号',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`deleted_flag` tinyint(1) DEFAULT '0' COMMENT '是否删除',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='数据源 ';
注:datasource_password这个字段我们不要明文保存数据库密码,我们加密后再放入这个字段里面
数据源缓存池的类代码
/**
* 数据源缓存池
*/
public class DataSourceCachePool {
/** 数据源连接池缓存【本地 class缓存 - 不支持分布式】 */
private static Map<String, DruidDataSource> dbSources = new HashMap<>();
private static RedisTemplate<String, Object> redisTemplate;
private static RedisTemplate<String, Object> getRedisTemplate() {
if (redisTemplate == null) {
redisTemplate = (RedisTemplate<String, Object>) SpringContextUtils.getBean("redisTemplate");
}
return redisTemplate;
}
/**
* 获取多数据源缓存
*
* @param dbKey
* @return
*/
public static DynamicDataSourceModel getCacheDynamicDataSourceModel(String dbKey) {
String redisCacheKey = ConfigConstant.SYS_DYNAMICDB_CACHE + dbKey;
if (getRedisTemplate().hasKey(redisCacheKey)) {
String model = (String)getRedisTemplate().opsForValue().get(redisCacheKey);
return JSON.parseObject(model,DynamicDataSourceModel.class);
}
DatasourceDao datasourceDao = (DatasourceDao)SpringContextUtils.getBean("datasourceDao");
DynamicDataSourceModel dbSource = datasourceDao.getDynamicDbSourceByCode(dbKey);
try{
dbSource.setDbPassword(AesUtil.decryptBySalt(dbSource.getDbPassword(),dbSource.getId()));
}catch (Exception e){
throw new RRException("动态数据源密钥解密失败,dbKey:"+dbKey);
}
if (dbSource != null) {
getRedisTemplate().opsForValue().set(redisCacheKey, JSONObject.toJSONString(dbSource));
}
return dbSource;
}
public static DruidDataSource getCacheBasicDataSource(String dbKey) {
return dbSources.get(dbKey);
}
/**
* put 数据源缓存
*
* @param dbKey
* @param db
*/
public static void putCacheBasicDataSource(String dbKey, DruidDataSource db) {
dbSources.put(dbKey, db);
}
/**
* 清空数据源缓存
*/
public static void cleanAllCache() {
//关闭数据源连接
for(Map.Entry<String, DruidDataSource> entry : dbSources.entrySet()){
String dbkey = entry.getKey();
DruidDataSource druidDataSource = entry.getValue();
if(druidDataSource!=null && druidDataSource.isEnable()){
druidDataSource.close();
}
//清空redis缓存
getRedisTemplate().delete(ConfigConstant.SYS_DYNAMICDB_CACHE + dbkey);
}
//清空缓存
dbSources.clear();
}
public static void removeCache(String dbKey) {
//关闭数据源连接
DruidDataSource druidDataSource = dbSources.get(dbKey);
if(druidDataSource!=null && druidDataSource.isEnable()){
druidDataSource.close();
}
//清空redis缓存
getRedisTemplate().delete(ConfigConstant.SYS_DYNAMICDB_CACHE + dbKey);
//清空缓存
dbSources.remove(dbKey);
}
}
上面的数据源缓存池主要代码是下面getCacheDynamicDataSourceModel方法的这段
这个方法的逻辑是先从redis缓存数据源配置,redis没有则从数据库获取,以及获取的配置的数据库密码是加密的,所以这里还要再解密
/**
* 获取多数据源缓存配置
*
* @param dbKey
* @return
*/
public static DynamicDataSourceModel getCacheDynamicDataSourceModel(String dbKey) {
String redisCacheKey = ConfigConstant.SYS_DYNAMICDB_CACHE + dbKey;
if (getRedisTemplate().hasKey(redisCacheKey)) {
String model = (String)getRedisTemplate().opsForValue().get(redisCacheKey);
return JSON.parseObject(model,DynamicDataSourceModel.class);
}
DatasourceDao datasourceDao = (DatasourceDao)SpringContextUtils.getBean("datasourceDao");
DynamicDataSourceModel dbSource = datasourceDao.getDynamicDbSourceByCode(dbKey);
try{
dbSource.setDbPassword(AesUtil.decryptBySalt(dbSource.getDbPassword(),dbSource.getId()));
}catch (Exception e){
throw new RRException("动态数据源密钥解密失败,dbKey:"+dbKey);
}
if (dbSource != null) {
getRedisTemplate().opsForValue().set(redisCacheKey, JSONObject.toJSONString(dbSource));
}
return dbSource;
}
还有一个重要的方法,把数据源放入缓存池的dbSource这个map属性里面
/**
* put 数据源缓存
*
* @param dbKey
* @param db
*/
public static void putCacheBasicDataSource(String dbKey, DruidDataSource db) {
dbSources.put(dbKey, db);
}
这个类的核心方法是getDbSourceByDbKey(),先判断缓存池有没有对应key的数据源,没有则读取数据源配置(先从redis读配置,没有再从数据库读配置),根据配置创建DruidDataSource数据源,再把数据源放入缓存池
getDbSourceByDbKey这个方法的dbKey是指能根据这个key找到数据库对应的记录,这里指该表的id
/**
* Spring JDBC 实时数据库访问
*
*/
@Slf4j
public class DynamicDBUtil {
/**
* 通过 dbKey ,获取数据源
*
* @param dbKey
* @return
*/
public static DruidDataSource getDbSourceByDbKey(final String dbKey) {
//先判断缓存中是否存在数据库链接
DruidDataSource cacheDbSource = DataSourceCachePool.getCacheBasicDataSource(dbKey);
if (cacheDbSource != null && !cacheDbSource.isClosed()) {
log.debug("--------getDbSourceBydbKey------------------从缓存中获取DB连接-------------------");
return cacheDbSource;
} else {
//获取多数据源配置
DynamicDataSourceModel dbSource = DataSourceCachePool.getCacheDynamicDataSourceModel(dbKey);
DruidDataSource dataSource = getJdbcDataSource(dbSource);
if(dataSource!=null && dataSource.isEnable()){
DataSourceCachePool.putCacheBasicDataSource(dbKey, dataSource);
}else{
throw new RRException("动态数据源连接失败,dbKey:"+dbKey);
}
log.info("--------getDbSourceBydbKey------------------创建DB数据库连接-------------------");
return dataSource;
}
}
/**
* 获取数据源【最底层方法,不要随便调用】
*
* @param dbSource
* @return
*/
private static DruidDataSource getJdbcDataSource(final DynamicDataSourceModel dbSource) {
DruidDataSource dataSource = new DruidDataSource();
String driverClassName = dbSource.getDbDriver();
String url = dbSource.getDbUrl();
String dbUser = dbSource.getDbUsername();
String dbPassword = dbSource.getDbPassword();
dataSource.setDriverClassName(driverClassName);
dataSource.setUrl(url);
//dataSource.setValidationQuery("SELECT 1 FROM DUAL");
dataSource.setTestWhileIdle(true);
dataSource.setTestOnBorrow(false);
dataSource.setTestOnReturn(false);
dataSource.setBreakAfterAcquireFailure(true);
dataSource.setConnectionErrorRetryAttempts(0);
dataSource.setUsername(dbUser);
dataSource.setMaxWait(60000);
dataSource.setPassword(dbPassword);
log.info("******************************************");
log.info("* *");
log.info("*====【"+dbSource.getCode()+"】=====Druid连接池已启用 ====*");
log.info("* *");
log.info("******************************************");
return dataSource;
}
/**
* 关闭数据库连接池
*
* @param dbKey
* @return
*/
public static void closeDbKey(final String dbKey) {
DruidDataSource dataSource = getDbSourceByDbKey(dbKey);
try {
if (dataSource != null && !dataSource.isClosed()) {
dataSource.getConnection().commit();
dataSource.getConnection().close();
dataSource.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
private static JdbcTemplate getJdbcTemplate(String dbKey) {
DruidDataSource dataSource = getDbSourceByDbKey(dbKey);
return new JdbcTemplate(dataSource);
}
/**
* 获取连接
* @param url
* @param username
* @param password
* @param driverName
* @return
*/
public static Connection getConn(String url,String username,String password,String driverName) {
Connection conn = null;
try {
Class.forName(driverName);
conn = DriverManager.getConnection(url, username, password);
} catch (Exception e) {
throw new RRException("无法连接,问题:"+e.getMessage(), e);
}
return conn;
}
/**
* 关闭数据库连接
* @param
*/
public static void closeConnection(Connection conn) {
try {
if(conn!=null){
conn.close();
}
} catch (SQLException e) {
throw new RRException("close connection failure", e);
}
}
}
这里比上一篇的DynamicDataSource新增了targetDataSources静态变量和setDataSource()静态方法。
targetDataSources这个属性用于存放多数据源
/**
* 多数据源
*
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
public static Map<Object, Object> targetDataSources = new ConcurrentHashMap<>(10);
@Override
protected Object determineCurrentLookupKey() {
return DynamicContextHolder.peek();
}
public static void setDataSource(String dbKey) throws Exception{
if(!DynamicDataSource.targetDataSources.containsKey(dbKey)){
DruidDataSource dataSource = DynamicDBUtil.getDbSourceByDbKey(dbKey);
DynamicDataSource.targetDataSources.put(dbKey,dataSource);
}
//切换动态多数据源的dbKey
DynamicContextHolder.push(dbKey);
DynamicDataSource dynamicDataSource = (DynamicDataSource) SpringContextUtils.getBean("dynamicDataSource");
//使得修改后的targetDataSources生效
dynamicDataSource.afterPropertiesSet();
}
}
下面代码通过dynamicDataSource.setTargetDataSources(DynamicDataSource.targetDataSources)把值引用赋值给dynamicDataSource对象(即指向同一块内存,修改了静态变量targetDataSources,就相当于修改了dynamicDataSource对象里面的targetDataSources属性)
@Configuration
@EnableConfigurationProperties(DynamicDataSourceProperties.class)
public class DynamicDataSourceConfig {
@Autowired
private DynamicDataSourceProperties properties;
@Bean
@ConfigurationProperties(prefix = "spring.datasource.druid")
public DataSourceProperties dataSourceProperties() {
return new DataSourceProperties();
}
@Bean
public DruidDataSource defaultDataSource(DataSourceProperties dataSourceProperties) {
//默认数据源,通过配置获取创建
DruidDataSource defaultDataSource = DynamicDataSourceFactory.buildDruidDataSource(dataSourceProperties);
return defaultDataSource;
}
@Bean
@Primary
@DependsOn({"defaultDataSource"})
public DynamicDataSource dynamicDataSource(DruidDataSource defaultDataSource) {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
//设置targetDataSources(通过数据库配置获取,首次创建没有数据源)
dynamicDataSource.setTargetDataSources(DynamicDataSource.targetDataSources);
//默认数据源
dynamicDataSource.setDefaultTargetDataSource(defaultDataSource);
return dynamicDataSource;
}
}
/**
* 多数据源注解
*
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DataSource {
String value() default "";
}
/**
* 多数据源,切面处理类
*
*/
@Aspect
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class DataSourceAspect {
protected Logger logger = LoggerFactory.getLogger(getClass());
@Pointcut("@annotation(io.renren.datasource.annotation.DataSource) " +
"|| @within(io.renren.datasource.annotation.DataSource)")
public void dataSourcePointCut() {
}
@Around("dataSourcePointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
MethodSignature signature = (MethodSignature) point.getSignature();
Class targetClass = point.getTarget().getClass();
Method method = signature.getMethod();
DataSource targetDataSource = (DataSource)targetClass.getAnnotation(DataSource.class);
DataSource methodDataSource = method.getAnnotation(DataSource.class);
if(targetDataSource != null || methodDataSource != null){
String value;
if(methodDataSource != null){
value = methodDataSource.value();
}else {
value = targetDataSource.value();
}
//根据dbKey动态设置数据源
DynamicDataSource.setDataSource(dbKey);
logger.debug("set datasource is {}", value);
}
try {
return point.proceed();
} finally {
DynamicContextHolder.poll();
logger.debug("clean datasource");
}
}
}
到这一步就已完成了,然后把DataSource注解加到service的类或方法上,即可实现操作指定的多数据源。
上面的注解DataSource的value是写死在代码里面的,但是我们有这样的需求,前端根据接口入参来操作指定数据源的数据。
所以我们在上面的基础上,再改造一下
再写两个注解
/**
* 多数据源注解-注解数据源的dbKey
*
* @author ZhangXinLin
*/
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DbKey {
}
这个自定义注解DbKey,是作用在参数上的,标志该参数是用来指定数据源的dbKey
/**
* 多数据源注解
*
* @author ZhangXinLin
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DynamicDataSource {
}
DynamicDataSource的自定义注解是用在controller的方法上
DynamicDataSource注解的切面类
这个切面类是根据方法的入参dbKey来动态切换数据源,核心代码是调用这行代码
//根据dbKey动态设置数据源
DynamicDataSource.setDataSource(dbKey);
/**
* @Description: 动态加载多数据源(启动后加载)
**/
@Aspect
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class DynamicDataSourceAspect {
protected Logger logger = LoggerFactory.getLogger(getClass());
@Pointcut("@annotation(io.renren.datasource.annotation.DynamicDataSource) " +
"|| @within(io.renren.datasource.annotation.DynamicDataSource)")
public void dynamicdataSourcePointCut() {
}
@Around("dynamicdataSourcePointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
//获取参数,根据参数获取数据源
String dbKey = getDbKey(point);
if( dbKey != null){
//根据dbKey动态设置数据源
DynamicDataSource.setDataSource(dbKey);
}
try {
return point.proceed();
} finally {
DynamicContextHolder.poll();
logger.debug("clean datasource");
}
}
/**
* 根据@DbKey注解获取数据源的dbKey
* @param point
* @return
*/
private String getDbKey(ProceedingJoinPoint point) {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
Object[] args = point.getArgs();
String value = null;
//参数注解,1维是参数,2维是注解
Annotation[][] annotations = method.getParameterAnnotations();
for (int i = 0; i < annotations.length; i++) {
Object param = args[i];
Annotation[] paramAnn = annotations[i];
//参数为空,直接下一个参数
if (param == null || paramAnn.length == 0) {
continue;
}
for (Annotation pAnnotation : paramAnn) {
if (pAnnotation.annotationType().equals(DbKey.class)) {
value = param.toString();
break;
}
}
}
return value;
}
}
然后在controller的方法上加上注解@DynamicDataSource,以及入参加上注解@Dbkey
/**
* 查看数据源的所有表列表
* @param id
* @return
*/
@DynamicDataSource
@RequestMapping("/getTableList/{id}")
public R getTableList(@PathVariable("id") @DbKey Integer id){
List<Map<String, Object>> list = datasourceService.queryTableList(id);
return R.ok().put("list", list);
}
可以看到,前端页面选择不同的数据库,后端接口就会根据dbKey的入参来动态切换数据源,从而查询出不同数据源的表名列表
源码在一个还没有写完的快速开发平台的项目里面(功能可以在线编写模版,线上配置数据源,不用改代码就可以编写开发模版,生成不同系统的基础代码);
这个项目还没写完,后面写完也会开源出来,所以这里的源码暂时没有