SpringBoot实现通过接口动态的添加或删除数据源

大多数SpringBoot应用,只有一个数据库,数据库信息配置在了配置文件中。一旦应用启动后就不能修改,
如果要修改数据源信息那么就需要修改配置,重新编译,打包,运行。

有时候需要这么一种场景,数据源并不是写在配置文件,而是存储在数据库(先有鸡还是先有蛋?)或者是内存中,可以随时的添加,删除。执行业务的时候通过参数指定当前使用的数据源。

例如多租户场景下,不同租户使用不同的数据源。但是又需要使用到Spring的声明式事务。

实现的大概逻辑

AbstractRoutingDataSource 类

自己写代码维护几个数据源并不是难事儿,主要是自己维护的数据源需要用上spring的声明式事务,这就需要使用Spring提供的工具类:org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource

它本身也是实现了 javax.sql.DataSource 接口。所以它可以作为 系统数据源,而且它本身提供了2个方法,可以实现运行时数据源的动态切换。

它维护了一个Map<Object, DataSource>,里面可以存储多个数据源。

核心的2个方法

// 在Spring开启事务之前,会调用这个方法,它返回一个 DataSource 数据源对象
// Spring就会使用这个数据源进行接下来的事务操作
protected DataSource determineTargetDataSource() 

// 数据源以 Map<Object, DataSource> 的形式存储。
// 该方法返回一个数据源的KEY,根据这个KEY到MAP中检索到当前使用的数据源
abstract protected Object determineCurrentLookupKey()

在简单理解了 AbstractRoutingDataSource 之后,就剩下的东西就很简单了。对于数据源的增加删除。其实就是对 AbstractRoutingDataSource 实例中的数据源Map进行添加和删除。

用户的每一个业务请求,都需要在参数携带数据源的标识,也就是KEY。这个KEY可以通过拦截器存储在ThreadLocal中。用户的参数不同,那么操作的数据源也就不同了。

Demo

工程使用 spring-data-jpa作为ORM框架,提供2个接口。

  • 数据源的添加,删除,查询。
  • 用户信息的添加,删除,查询。

数据库建表语句

