有時候我們需要把數據存放到多個數據庫中,但是一個數據源只能訪問一個數據庫。想訪問不同的數據庫,那么就需要切換不同的數據源。有時候我們要切換的數據源是未知的,在程序運行的過程中才能知道要訪問哪一個數據庫,這時候就需要使用動態增加數據源的方法。我們可以先在配置文件中配置一個默認數據源,程序運行過程中需要訪問其它數據庫的時候,就動態的創建新的數據源織入到程序當中,讓程序使用該新建的數據源。將這些新建的數據源緩存起來,后面需要用到就可以獲取到,實現切換不同的數據源。主要的步驟為:“首先在配置文件中配置一個默認數據源”、“新建DynamicDataSource類來實現切換不同的數據源”、“DynamicDataSource實現動態增加數據源”、“使用Spring的AOP,將線程ThreadLocal的dbName變量清空,防止影響下一次訪問數據庫”、“在我們的產品中,不同的公司對應着不同的數據庫dbName,所以可以根據dbName創建不同的數據源”。
1、 首先在配置文件中配置一個默認數據源。
在ApplicationContext.xml中配置數據源。
(1) 配置sqlSessionFactory,指定數據源為dynamicDataSource:
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dynamicDataSource" />
<property name="configLocation" value="classpath:mybatis-config.xml"></property>
</bean>
(2) 配置dynamicDataSource,為自定義的DynamicDataSource類,負責切換數據源和動態增加數據源,指定了默認數據源為jdbcDataSource_nbr_bx:
<!--動態數據源的配置 -->
<bean id="dynamicDataSource" class="com.bx.erp.action.interceptor.DynamicDataSource" primary="true">
<property name="targetDataSources">
<map key-type="java.lang.String">
<entry value-ref="jdbcDataSource_nbr_bx" key="jdbcDataSource_nbr_bx" />
</map>
</property>
<property name="defaultTargetDataSource" ref="jdbcDataSource_nbr_bx" />
</bean>
(3) 配置默認數據源jdbcDataSource_nbr_bx:
<bean id="jdbcDataSource_nbr_bx" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName">
<value>${driverClassName}</value>
</property>
<property name="url">
<value>jdbc:mysql://localhost:3306/nbr_bx?useUnicode=true&serverTimezone=Asia/Shanghai&characterEncoding=utf8&zeroDateTimeBehavior=CONVERT_TO_NULL</value>
</property>
<property name="username">
<value>${db.nbx.mysql.username}</value>
</property>
<property name="password">
<value>${db.nbx.mysql.password}</value>
</property>
</bean>
2、 新建DynamicDataSource類來實現切換不同的數據源。
Spring框架有一個AbstractRoutingDataSource類,它可以在程序運行的時候,把某個數據源動態織入到程序中,以便訪問不同的數據庫。它有一個determineCurrentLookupKey方法,該方法返回一個鍵key,通過這個key,在數據源集合中獲取其中一個數據源。最后根據獲取到的數據源去訪問數據庫。所以DynamicDataSource類可以繼承AbstractRoutingDataSource,實現determineCurrentLookupKey方法,通過返回不同的key實現切換不同的數據源,默認使用配置文件中的數據源。
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
String dbName = DataSourceContextHolder.getDbName();
if (dbName == null) {
dbName = DataSourceContextHolder.DATASOURCE_jdbcDataSource_nbr_bx; // 默認使用公共DB:nbr_bx
} else {
this.selectDataSource(dbName);
if (dbName.equals(DataSourceContextHolder.DATASOURCE_jdbcDataSource_nbr_bx)) {
dbName = DataSourceContextHolder.DATASOURCE_jdbcDataSource_nbr_bx;
}
}
logger.debug("--------> using datasource " + dbName);
return dbName;
}
……
3、 DynamicDataSource實現動態增加數據源。
我們在配置文件中只配置了一個默認數據源,那么需要增加數據源,才能夠切換不同的數據源訪問不同的數據庫。
增加一個數據源的步驟如下:
例如我們要訪問一個名稱為nbr_test的數據庫,這個訪問操作對應着某一個線程,線程要訪問哪一個數據庫,我們可以使用ThreadLocal類來設置和獲取。ThreadLocal類可以防止一個線程的變量被其它線程修改。
我們用DataSourceContextHolder封裝ThreadLocal:
public class DataSourceContextHolder{
public static final String DATASOURCE_jdbcDataSource_nbr_bx = "jdbcDataSource_nbr_bx";
private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();
public static void setDbName(String dbName) {
contextHolder.set(dbName);
}
public static String getDbName() {
return ((String) contextHolder.get());
}
public static void clearDbName() {
contextHolder.remove();
}
}
在訪問數據庫前,我們設置訪問nbr_test這個數據庫:
DataSourceContextHolder.setDbName(dbName);
DynamicDataSource的determineCurrentLookupKey方法獲取到當前線程的數據庫名稱dbName:
String dbName = DataSourceContextHolder.getDbName();
DynamicDataSource的selectDataSource方法從 targetDataSources這個hashmap中查找是否存在這個數據源,因為現在還沒有這個數據源,所以需要去創建,后面創建好了之后,也會把創建好的數據源存到這個targetDataSources中:
/** @describe 數據源存在時不做處理,不存在時創建新的數據源鏈接,並將新數據鏈接添加至緩存 */
public void selectDataSource(String dbName) {
String sid = DataSourceContextHolder.getDbName();
if (DataSourceContextHolder.DATASOURCE_jdbcDataSource_nbr_bx.equals(dbName)) {
DataSourceContextHolder.setDbName(DataSourceContextHolder.DATASOURCE_jdbcDataSource_nbr_bx);
return;
}
Object obj = this._targetDataSources.get(dbName);
if (obj != null && sid.equals(dbName)) {
return;
} else {
BasicDataSource dataSource = this.getDataSource(dbName);
if (null != dataSource)
this.setDataSource(dbName, dataSource);
}
}
創建一個數據源:
public BasicDataSource getDataSource(String serverId) {
return (BasicDataSource) createDataSource(serverId);
}
/** 該方法為同步方法,防止並發創建兩個相同的數據庫 使用雙檢鎖的方式,防止並發 */
private synchronized DataSource createDataSource(String dbName) {
String url = String.format(DB_MYSQL_URL, dbName);
//
BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName(DRIVER_CLASS_NAME);
dataSource.setUrl(url);
dataSource.setUsername(DB_MYSQL_USERNAME);
dataSource.setPassword(DB_MYSQL_PASSWORD);
dataSource.setTestWhileIdle(true);
dataSource.setValidationQuery("select 1");
dataSource.setTestOnBorrow(true);
return dataSource;
}
數據庫連接信息放在了一個配置文件中,通過注解@Value來獲取,訪問不同的的數據庫只是dbName這個字段不同,所以可以使用String.format(DB_MYSQL_URL, dbName)來修改連接url,訪問不同的數據庫。
注解:
@Value("${driverClassName}")
private String DRIVER_CLASS_NAME;
@Value("${db.mysql.url}")
private String DB_MYSQL_URL;
@Value("${db.mysql.username}")
private String DB_MYSQL_USERNAME;
@Value("${db.mysql.password}")
private String DB_MYSQL_PASSWORD;
配置文件:
# 數據庫驅動
driverClassName=com.mysql.cj.jdbc.Driver
# 數據庫URL
db.mysql.url=jdbc:mysql://localhost:3306/%s?useUnicode=true&serverTimezone=Asia/Shanghai&characterEncoding=utf8&zeroDateTimeBehavior=CONVERT_TO_NULL
# 數據庫用戶名
db.mysql.username.encryption=81A3FD464E18C4497A79CE7CC9D5B660
db.nbx.mysql.username.encryption=81A3FD464E18C4497A79CE7CC9D5B660
# 密碼
db.mysql.password.encryption=16CCEF25E22FA42E89821B7B27858DE26DD8BFF1139FF2C70BECCA88E373F809
db.nbx.mysql.password.encryption=16CCEF25E22FA42E89821B7B27858DE26DD8BFF1139FF2C70BECCA88E373F809
創建好一個數據源后,把它放到集合緩存中,下次就不用重新創建了,同時告訴父類AbstractRoutingDataSource,要使用當前這個數據源訪問數據庫:
BasicDataSource dataSource = this.getDataSource(dbName);
if (null != dataSource)
this.setDataSource(dbName, dataSource);
public void setDataSource(String serverId, BasicDataSource dataSource) {
this.addTargetDataSource(serverId, dataSource);
DataSourceContextHolder.setDbName(serverId);
}
public void addTargetDataSource(String key, BasicDataSource dataSource) {
this._targetDataSources.put(key, dataSource);
this.setTargetDataSources(this._targetDataSources);
}
public void setTargetDataSources(Map<Object, Object> targetDataSources) {
this._targetDataSources = targetDataSources;
super.setTargetDataSources(this._targetDataSources);
afterPropertiesSet();
}
4、 使用Spring的AOP,將線程ThreadLocal的dbName變量清空,防止影響下一次訪問數據庫。
@Aspect // for aop
@Component // for auto scan
@Order(0) // 在事務前執行
public class DataSourceInterceptor {
@Pointcut("execution(public * com.bx.erp.action.bo.*.*(..))")
public void dataSourceSlave() {
};
@Before("dataSourceSlave()")
public void before(JoinPoint jp) {
// System.out.println("進入切面");
}
@After("dataSourceSlave()")
public void removeDataSoruce(JoinPoint joinPoint) throws Throwable {
DataSourceContextHolder.clearDbName();
}
5、 在我們的產品中,不同的公司對應着不同的數據庫dbName,所以可以根據dbName創建不同的數據源。
Company company = getCompanyFromSession(session);
String dbName = company.getDbName();
DataSourceContextHolder.setDbName(dbName);