首頁 > 軟體

spring註解 @PropertySource設定資料來源全流程

2022-03-25 19:00:07

@PropertySource資料來源設定

一般在設定資料來源是都會使用xml的方式注入,key-value在properties中管理;spring4.X已有著比較完善的註解來替換xml的設定方式。

使用xml設定資料來源

通常我們使用xml設定資料來源,使用SpEL獲取properties中的設定。

applicationContext.xml 中設定 dataSource 及 PreferencesPlaceholderConfigurer,使用 PropertyPlaceholderConfigurer進行Bean屬性替換

<bean id="configProperties" class="org.springframework.beans.factory.config.PropertiesFactoryBean">
        <property name="locations">
            <list>
                <value>classpath:/jdbc.properties</value>
            </list>
        </property>
        <property name="fileEncoding" value="utf-8"/>
    </bean>
<bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PreferencesPlaceholderConfigurer">
    <property name="properties" ref="configProperties" />
</bean>
<!-- 使用proxool連線池的資料來源, -->
<bean id="dataSource" class="org.logicalcobwebs.proxool.ProxoolDataSource">
    <!-- 資料來源的別名 -->
    <property name="alias" value="${proxool.alias}" /> 
    <!-- 驅動 -->
    <property name="driver" value="${proxool.driver}" /> 
    <!-- 連結URL  -->
    <property name="driverUrl" value="${proxool.driverUrl}" /> 
    <!-- 使用者名稱-->
    <property name="user" value="${proxool.user}" />
    <!-- 密碼 -->
    <property name="password" value="${proxool.password}" /> 
    <!-- 最大連結數-->
    <property name="maximumConnectionCount" value="${proxool.maximumConnectionCount}" /> 
    <!-- 最小連結數 -->
    <property name="minimumConnectionCount" value="${proxool.minimumConnectionCount}" /> 
    <!-- ...(略) -->
</bean> 

jdbc.properties

proxool.alias=mySql
proxool.driver=com.mysql.jdbc.Driver
proxool.driverUrl=jdbc:mysql://localhost:3306/test?characterEncoding=utf8
proxool.user=root
proxool.password=root
proxool.maximumActiveTime=1200
proxool.maximumConnectionCount=50
#...

使用javaBean設定資料來源

DataSourceConfiguration類是資料來源的javaBean設定方式,@Configuratio註解當前類,

spring啟動時會掃描被@Configuratio註解的類,注入當前類中設定的方法bean;

當然別忘了啟用註解掃描:

<context:annotation-config/>  
<context:component-scan base-package="com.XXX.test.dateSource"></context:component-scan>

@value註解讀取設定

@value中可以直接使用SpEL,獲取properties設定,成員變數也不需要getter、setter,不過還是有一個前提,需要設定xml:

<bean id="configProperties" class="org.springframework.beans.factory.config.PropertiesFactoryBean">
        <property name="locations">
            <list>
                <value>classpath:/jdbc.properties</value>
            </list>
        </property>
        <property name="fileEncoding" value="utf-8"/>
    </bean>
<bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PreferencesPlaceholderConfigurer">
    <property name="properties" ref="configProperties" />
</bean>

@Bean註解:spring掃面當前類時,注入每個有@Bean註解的方法的返回值Bean, name屬性預設為返回值類類名首字母小寫,這裡自己設定name。

package com.XXX.test.dateSource;
import org.logicalcobwebs.proxool.ProxoolDataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuratio
public class DataSourceConfiguration{
    @Value("${proxool.alias}")
    private String alias;
    @Value("${proxool.driver}")
    private String driver;
    @Value("${proxool.driverUrl}")
    private String driverUrl;
    @Value("${proxool.user}")
    private String user;
    @Value("${proxool.password}")
    private String password;
    //...
    @Bean(name="dataSource")
    public ProxoolDataSource dataSource(){
         ProxoolDataSource proxoolDataSource = new ProxoolDataSource();
         proxoolDataSource.setDriver(driver);
         proxoolDataSource.setDriverUrl(driverUrl);
         proxoolDataSource.setUser(user);
         proxoolDataSource.setPassword(password);
         //...
         return proxoolDataSource;
     }
 }

這時dataSource已被注入,使用時可註解注入,如下:

    @Autowired
    private ProxoolDataSource dataSource;

@PropertySource註解讀取設定

@PropertySource註解當前類,引數為對應的組態檔路徑,這種方式載入組態檔,可不用在xml中設定PropertiesFactoryBean引入jdbc.properties,使用時方便得多,DataSourceConfiguration不再需要成員變數,取而代之的是需要注入一個Environment環境設定,使用env.getProperty(key)獲取資料:

package com.XXX.test.dateSource;
import org.logicalcobwebs.proxool.ProxoolDataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;
@Configuratio
@PropertySource("classpath:/jdbc.properties")
public class DataSourceConfiguration{
    @Autowired
    private Environment env;
    @Bean(name="dataSource")
    public ProxoolDataSource dataSource(){
         ProxoolDataSource proxoolDataSource = new ProxoolDataSource();
         proxoolDataSource.setDriver(env.getProperty("proxool.alias"));
         proxoolDataSource.setDriverUrl(env.getProperty("proxool.driver"));
         proxoolDataSource.setUser(env.getProperty("proxool.user"));
         proxoolDataSource.setPassword(env.getProperty("proxool.password"));
         //...
         return proxoolDataSource;
     }
 }