CREATE TABLE `user` (
  `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `balance` decimal(10,2) DEFAULT NULL COMMENT '账户余额',
  `create_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `enabled` tinyint unsigned NOT NULL COMMENT '是否启用。0:禁用,1:启用',
  `name` varchar(50) COLLATE utf8mb4_general_ci NOT NULL COMMENT '名字',
  `update_at` timestamp NULL DEFAULT NULL COMMENT '修改时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='用户';

这里仅仅说明核心的代码,完整代码请在文末Github中获取
这里仅仅说明核心的代码,完整代码请在文末Github中获取
这里仅仅说明核心的代码,完整代码请在文末Github中获取

DynamicDataSource

数据源的实现

package io.springboot.demo.datasource;

import java.util.HashMap;
import java.util.Map;

import javax.sql.DataSource;

import org.springframework.beans.factory.DisposableBean;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.stereotype.Component;

import com.zaxxer.hikari.HikariDataSource;

import lombok.extern.slf4j.Slf4j;

@Component
@Slf4j
public class DynamicDataSource extends AbstractRoutingDataSource implements DisposableBean {

	// 系統中维护的数据源
	private final Map<Object, DataSource> dataSources = new HashMap<>();
	
	/**
	 * 根据KEY获取到数据源
	 */
	protected DataSource determineTargetDataSource() {
		DataSource dataSource = this.dataSources.get(this.determineCurrentLookupKey());
		if (dataSource == null) {
			// TODO 数据源不存在
		}
		return dataSource;
	}
	
	/**
	 * 获取当前的KEY
	 */
	@Override
	protected Object determineCurrentLookupKey() {
		return DataSourceKeyHolder.get();
	}
	
	public Map<Object, DataSource> getDataSources() {
		return dataSources;
	}

	/**
	 * 复写父类的afterPropertiesSet,空实现。
	 * 父类会进行非空校验,会导致异常
	 */
	@Override
	public void afterPropertiesSet() {
	}

	@Override
	public void destroy() throws Exception {
		this.dataSources.entrySet().forEach(db -> {
			log.info("释放 {} 数据源资源", db.getKey());
			((HikariDataSource) db.getValue()).close();
		});
	}
}

DataSourceController

数据源管理接口

package io.springboot.demo.controller;

import java.util.Collections;
import java.util.Map;

import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.zaxxer.hikari.HikariDataSource;

import io.springboot.demo.datasource.DynamicDataSource;
import lombok.Data;

// 数据源Model
@Data
class DataSourceDTO {
	private String key;			// 唯一标识
	private String url;			// JDBC连接地址
	private String username;	// 用户名
	private String password;	// 密码
}


@RestController
@RequestMapping("/api/datasource")
public class DataSourceController {
	
	@Autowired
	private DynamicDataSource dataSource;
	
	/**
	 * 获取系统中的所有数据源
	 * @return
	 */
	@GetMapping
	public Object list () {
		return this.dataSource.getDataSources().entrySet()
				.stream()
				.map(db -> Collections.singletonMap(db.getKey(), ((HikariDataSource) db.getValue()).getJdbcUrl()))
				.toList();
	}
	
	/**
	 * 添加数据源
	 * @param payload
	 * @return
	 */
	@PostMapping(produces = "text/plain; charset=utf-8")
	public Object add (@RequestBody DataSourceDTO payload) {
		
		Map<Object, DataSource> dataSource = this.dataSource.getDataSources();
		
		if (dataSource.containsKey(payload.getKey())) {
			return "已经存在:" + payload.getKey();
		}
		

		HikariDataSource hikariDataSource = new HikariDataSource();
		hikariDataSource.setJdbcUrl(payload.getUrl());
		hikariDataSource.setUsername(payload.getUsername());
		hikariDataSource.setPassword(payload.getPassword());
		
		dataSource.put(payload.getKey(), hikariDataSource);
		
		return "添加成功";
	}
	
	/**
	 * 删除数据源
	 * @param key
	 * @return
	 */
	@DeleteMapping("/{key}")
	public Object delete (@PathVariable("key") String key) {
		DataSource dataSource = this.dataSource.getDataSources().remove(key);
		if (dataSource == null) {
			return "数据源不存在:" + key;
		}
		
		((HikariDataSource) dataSource).close();
		
		return "删除成功";
	}
}

UserController

用户管理接口

package io.springboot.demo.controller;

import java.time.LocalDateTime;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import io.springboot.demo.entity.User;
import io.springboot.demo.service.UserService;
import lombok.extern.slf4j.Slf4j;

@RestController
@RequestMapping("/api/user")
@Slf4j
public class UserController {
	
	@Autowired
	private UserService userService;
	
	@GetMapping
	public Object list() {
		return this.userService.findAll();
	}
	
	@PostMapping
	public Object create (@RequestBody User user) {
		user.setCreateAt(LocalDateTime.now());
		
		log.info("添加用户: {}", user);
		
		this.userService.save(user);
		return user;
	}
	
	@DeleteMapping(value = "/{id}", produces = "text/plain; charset=utf-8")
	public Object delete (@PathVariable("id") Integer id) {
		this.userService.deleteById(id);
		return "删除成功";
	}
}

DataSourceInterceptor

从请求中解析出当前要使用的数据源的KEY

package io.springboot.demo.interceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import io.springboot.demo.datasource.DataSourceKeyHolder;
import io.springboot.demo.datasource.DynamicDataSource;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class DataSourceInterceptor implements HandlerInterceptor {
	
	@Autowired
	private DynamicDataSource dataSource;
	
	/**
	 * 从 Request 中解析出要使用的DataSourceKey
	 */
	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)throws Exception {
		
		// 从Header 解析
		String dataSourceKey = request.getHeader("x-db-key");
		
		// 从查询参数解析
		if (!StringUtils.hasText(dataSourceKey)) {
			dataSourceKey = request.getParameter("dbKey");
		}
		
		log.info("DB标识: {}", dataSourceKey);
		
		if (!StringUtils.hasText(dataSourceKey)) {
			response.setContentType("text/plain");
			response.setCharacterEncoding("utf-8");
			response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
			response.getWriter().println("缺少DB标识");
			return false;
		}
		
		if(!this.dataSource.getDataSources().containsKey(dataSourceKey)) {
			response.setContentType("text/plain");
			response.setCharacterEncoding("utf-8");
			response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
			response.getWriter().println("数据源:" + dataSourceKey + ", 不存在");
			return false;
		}
		
		
		DataSourceKeyHolder.set(dataSourceKey);
		
		return true;
	}
	
	@Override
	public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
		DataSourceKeyHolder.clear();
		HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
	}
}

测试

先启动系统

添加3个数据源



数据库中的3个数据源也就是这样

image

查看系统数据源

添加用户,指定数据源 demo1

image

查询用户

从 demo1 查询,可以获取到新增的结果

image

从 demo3 查询,结果为空

image

删除Demo3数据源

image

最后

这仅仅是一个实现数据源动态增删思路的一个Demo,它还有很多问题。例如数据源Map的并发问题,数据库资源的释放问题,数据库的初始化时间问题等等需要在实际应用的时候考虑到。

完整源码

1 Like