1. 程式人生 > >JAVA中使用程式碼建立多資料來源,並實現動態切換(一)

JAVA中使用程式碼建立多資料來源,並實現動態切換(一)

摘要: 案例基於Spring+mybatis。 設計的目的:解決不確定(個數和種類)資料來源的切換問題

2017-06-06 11:31:57補充:近日,在本文的基礎之上,擴充套件了下,使用atomikos來管理事務,保證多資料來源操作時,事務一致性。(https://my.oschina.net/simpleton/blog/916108)

另外,感謝朋友對本文的關注和對博主的支援,最近有很多朋友聯絡我希望深入探討下本文涉及內容,不過由於近日太忙,沒有及時回覆大家,請見諒。

近日,博主有個業務需求,就是根據資料庫儲存的不同資料來源資訊,動態建立資料來源並實現業務不同而轉到不同的資料來源上處理。

資料庫儲存起來的資料來源資訊是不確定的,可以刪除和新增,這些是業務前提。

在網上找了下相關資料,對於使用Spring配置,直接配置多套資料來源,使用AOP動態切換的方式居多,這種方式博主以前也使用過,很強大。不過有個前提就是多個數據源的資訊是預先就確定的。那麼對於不確定資料來源資訊的業務需求,就只有使用程式碼動態實現資料來源初始化和銷燬操作了。

好了,有了這些思路,可以開始準備寫程式碼了。

1、建立一個執行緒上下文物件(使用ThreadLocal,保證執行緒安全)。上下文物件中主要維護了資料來源的KEY和資料來源的地址等資訊,當KEY對應的資料來源找不到時,根據資料來源地址、驅動和使用者名稱等建立 一個數據源,這裡也是業務中需要解決的一個核心問題(JAVA動態建立資料來源)。

/**
* Copyright (c) 2015 - 2016 eay Inc.
* All rights reserved.
*/
package com.eya.pubservice.datasource;

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

/**
* 當前正在使用的資料來源資訊的執行緒上線文
* @create ll
* @createDate 2017年3月27日 下午2:37:07
* @update
* @updateDate
*/
public class DBContextHolder {
/* 資料來源的KEY

/
public static final String DATASOURCE_KEY = “DATASOURCE_KEY”;
/* 資料來源的URL /
public static final String DATASOURCE_URL = “DATASOURCE_URL”;
/* 資料來源的驅動 /
public static final String DATASOURCE_DRIVER = “DATASOURCE_DRIVER”;
/* 資料來源的使用者名稱 /
public static final String DATASOURCE_USERNAME = “DATASOURCE_USERNAME”;
/* 資料來源的密碼
/
public static final String DATASOURCE_PASSWORD = “DATASOURCE_PASSWORD”;

private static final ThreadLocal<Map<String, Object>> contextHolder = new ThreadLocal<Map<String, Object>>();

public static void setDBType(Map<String, Object> dataSourceConfigMap) {
    contextHolder.set(dataSourceConfigMap);
}

public static Map<String, Object> getDBType() {
    Map<String, Object> dataSourceConfigMap = contextHolder.get();
    if (dataSourceConfigMap == null) {
        dataSourceConfigMap = new HashMap<String, Object>();
    }
    return dataSourceConfigMap;
}

public static void clearDBType() {
    contextHolder.remove();
}

}

2、建立一個AbstractRoutingDataSource的子類,實現其determineCurrentLookupKey方法,用於決定使用哪一個資料來源。說明一下,這裡實現了ApplicationContextAware介面,用於在Spring載入完成後,注入Spring上下文物件,用於獲取Bean。

/**
* Copyright (c) 2015 - 2016 eya Inc.
* All rights reserved.
*/
package com.eya.pubservice.datasource;

import java.util.Map;

import javax.sql.DataSource;

import org.apache.commons.collections.MapUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

/**
* 動態資料來源父類
* @create ll
* @createDate 2017年3月27日 下午2:38:05
* @update
* @updateDate
*/
public abstract class AbstractDynamicDataSource extends AbstractRoutingDataSource
implements
ApplicationContextAware {

/** 日誌 */
protected Logger logger = LoggerFactory.getLogger(getClass());
/** 預設的資料來源KEY */
protected static final String DEFAULT_DATASOURCE_KEY = "defaultDataSource";

/** 資料來源KEY-VALUE鍵值對 */
public Map<Object, Object> targetDataSources;

/** spring容器上下文 */
private static ApplicationContext ctx;

public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
    ctx = applicationContext;
}

public static ApplicationContext getApplicationContext() {
    return ctx;
}

public static Object getBean(String name) {
    return ctx.getBean(name);
}

/**
 * @param targetDataSources the targetDataSources to set
 */
public void setTargetDataSources(Map<Object, Object> targetDataSources) {
    this.targetDataSources = targetDataSources;
    super.setTargetDataSources(targetDataSources);
    // afterPropertiesSet()方法呼叫時用來將targetDataSources的屬性寫入resolvedDataSources中的
    super.afterPropertiesSet();
}

/**
 * 建立資料來源
 * @param driverClassName 資料庫驅動名稱
 * @param url 連線地址
 * @param username 使用者名稱
 * @param password 密碼
 * @return 資料來源{@link T}
 * @Author : ll. create at 2017年3月27日 下午2:44:34
 */
public abstract T createDataSource(String driverClassName, String url, String username,
                                   String password);

/**
 * 設定系統當前使用的資料來源
 * <p>資料來源為空或者為0時,自動切換至預設資料來源,即在配置檔案中定義的預設資料來源
 * @see org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource#determineCurrentLookupKey()
 */
@Override
protected Object determineCurrentLookupKey() {
    logger.info("【設定系統當前使用的資料來源】");
    Map<String, Object> configMap = DBContextHolder.getDBType();
    logger.info("【當前資料來源配置為:{}】", configMap);
    if (MapUtils.isEmpty(configMap)) {
        // 使用預設資料來源
        return DEFAULT_DATASOURCE_KEY;
    }
    // 判斷資料來源是否需要初始化
    this.verifyAndInitDataSource();
    logger.info("【切換至資料來源:{}】", configMap);
    return configMap.get(DBContextHolder.DATASOURCE_KEY);
}

/**
 * 判斷資料來源是否需要初始化
 * @Author : ll. create at 2017年3月27日 下午3:57:43
 */
private void verifyAndInitDataSource() {
    Map<String, Object> configMap = DBContextHolder.getDBType();
    Object obj = this.targetDataSources.get(configMap.get(DBContextHolder.DATASOURCE_KEY));
    if (obj != null) {
        return;
    }
    logger.info("【初始化資料來源】");
    T datasource = this.createDataSource(configMap.get(DBContextHolder.DATASOURCE_DRIVER)
        .toString(), configMap.get(DBContextHolder.DATASOURCE_URL).toString(),
        configMap.get(DBContextHolder.DATASOURCE_USERNAME).toString(),
        configMap.get(DBContextHolder.DATASOURCE_PASSWORD).toString());
    this.addTargetDataSource(configMap.get(DBContextHolder.DATASOURCE_KEY).toString(),
        datasource);
}

/**
 * 往資料來源key-value鍵值對集合新增新的資料來源
 * @param key 新的資料來源鍵
 * @param dataSource 新的資料來源
 * @Author : ll. create at 2017年3月27日 下午2:56:49
 */
private void addTargetDataSource(String key, T dataSource) {
    this.targetDataSources.put(key, dataSource);
    super.setTargetDataSources(this.targetDataSources);
    // afterPropertiesSet()方法呼叫時用來將targetDataSources的屬性寫入resolvedDataSources中的
    super.afterPropertiesSet();
}

}

3、編寫AbstractDynamicDataSource的實現類,使用com.alibaba.druid.pool.DruidDataSource資料來源。主要實現建立資料來源的方法(createDataSource)

/**
* Copyright (c) 2015 - 2016 eya Inc.
* All rights reserved.
*/
package com.eya.pubservice.datasource;

import java.sql.SQLException;
import java.util.List;

import org.apache.commons.lang3.StringUtils;

import com.alibaba.druid.filter.Filter;
import com.alibaba.druid.pool.DruidDataSource;

/**
* Druid資料來源
*

摘抄自http://www.68idc.cn/help/buildlang/java/20160606618505.html
* @create ll
* @createDate 2017年3月27日 下午2:40:17
* @update
* @updateDate
*/
public class DruidDynamicDataSource extends AbstractDynamicDataSource {

private boolean testWhileIdle = true;
private boolean testOnBorrow = false;
private boolean testOnReturn = false;

// 是否開啟連線洩露自動檢測
private boolean removeAbandoned = false;
// 連線長時間沒有使用,被認為發生洩露時長
private long removeAbandonedTimeoutMillis = 300 * 1000;
// 發生洩露時是否需要輸出 log,建議在開啟連線洩露檢測時開啟,方便排錯
private boolean logAbandoned = false;

// 只要maxPoolPreparedStatementPerConnectionSize>0,poolPreparedStatements就會被自動設定為true,使用oracle時可以設定此值。
//    private int maxPoolPreparedStatementPerConnectionSize = -1;

// 配置監控統計攔截的filters
private String filters; // 監控統計:"stat" 防SQL注入:"wall" 組合使用: "stat,wall"
private List<Filter> filterList;

/*
 * 建立資料來源
 * @see com.cdelabcare.pubservice.datasource.IDynamicDataSource#createDataSource(java.lang.String, java.lang.String, java.lang.String, java.lang.String)
 */
@Override
public DruidDataSource createDataSource(String driverClassName, String url, String username,
                                        String password) {
    DruidDataSource parent = (DruidDataSource) super.getApplicationContext().getBean(
        DEFAULT_DATASOURCE_KEY);
    DruidDataSource ds = new DruidDataSource();
    ds.setUrl(url);
    ds.setUsername(username);
    ds.setPassword(password);
    ds.setDriverClassName(driverClassName);
    ds.setInitialSize(parent.getInitialSize());
    ds.setMinIdle(parent.getMinIdle());
    ds.setMaxActive(parent.getMaxActive());
    ds.setMaxWait(parent.getMaxWait());
    ds.setTimeBetweenConnectErrorMillis(parent.getTimeBetweenConnectErrorMillis());
    ds.setTimeBetweenEvictionRunsMillis(parent.getTimeBetweenEvictionRunsMillis());
    ds.setMinEvictableIdleTimeMillis(parent.getMinEvictableIdleTimeMillis());

    ds.setValidationQuery(parent.getValidationQuery());
    ds.setTestWhileIdle(testWhileIdle);
    ds.setTestOnBorrow(testOnBorrow);
    ds.setTestOnReturn(testOnReturn);

    ds.setRemoveAbandoned(removeAbandoned);
    ds.setRemoveAbandonedTimeoutMillis(removeAbandonedTimeoutMillis);
    ds.setLogAbandoned(logAbandoned);

    // 只要maxPoolPreparedStatementPerConnectionSize>0,poolPreparedStatements就會被自動設定為true,參照druid的原始碼
    ds.setMaxPoolPreparedStatementPerConnectionSize(parent
        .getMaxPoolPreparedStatementPerConnectionSize());

    if (StringUtils.isNotBlank(filters))
        try {
            ds.setFilters(filters);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }

    addFilterList(ds);
    return ds;
}

private void addFilterList(DruidDataSource ds) {
    if (filterList != null) {
        List<Filter> targetList = ds.getProxyFilters();
        for (Filter add : filterList) {
            boolean found = false;
            for (Filter target : targetList) {
                if (add.getClass().equals(target.getClass())) {
                    found = true;
                    break;
                }
            }
            if (!found)
                targetList.add(add);
        }
    }
}

}

4、使用Spring配置預設資料來源。系統執行肯定有一套預設的資料來源(否則動態建立的資料來源資訊從哪裡來呢?上面提到的,動態建立的資料來源資訊是存放在資料庫中的)。這裡我貼出完整的Spring配置。

相關推薦

JAVA使用程式碼建立資料來源實現動態切換

摘要: 案例基於Spring+mybatis。 設計的目的:解決不確定(個數和種類)資料來源的切換問題 2017-06-06 11:31:57補充:近日,在本文的基礎之上,擴充套件了下,使用atomikos來管理事務,保證多資料來源操作時,事務一致性。(htt

一些簡單的例子讓你在Java能更好的學習理解迴圈結構1

一、java中流程控制方式採用三種基本流程結構:順序結構,選擇(分支)結構,迴圈結構。   1、[if-else 結構]    if(1>2){     system.out.println("if條件成立時,執行的程式碼");   }else{     System.out.println("if條

基於httpd-2.2和httpd-2.4配置虛擬主機web站點提供https服務

基於主機名的虛擬主機服務 使用httpd-2.2和httpd-2.4實現> 1.建立httpd服務,要求:> 1) 提供兩個基於名稱的虛擬主機www1, www2;要求每個虛擬主機都有單獨的錯誤日誌和訪問日誌; > 2) 通過www1的/server-status提供狀態信息,且僅允許172

基於httpd-2.2配置虛擬主機web站點提供https服務

基於主機名的虛擬主機配置 為虛擬主機提供https服務 使用httpd-2.2和httpd-2.4實現> 1.建立httpd服務,要求:> 1) 提供兩個基於名稱的虛擬主機www1, www2;要求每個虛擬主機都有單獨的錯誤日誌和訪問日誌; > 2) 通過www1的/server-

