基於註解的Spring多數據源配置和使用

前一段時間研究了一下spring多數據源的配置和使用,爲了後期從多個數據源拉取數據定時進行數據分析和報表統計作準備。因爲以前作過的項目都是單數據源的,沒有遇到這種場景,因此也一直沒有去了解過如何配置多數據源。
後來發現其實基於spring來配置和使用多數據源仍是比較簡單的,由於spring框架已經預留了這樣的接口能夠方便數據源的切換。
先看一下spring獲取數據源的源碼:javascript

能夠看到AbstractRoutingDataSource獲取數據源以前會先調用determineCurrentLookupKey方法查找當前的lookupKey,這個lookupKey就是數據源標識。
所以經過重寫這個查找數據源標識的方法就可讓spring切換到指定的數據源了。
第一步:建立一個DynamicDataSource的類,繼承AbstractRoutingDataSource並重寫determineCurrentLookupKey方法,代碼以下:java

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.lang.Nullable;

/**
 * Created by WJ on 2018/3/23 0023.
 */
public class DynamicDataSource extends AbstractRoutingDataSource {


    @Nullable
    @Override
    protected Object determineCurrentLookupKey() {
        // 從自定義的位置獲取數據源標識
        return DynamicDataSourceHolder.getDataSource();
    }
}

第二步:建立DynamicDataSourceHolder用於持有當前線程中使用的數據源標識,代碼以下:spring

/**
 * Created by WJ on 2018/3/23 0023.
 */
public class DynamicDataSourceHolder {

    /**
     * 注意:數據源標識保存在線程變量中,避免多線程操做數據源時互相干擾
     */
    private static final ThreadLocal<String> THREAD_DATA_SOURCE = new ThreadLocal<String>();

    public static String getDataSource() {
        return THREAD_DATA_SOURCE.get();
    }

    public static void setDataSource(String dataSource) {
        THREAD_DATA_SOURCE.set(dataSource);
    }

    public static void clearDataSource() {
        THREAD_DATA_SOURCE.remove();
    }
}

第三步:配置多個數據源和第一步裏建立的DynamicDataSource的bean,簡化的配置以下:sql

<!-- 阿里巴巴Druid數據庫鏈接池 -->
<!--建立數據源1,鏈接數據庫db1 -->
<bean id="dataSource1" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
    <!-- 基本屬性 url、user、password -->
    <property name="url" value="${db1.jdbc.url}" />
    <property name="username" value="${db1.jdbc.user}" />
    <property name="password" value="${db1.jdbc.password}" />

    <!-- 配置初始化大小、最小、最大 -->
    <property name="initialSize" value="1" />
    <property name="minIdle" value="1" />
    <property name="maxActive" value="${jdbc.pool.maxActive}" />

    <!-- 配置獲取鏈接等待超時的時間 -->
    <property name="maxWait" value="60000" />

    <!-- 配置間隔多久才進行一次檢測,檢測須要關閉的空閒鏈接,單位是毫秒 -->
    <property name="timeBetweenEvictionRunsMillis" value="60000" />

    <!-- 配置一個鏈接在池中最小生存的時間,單位是毫秒 -->
    <property name="minEvictableIdleTimeMillis" value="300000" />

    <property name="validationQuery" value="select 1 from dual" />
    <property name="testWhileIdle" value="true" />
    <property name="testOnBorrow" value="false" />
    <property name="testOnReturn" value="false" />

    <!-- 打開PSCache,而且指定每一個鏈接上PSCache的大小 -->
    <property name="poolPreparedStatements" value="false" />
    <property name="maxPoolPreparedStatementPerConnectionSize" value="20" />

    <!-- 配置druid監控統計攔截的filters -->
    <!-- stat 統計監控信息,wall 防sql注入,slf4j 日誌打印 -->
    <!--
     <property name="filters" value="stat,wall,slf4j,config"/>
     -->
    <property name="filters" value="stat,slf4j,config"/>
    <!-- 解壓數據庫鏈接密碼 -->
    <property name="connectionProperties" value="config.decrypt=false" />
