最近由于工作需要,项目中需要实现多数据源切换的功能。之前在网上找了很多的资料,大多是在配置文件中已经配置好数据源,而在数据库中配置并动态添加的却很少。目前已实现的功能是数据源可以在数据库中进行配置,也可以在jdbc.properties配置文件中配置多个数据源。
实现数据源的动态切换主要是用了Spring AOP和AbstractRoutingDataSource类。AbstractRoutingDataSource直译就是抽象的数据路由,它的主要功能就是切换数据源。
首先是继承AbstractRoutingDataSource,实现自己的数据源切换逻辑。
/**
* 切换数据源
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
/**
* 存储创建的数据源
* 用户每次登录时需要清空
*/
private Map<Object, Object> _targetDataSource;
@Autowired
private HttpSession httpSession;
private static final Logger LOGGER = Logger.getLogger(DynamicDataSource.class);
@Override
protected Object determineCurrentLookupKey() {
String ds = DataSourceHolder.getCustomeType();
LOGGER.info("Get datasource name>>>>>>>>>>>" + ds);
LOGGER.info("Start Set dynamicDataSource......");
//设置数据源
this.selectDataSource(ds);
LOGGER.info("Set dynamicDataSource Success!");
LOGGER.info("Use dataSource name >>>>>>>>>>" + ds);
DataSourceHolder.remove();
return ds;
}
/**
* 选择数据源
* 如果数据源已存在,则不做处理
*/
private void selectDataSource(String sourceName) {
String getSourceName = DataSourceHolder.getCustomeType();
//添加到缓存中
Object source = this._targetDataSource.get(sourceName);
if (source == null || !sourceName.equals(getSourceName)) {
//获取数据源
BasicDataSource basicDataSource = getBasicDataSource(sourceName);
//设置数据源
if (basicDataSource != null) {
setDataSource(sourceName, basicDataSource);
}
}
}
/**
* 切换数据源
*/
public void setTargetDataSources(Map<Object, Object> targetDataSource) {
this._targetDataSource = targetDataSource;
super.setTargetDataSources(targetDataSource);
afterPropertiesSet();
}
/**
* 添加数据源
*/
private void addDataSource(String key,BasicDataSource dataSource) {
this._targetDataSource.put(key, dataSource);
this.setTargetDataSources(this._targetDataSource);
}
/**
* 设置数据源
*/
private void setDataSource(String key, BasicDataSource dataSource) {
addDataSource(key, dataSource);
DataSourceHolder.setCustomeType(key);
}
/**
* 根据数据源名称获取数据源
*
* @param sourceName 数据源名称
* @return 数据源
*/
private BasicDataSource getBasicDataSource(String sourceName) {
BasicDataSource basicDataSource = new BasicDataSource();
//从session中获取数据库中存储的配置,在用户登录时存储中Session中
Map<String, String> configs = (Map<String, String>) httpSession.getAttribute("sysManage");
String driverName = "", url = "", username = "", password = "";
if (sourceName.startsWith("his")) {
String hisConfig = configs.get(SystemManageController.HISDATABASECONFIG);
//获取HIS数据库相关配置信息
JSONObject jsonObject = JSONObject.fromObject(hisConfig);
driverName = (String) jsonObject.get("hisDriverName");
url = (String) jsonObject.get("hisDatabaseUrl");
username = (String) jsonObject.get("hisUsername");
password = (String) jsonObject.get("hisPassword");
}
basicDataSource.setDriverClassName(driverName);
basicDataSource.setUrl(url);
basicDataSource.setUsername(username);
basicDataSource.setPassword(password);
return basicDataSource;
}
/**
* 清空存储的数据源连接信息
* 在用户登录的调用
*/
public void clearDataSource() {
if (_targetDataSource != null) {
Map<Object, Object> tempMap = new HashMap<>();
tempMap.put("gzz", _targetDataSource.get("gzz"));
_targetDataSource.clear();
_targetDataSource.putAll(tempMap);
}
}
}
使用线程局部变量来临时存储当前用户需要切换的数据源名称。
public class DataSourceHolder {
private static final ThreadLocal<String> datasourcce = new ThreadLocal<String>();
public static void setCustomeType(String type) {
datasourcce.set(type);
}
public static String getCustomeType() {
return datasourcce.get();
}
public static void remove() {
datasourcce.remove();
}
}
编写一个切面,用于程序在执行service层方法时动态切换数据源。
@Component
public class DataSourceAspect {
public void changeDateSource(JoinPoint jp) {
try {
String methodName = jp.getSignature().getName();
Class<?> targetClass = Class.forName(jp.getTarget().getClass().getName());
for (Method method : targetClass.getMethods()) {
if (methodName.equals(method.getName())) {
Class<?>[] args = method.getParameterTypes();
if (args.length == jp.getArgs().length) {
DataSource ds = method.getAnnotation(DataSource.class);
//如果注解不为空
if (ds != null) {
DataSourceHolder.setCustomeType(ds.name());
}
//如果注解为空,根据方法名称的后缀获取
else{
//方法名是否已特定的后缀结束
if (methodName.contains("_")) {
//获取数据源的名称
String sourceName = methodName.substring(methodName.lastIndexOf("_") + 1, methodName.length());
//根据名称在数据库中获取对应的数据源
if (StringUtils.isNotBlank(sourceName)) {
DataSourceHolder.setCustomeType(sourceName);
}else{
DataSourceHolder.setCustomeType("gzz");
}
}else{
DataSourceHolder.setCustomeType("gzz");
}
}
}
}
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
创建一个注解,用于对项目中已配置的数据源通过注解的方式切换,而不需要再读取数据库。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DataSource {
public String name() default "";
}
spring的相关配置:
<bean name="gzz" class="com.alibaba.druid.pool.DruidDataSource"
init-method="init" destroy-method="close">
<property name="url" value="${gzz_driverUrl}"/>
<property name="username" value="${gzz_username}"/>
<property name="password" value="${gzz_password}"/>
</bean>
<bean id="dataSource" class="com.xxx.datasource.DynamicDataSource">
<property name="targetDataSources">
<map key-type="java.lang.String">
<entry key="gzz" value-ref="gzz"/>
</map>
</property>
<property name="defaultTargetDataSource" ref="gzz"/>
</bean>
<bean id="dataSourceAspect" class="com.xxx.aop.DataSourceAspect"/>
<aop:aspect ref="dataSourceAspect" order="1">
<aop:before method="changeDateSource"
pointcut="execution(* com.xxx..service.*.*(..))" />
</aop:aspect>
</aop:config>
在service层有如下两种使用方法
@Override
public List<User> getHisData_his() {
try {
//...
} catch (Exception e) {
//...
throw new ServiceException("读取数据失败");
}
}
@DataSource(name = "gzz")
@Override
public List<User> getSycData() {
//...
}