Eclipse 4.8.0 photon 如何建立dynamic web project完成Tomcat配置

一、檢視eclipse版本,不同版本遇到的問題可能不一樣。我是Photon Realease(4.8.0)。 步驟:Help -> About Eclipse IDE 開啟後窗口如下可視: 二、建立dynamic web project 在該版本中,是沒

Qt國語言的實現切換國際化

1、建立語言檔案   開啟.pro檔案,加入以下: TRANSLATIONS+=cn.ts   工具->外部->Qt語言家->更新翻譯,將生成翻譯檔案cn.ts 2、編輯翻譯檔案   啟動Linguist(Qt語言家),開啟cn.ts,然後進行

請使用迭代查詢一個list最小和最大值返回一個tuplePython

from collections import Iterable, Iterator def g(): yield 1 yield 2 yield 3 print('Iterable? [1, 2, 3]:', isinstance(

如何git命令建立一個本地分支提交到遠端remote

本地新建分支: 1、git clone 地址(遠端倉庫地址) 2、cd desting(到資料夾路徑)    //clone之後   專案資料夾名稱為desting 3、git remote 4、git checkout -b destingxxx origin/mast

Android列表載入更資料實現點贊

MainActivity package com.gz.test_listview; import android.app.Activity; import android.content.Intent; import android.os.Bundle

java——從鍵盤上輸入一個年份輸入一個月份數字輸出該月份有多少天

/* (程式頭部註釋開始) </p><p>* 程式的版權和版本宣告部分 * Copyright (c) 2011, 煙臺大學計算機學院學生 * 作 者: 李兆慶

js一些常用的校驗工作中用的較附常用正則表示式

做web的經常會遇到一些js校驗,比如身份證呀,手機號呀,郵箱呀等等,分享下我工作中遇到的這些,整理了下 /* * 校驗是否為空(null/空串) */ var checkNull = function(str){ if(str == null || str ==

Java發編程

implement 返回 tile 對象 not seconds dex note 系統調用 1、定義

發工具類等待線程的CountDownLatch

err 更多 表示 amp throw new 所有 barrier let 前言 ??JDK中為了處理線程之間的同步問題,除了提供鎖機制之外,還提供了幾個非常有用的並發工具類:CountDownLatch、CyclicBarrier、Semphore、Exchanger、

基於httpd-2.2和httpd-2.4配置虛擬主機web站點提供https服務

基於主機名配置虛擬主機web站點 使用httpd-2.2和httpd-2.4實現> 1.建立httpd服務,要求:> 1) 提供兩個基於名稱的虛擬主機www1, www2;要求每個虛擬主機都有單獨的錯誤日誌和訪問日誌; > 2) 通過www1的/server-status提供狀態信息,且僅允

基於httpd-2.4配置虛擬主機web站點提供https服務

基於主機名配置虛擬主機web站點 為虛擬主機站點提供https服務 使用httpd-2.2和httpd-2.4實現> 1.建立httpd服務,要求:> 1) 提供兩個基於名稱的虛擬主機www1, www2;要求每個虛擬主機都有單獨的錯誤日誌和訪問日誌; > 2) 通過www1的/s