</bean>

<!--建立數據源2,鏈接數據庫db2 -->
<bean id="dataSource2" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
    <!-- 基本屬性 url、user、password -->
    <property name="url" value="${db2.jdbc.url}" />
    <property name="username" value="${db2.jdbc.user}" />
    <property name="password" value="${db2.jdbc.password}" />

    <!-- 配置初始化大小、最小、最大 -->
    <property name="initialSize" value="1" />
    <property name="minIdle" value="1" />
    <property name="maxActive" value="${jdbc.pool.maxActive}" />

    <!-- 配置獲取鏈接等待超時的時間 -->
    <property name="maxWait" value="60000" />

    <!-- 配置間隔多久才進行一次檢測,檢測須要關閉的空閒鏈接,單位是毫秒 -->
    <property name="timeBetweenEvictionRunsMillis" value="60000" />

    <!-- 配置一個鏈接在池中最小生存的時間,單位是毫秒 -->
    <property name="minEvictableIdleTimeMillis" value="300000" />

    <property name="validationQuery" value="select 1 from dual" />
    <property name="testWhileIdle" value="true" />
    <property name="testOnBorrow" value="false" />
    <property name="testOnReturn" value="false" />

    <!-- 打開PSCache,而且指定每一個鏈接上PSCache的大小 -->
    <property name="poolPreparedStatements" value="false" />
    <property name="maxPoolPreparedStatementPerConnectionSize" value="20" />

    <!-- 配置druid監控統計攔截的filters -->
    <!-- stat 統計監控信息,wall 防sql注入,slf4j 日誌打印 -->
    <!--
     <property name="filters" value="stat,wall,slf4j,config"/>
     -->
    <property name="filters" value="stat,slf4j,config"/>
    <!-- 解壓數據庫鏈接密碼 -->
    <property name="connectionProperties" value="config.decrypt=false" />
</bean>


<bean id="dynamicDataSource" class="com.keeprisk.core.support.DynamicDataSource">
    <property name="targetDataSources">
        <map key-type="java.lang.String">
            <!-- 指定lookupKey和與之對應的數據源 -->
            <entry key="dataSource1" value-ref="dataSource1"></entry>
            <entry key="dataSource2" value-ref="dataSource2"></entry>
        </map>
    </property>
    <!-- 這裏能夠指定默認的數據源 -->
    <property name="defaultTargetDataSource" ref="dataSource1" />
</bean>

到這裏已經可使用多數據源了,在操做數據庫以前只要DynamicDataSourceHolder.setDataSource("dataSource2")便可切換到數據源2並對數據庫db2進行操做了。數據庫

示例代碼以下:express

@Service
public class DataServiceImpl implements DataService {
     @Autowired
     private DataMapper dataMapper;
 
    @Override
     public List<Map<String, Object>> getList1() {
         // 沒有指定,則默認使用數據源1
         return dataMapper.getList1();
     }
 
     @Override
     public List<Map<String, Object>> getList2() {
         // 指定切換到數據源2
         DynamicDataSourceHolder.setDataSource("dataSource2");
         return dataMapper.getList2();
     }
 }

--------------------------------------------華麗的分割線-----------------------------------多線程

可是問題來了,若是每次切換數據源時都調用DynamicDataSourceHolder.setDataSource("xxx")就顯得十分繁瑣了,並且代碼量大了很容易會遺漏,後期維護起來也比較麻煩。能不能直接經過註解的方式指定須要訪問的數據源呢,好比在dao層使用@DataSource("xxx")就指定訪問數據源xxx?固然能夠!前提是,再加一點額外的配置^_^。
首先,咱們得定義一個名爲DataSource的註解,代碼以下:app

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
 * Created by WJ on 2018/3/23 0023.
 */
@Target({ TYPE, METHOD })
@Retention(RUNTIME)
public @interface DataSource {
    String value();
}