這裡主要是說明註解的用法,所以沒有具體體現資料來源全部引數的設定。對於有強迫症的來說若專案中所有bean都使用註解,幾乎不太希望僅dataSource用xml類設定,換成類的方式類設定強迫感就消失了! 

註解的spring多資料來源設定及使用

前一段時間研究了一下spring多資料來源的設定和使用,為了後期從多個資料來源拉取資料定時進行資料分析和報表統計做準備。由於之前做過的專案都是單資料來源的,沒有遇到這種場景,所以也一直沒有去了解過如何設定多資料來源。

後來發現其實基於spring來設定和使用多資料來源還是比較簡單的,因為spring框架已經預留了這樣的介面可以方便資料來源的切換。

先看一下spring獲取資料來源的原始碼

可以看到AbstractRoutingDataSource獲取資料來源之前會先呼叫determineCurrentLookupKey方法查詢當前的lookupKey,這個lookupKey就是資料來源標識。

因此通過重寫這個查詢資料來源標識的方法就可以讓spring切換到指定的資料來源了。

第一步:建立一個DynamicDataSource的類

繼承AbstractRoutingDataSource並重寫determineCurrentLookupKey方法,程式碼如下:

public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        // 從自定義的位置獲取資料來源標識
        return DynamicDataSourceHolder.getDataSource();
    }
}

第二步:建立DynamicDataSourceHolder

用於持有當前執行緒中使用的資料來源標識,程式碼如下:

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,簡化的設定如下:

<!--建立資料來源1,連線資料庫db1 -->
<bean id="dataSource1" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <property name="driverClassName" value="${db1.driver}" />
    <property name="url" value="${db1.url}" />
    <property name="username" value="${db1.username}" />
    <property name="password" value="${db1.password}" />
</bean>
<!--建立資料來源2,連線資料庫db2 -->
<bean id="dataSource2" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <property name="driverClassName" value="${db2.driver}" />
    <property name="url" value="${db2.url}" />
    <property name="username" value="${db2.username}" />
    <property name="password" value="${db2.password}" />
</bean>
<!--建立資料來源3,連線資料庫db3 -->
<bean id="dataSource3" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <property name="driverClassName" value="${db3.driver}" />
    <property name="url" value="${db3.url}" />
    <property name="username" value="${db3.username}" />
    <property name="password" value="${db3.password}" />
</bean>
<bean id="dynamicDataSource" class="com.test.context.datasource.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>
            <entry key="dataSource3 " value-ref="dataSource3"></entry>
        </map>
    </property>
    <!-- 這裡可以指定預設的資料來源 -->
    <property name="defaultTargetDataSource" ref="dataSource1" />
</bean>

到這裡已經可以使用多資料來源了,在運算元據庫之前只要DynamicDataSourceHolder.setDataSource("dataSource2")即可切換到資料來源2並對資料庫db2進行操作了。

範例程式碼如下:

@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();
    }
    @Override
    public List<Map<String, Object>> getList3() {
        // 指定切換到資料來源3
        DynamicDataSourceHolder.setDataSource("dataSource3");
        return dataMapper.getList3();
    }
}

----------------------------華麗的分割線----------------------------

但是問題來了,如果每次切換資料來源時都呼叫DynamicDataSourceHolder.setDataSource("xxx")就顯得十分繁瑣了,而且程式碼量大了很容易會遺漏,後期維護起來也比較麻煩。能不能直接通過註解的方式指定需要存取的資料來源呢,比如在dao層使用@DataSource("xxx")就指定存取資料來源xxx?當然可以!前提是,再加一點額外的設定^_^。

首先,我們得定義一個名為DataSource的註解,程式碼如下:

@Target({ TYPE, METHOD })
@Retention(RUNTIME)
public @interface DataSource {
    String value();
}

然後,定義AOP切面以便攔截所有帶有註解@DataSource的方法,取出註解的值作為資料來源標識放到DynamicDataSourceHolder的執行緒變數中:

public class DataSourceAspect {
    /**
     * 攔截目標方法,獲取由@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) {
            System.out.println(clazz + ":" + e.getMessage());
        }
    }
}

最後在spring組態檔中設定攔截規則就可以了,比如攔截service層或者dao層的所有方法:

<bean id="dataSourceAspect" class="com.test.context.datasource.DataSourceAspect" />
    <aop:config>
        <aop:aspect ref="dataSourceAspect">
            <!-- 攔截所有service方法 -->
            <aop:pointcut id="dataSourcePointcut" expression="execution(* com.test.*.dao.*.*(..))"/>
            <aop:before pointcut-ref="dataSourcePointcut" method="intercept" />
        </aop:aspect>
    </aop:config>
</bean>

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();
    }
    @Override
    // 覆蓋類上指定的,使用資料來源3
    @DataSource("dataSource3")
    public List<Map<String, Object>> getList3() {
        return dataMapper.getList3();
    }
}

提示:註解@DataSource既可以加在方法上,也可以加在介面或者介面的實現類上,優先順序別:方法>實現類>介面。也就是說如果介面、介面實現類以及方法上分別加了@DataSource註解來指定資料來源,則優先以方法上指定的為準。

以上為個人經驗,希望能給大家一個參考,也希望大家多多支援it145.com。


IT145.com E-mail:sddin#qq.com