最近由於工作需要,項目中需要實現多數據源切換的功能。之前在網上找了很多的資料,大多是在配置文件中已經配置好數據源,而在數據庫中配置並動態添加的卻很少。目前已實現的功能是數據源可以在數據庫中進行配置,也可以在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() {
//...
}