而後,定義AOP切面以便攔截全部帶有註解@DataSource的方法,取出註解的值做爲數據源標識放到DynamicDataSourceHolder的線程變量中:框架

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Method;

/**
 * Created by WJ on 2018/3/23 0023.
 */
public class DataSourceAspect {
    private static final Logger logger = LoggerFactory.getLogger(DataSourceAspect.class);

    /**
     * 攔截目標方法,獲取由@DataSource指定的數據源標識,設置到線程存儲中以便切換數據源
     *
     * @param point
     * @throws Exception
     */
    public void intercept(JoinPoint point) throws Exception {
        Class<?> target = point.getTarget().getClass();
        MethodSignature signature = (MethodSignature) point.getSignature();
        // 默認使用目標類型的註解,若是沒有則使用其實現接口的註解
        for (Class<?> clazz : target.getInterfaces()) {
            resolveDataSource(clazz, signature.getMethod());
        }
        resolveDataSource(target, signature.getMethod());
    }

    /**
     * 提取目標對象方法註解和類型註解中的數據源標識
     *
     * @param clazz
     * @param method
     */
    private void resolveDataSource(Class<?> clazz, Method method) {
        try {
            Class<?>[] types = method.getParameterTypes();
            // 默認使用類型註解
            if (clazz.isAnnotationPresent(DataSource.class)) {
                DataSource source = clazz.getAnnotation(DataSource.class);
                DynamicDataSourceHolder.setDataSource(source.value());
            }
            // 方法註解能夠覆蓋類型註解
            Method m = clazz.getMethod(method.getName(), types);
            if (m != null && m.isAnnotationPresent(DataSource.class)) {
                DataSource source = m.getAnnotation(DataSource.class);
                DynamicDataSourceHolder.setDataSource(source.value());
            }
        } catch (Exception e) {
            logger.error("{}:",clazz,e);

        }
    }
}

最後在spring配置文件中配置攔截規則就能夠了,好比攔截service層或者dao層的全部方法:dom

<bean id="dataSourceAspect" class="com.keeprisk.core.support.DataSourceAspect"/>

<aop:config>
    <aop:aspect ref="dataSourceAspect">
        <!-- 攔截全部dao方法 -->
        <aop:pointcut id="dataSourcePointcut" expression="execution(* com.keeprisk.core.dao.*.*(..))"/>
        <aop:before pointcut-ref="dataSourcePointcut" method="intercept" />
    </aop:aspect>
</aop:config>

OK,這樣就能夠直接在類或者方法上使用註解@DataSource來指定數據源,不須要每次都手動設置了。

示例代碼以下:

@Service
// 默認DataServiceImpl下的全部方法均訪問數據源1
@DataSource("dataSource1")
public class DataServiceImpl implements DataService {
    @Autowired
    private DataMapper dataMapper;

    @Override
    public List<Map<String, Object>> getList1() {
        // 不指定,則默認使用數據源1
        return dataMapper.getList1();
    }

    @Override
    // 覆蓋類上指定的,使用數據源2
    @DataSource("dataSource2")
    public List<Map<String, Object>> getList2() {
        return dataMapper.getList2();
    }

}

提示:註解@DataSource既能夠加在方法上,也能夠加在接口或者接口的實現類上,優先級別:方法>實現類>接口。也就是說若是接口、接口實現類以及方法上分別加了@DataSource註解來指定數據源,則優先以方法上指定的爲準。

說明:若在Service層訪問多個數據源,在類中註解上數據源,可能不太方便,全部能夠將@DataSource 放在dao層,例如:

import com.keeprisk.core.domain.WeixinCity;
import com.keeprisk.core.support.DataSource;

@DataSource("dataSource1")
public interface WeixinCityMapper {
    int deleteByPrimaryKey(Integer id);

    int insertSelective(WeixinCity record);

    WeixinCity selectByPrimaryKey(Integer id);

    int updateByPrimaryKeySelective(WeixinCity record);
    
    List<WeixinCity> selectByPage(WeixinCity record);
}
相關文章
相關標籤/搜索