Java發編程-為什麽要

是否 退出 line 但是 英雄聯盟 編程 ati 效果 執行順序 並發所帶來的好處 1. 並發在某些情況(並不是所有情況)下可以帶來性能上的提升 1) 提升對CPU的使用效率   提升多核CPU的利用率:一般來說一臺主機上的會有多個CPU核心,我們可以創建多個線程,理論上

web調用手機相冊實現動態增加圖片功能

ogr capture form use rod index oot 3.3 jquery 註:經測試h5調用相冊效果有兼容性問題,安卓僅能調用拍照功能(部分安卓可能會調不起來,所以建議用app原生調用),ios可調起拍照和相冊功能。 <html xmlns="

Java發編程Thread詳解

能夠 lds readn 暫停 正在執行 思考 基本 進程 -c 一、概述 在開始學習Thread之前,我們先來了解一下 線程和進程之間的關系: 線程(Thread)是進程的一個實體,是CPU調度和分派的基本單位。 線程不能夠獨立執行,必須依存在應用程序中,由應用程序提供

javaSynchronized同步方法例項

是執行緒獲取物件的鎖,且該執行緒執行物件所屬類中的同步方法! 當然執行緒也是物件,也有鎖,鎖只是物件的一個標誌位。 哪個執行緒獲得了SynClass的物件鎖,則哪個執行緒進入同步方法中執行 publi

Java執行緒之進階篇

一、執行緒池 1.1 執行緒池的建立 1.1.1 ThreadPoolExecutor 1.1.2 執行緒池的分類