ShardingSphere的配置設計


什么是行表達式?

在引入配置體系的學習之前,我們先來介紹 ShardingSphere 框架為開發人員提供的一個輔助功能,這個功能就是行表達式。

行表達式是 ShardingSphere 中用於實現簡化和統一配置信息的一種工具,在日常開發過程中應用得非常廣泛。 它的使用方式非常直觀,只需要在配置中使用 ${expression} 或 $->{expression} 表達式即可。

使用的"ds${0..1}.user${0..1}"就是一個行表達式,用來設置可用的數據源或數據表名稱。基於行表達式語法,${begin..end} 表示的是一個從"begin"到"end"的范圍區間,而多個 ${expression} 之間可以用"."符號進行連接,代表多個表達式數值之間的一種笛卡爾積關系。這樣,如果采用圖形化的表現形式,"ds${0..1}.user${0..1}"表達式最終會解析成這樣一種結果:

image-20201117223007514

當然,類似場景也可以使用枚舉的方式來列舉所有可能值。行表達式也提供了 ${[enum1, enum2,…, enumx]} 語法來表示枚舉值,所以"ds${0..1}.user${0..1}"的效果等同於"ds${[0,1]}.user${[0,1]}"。

同樣,在上一課時中使用到的 ds${age % 2} 表達式,它表示根據 age 字段進行對 2 取模,從而自動計算目標數據源是 ds0 還是 ds1。所以,除了配置數據源和數據表名稱之外,行表達式在 ShardingSphere 中另一個常見的應用場景就是配置各種分片算法,我們會在后續的示例中大量看到這種使用方法。

由於 ${expression} 與 Spring 本身的屬性文件占位符沖突,而 Spring 又是目前主流的開發框架,因此在正式環境中建議你使用 $->{expression} 來進行配置。

ShardingSphere 有哪些核心配置?
對於分庫分表、讀寫分離操作而言,配置的主要任務是完成各種規則的創建和初始化。配置是整個 ShardingSphere 的核心,也是我們在日常開發過程中的主要工作。可以說,只要我們掌握了 ShardingSphere 的核心配置項,就相當於掌握了這個框架的使用方法。那么,ShardingSphere 有哪些核心配置呢?這里以分片引擎為例介紹最常用的幾個配置項,而與讀寫分離、數據脫敏、編排治理相關的配置項我們會在介紹具體的應用場景時再做展開。

ShardingRuleConfiguration
我們在上一課時中已經了解了如何通過框架之間的集成方法來創建一個 DataSource,這個 DataSource 就是我們使用 ShardingSphere 的入口。我們也看到在創建 DataSource 的過程中使用到了一個 ShardingDataSourceFactory 類,這個工廠類的構造函數中需要傳入一個 ShardingRuleConfiguration 對象。顯然,從命名上看,這個 ShardingRuleConfiguration 就是用於分片規則的配置入口。

ShardingRuleConfiguration 中所需要配置的規則比較多,我們可以通過一張圖例來進行簡單說明,在這張圖中,我們列舉了每個配置項的名稱、類型以及個數關系:

image-20201117223031943

這里引入了一些新的概念,包括綁定表、廣播表等,這些概念在下一課時介紹到 ShardingSphere 的分庫分表操作時都會詳細展開,這里不做具體介紹。事實上,對於 ShardingRuleConfiguration 而言,必須要設置的只有一個配置項,即 TableRuleConfiguration。

  • TableRuleConfiguration
    從命名上看,TableRuleConfiguration 是表分片規則配置,但事實上,這個類同時包含了對分庫和分表兩種場景的設置。TableRuleConfiguration 包含很多重要的配置項:

  • actualDataNodes

​ actualDataNodes 代表真實的數據節點,由數據源名+表名組成,支持行表達式。例如,前面介紹的"ds${0..1}.user${0..1}"就是比較典型的一種配置方式。

  • databaseShardingStrategyConfig

databaseShardingStrategyConfig 代表分庫策略,如果不設置則使用默認分庫策略,這里的默認分庫策略就是 ShardingRuleConfiguration 中的 defaultDatabaseShardingStrategyConfig 配置。

  • tableShardingStrategyConfig

和 databaseShardingStrategyConfig 一樣,tableShardingStrategyConfig 代表分表策略,如果不設置也會使用默認分表策略,這里的默認分表策略同樣來自 ShardingRuleConfiguration 中的 defaultTableShardingStrategyConfig 配置。

  • keyGeneratorConfig

keyGeneratorConfig 代表分布式環境下的自增列生成器配置,ShardingSphere 中集成了雪花算法等分布式 ID 的生成器實現。

  • ShardingStrategyConfiguration
    我們注意到,databaseShardingStrategyConfig 和 tableShardingStrategyConfig 的類型都是一個 ShardingStrategyConfiguration 對象。在 ShardingSphere 中,ShardingStrategyConfiguration 實際上是一個空接口,存在一系列的實現類,其中的每個實現類都代表一種分片策略:

image-20201117223729454

在這些具體的分片策略中,通常需要指定一個分片列 shardingColumn 以及一個或多個分片算法 ShardingAlgorithm。當然也有例外,例如 HintShardingStrategyConfiguration 直接使用數據庫的 Hint 機制實現強制路由,所以不需要分片列。我們會在《路由引擎:如何在路由過程中集成多種分片策略和分片算法?》中對這些策略的實現過程做詳細的剖析。

  • KeyGeneratorConfiguration
    可以想象,對於一個自增列而言,KeyGeneratorConfiguration 中首先需要指定一個列名 column。同時,因為 ShardingSphere 中內置了一批自增列的實現機制(例如雪花算法 SNOWFLAKE 以及通用唯一識別碼 UUID),所以需要通過一個 type 配置項進行指定。最后,我們可以利用 Properties 配置項來指定自增值生成過程中所需要的相關屬性配置。關於這一點,我們在上一課時中也看到了示例,即雪花算法中配置 workerId 為 33。

基於以上核心配置項,我們已經可以完成日常開發過程中常見的分庫分表操作。當然,對於不同的開發人員,如何采用某一個特定的方式將這些配置項信息集成到業務代碼中,也存在着不同的訴求。因此,ShardingSphere 中也提供了一系列的配置方式供開發人員進行選擇。

ShardingSphere 提供了哪些配置方式?

從 Java 代碼到配置文件,ShardingSphere 提供了 4 種配置方式,用於不同的使用場景,分別是:

  • Java 代碼配置

  • Yaml 配置

  • Spring 命名空間配置

  • Spring Boot配置

我們來看一下這四種配置的具體方法。

Java 代碼配置
Java 代碼配置是使用 ShardingSphere 所提供的底層 API 來完成配置系統構建的原始方式。在上一課時中,我們已經看到了如何初始化 ShardingRuleConfiguration 和 TableRuleConfiguration 類,並通過 ShardingDataSourceFactory 創建目標 DataSource 的具體方式,這里不再展開。

在日常開發中,我們一般不會直接使用 Java 代碼來完成 ShardingSphere 配置體系的構建。一方面,如果使用 Java 代碼來實現配置,一旦有變動就需要重新編譯代碼並發布,不利於實現配置信息的動態化管理和系統的持續集成。另一方面,代碼級別的配置方式也比較繁瑣,不夠直接且容易出錯,維護性也不好。

當然,也可能有例外情況。一種情況是,如果我們需要和其他框架進行更加底層的集成或定制化開發時,往往只能采用 Java 代碼才能達到理想的效果。同時,對於剛接觸 ShardingSphere 的開發人員而言,基於框架提供的 API 進行開發更加有利於快速掌握框架提供的各種類之間的關聯關系和類層結構。

Yaml 配置
Yaml 配置是 ShardingSphere 所推崇的一種配置方式。Yaml 的語法和其他高級語言類似,並且可以非常直觀地描述多層列表和對象等數據形態,特別適合用來表示或編輯數據結構和各種配置文件。

在語法上,常見的"!!"表示實例化該類;以"-"開頭的多行構成一個數組;以":"表示鍵值對;以"#"表示注釋。關於 Yaml 語法的更多介紹可以參考百度百科 https://baike.baidu.com/item/YAML。請注意,Yaml 大小寫敏感,並使用縮進表示層級關系。 這里給出一個基於 ShardingSphere 實現讀寫分離場景下的配置案例:

dataSources:

  dsmaster: !!com.alibaba.druid.pool.DruidDataSource

    driverClassName: com.mysql.jdbc.Driver

    url: jdbc:mysql://119.3.52.175:3306/dsmaster

    username: root

    password: root

  dsslave0: !!com.alibaba.druid.pool.DruidDataSource

    driverClassName: com.mysql.jdbc.Driver

    url: jdbc:mysql://119.3.52.175:3306/dsslave0

    username: root

    password: root

  dsslave1: !!com.alibaba.druid.pool.DruidDataSource

    driverClassName: com.mysql.jdbc.Driver

    url: jdbc:mysql://119.3.52.175:3306/dsslave1

    username: root

    password: root

 

masterSlaveRule:

  name: health_ms

  masterDataSourceName: dsmaster

  slaveDataSourceNames: [dsslave0, dsslave1]

可以看到,這里配置了 dsmaster、dsslave0 和 dsslave1 這三個 DataSource,然后針對每個 DataSource 分別設置了它們的驅動信息。最后,基於這三個 DataSource 配置了一個 masterSlaveRule 規則,用於指定具體的主從架構。

在 ShardingSphere 中,我們可以把配置信息存放在一個 .yaml 配置文件中,並通過加載這個配置文件來完成配置信息的解析。這種機制為開發人員高效管理配置信息提供了更多的靈活性和可定制性。在今天內容的最后,我們會詳細剖析這一機制的實現原理。

Spring 命名空間配置

我們可以通過自定義配置標簽實現方案來擴展 Spring 的命名空間,從而在 Spring 中嵌入各種自定義的配置項。Spring 框架從 2.0 版本開始提供了基於 XML Schema 的風格來定義 Javabean 的擴展機制。通過 XML Schema 的定義,把一些原本需要通過復雜的 Javabean 組合定義的配置形式,用一種更加簡單而可讀的方式呈現出來。基於 Scheme 的 XML 由三部分構成,我們用一個示例說明:

<beans

    ...

    http://shardingsphere.apache.org/schema/shardingsphere/masterslave

    http://shardingsphere.apache.org/schema/shardingsphere/masterslave/master-slave.xsd">

 

	<bean id=" dsmaster " class=" com.alibaba.druid.pool.DruidDataSource" destroy-method="close">

        <property name="driverClassName" value="com.mysql.jdbc.Driver"/>

        <property name="url" value="jdbc:mysql://localhost:3306/dsmaster"/>

        <property name="username" value="root"/>

        <property name="password" value="root"/>

    </bean>

    <bean id="dsslave0" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">

        <property name="driverClassName" value="com.mysql.jdbc.Driver"/>

        <property name="url" value="jdbc:mysql://localhost:3306/dsslave0"/>

        <property name="username" value="root"/>

        <property name="password" value="root"/>

    </bean>

   <bean id="dsslave1" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">

        <property name="driverClassName" value="com.mysql.jdbc.Driver"/>

        <property name="url" value="jdbc:mysql://localhost:3306/dsslave1"/>

        <property name="username" value="root"/>

        <property name="password" value="root"/>

    </bean>

 

    <master-slave:load-balance-algorithm id="randomStrategy" type="RANDOM" />

    <master-slave:data-source id="masterSlaveDataSource" master-data-source-name="dsmaster" slave-data-source-names="dsslave0, dsslave1" strategy-ref="randomStrategy" />

</beans>

在這段代碼中,我們在 Spring 中引入了 master-slave 這個新的命名空間,並完成了負載均衡算法和三個主從 DataSource 的設置。

Spring Boot 配置

Spring Boot 已經成為 Java 領域最流行的開發框架,提供了約定優於配置的設計理念。通常,開發人員可以把配置項放在 application.properties 文件中。同時,為了便於對配置信息進行管理和維護,Spring Boot 也提供了 profile 的概念,可以基於 profile 來靈活組織面對不同環境或應用場景的配置信息。在采用 profile 時,配置文件的命名方式有一定的約定:

spring.shardingsphere.datasource.names=dsmaster,dsslave0,dsslave1

 

spring.shardingsphere.datasource.dsmaster.type=com.alibaba.druid.pool.DruidDataSource

spring.shardingsphere.datasource.dsmaster.driver-class-name=com.mysql.jdbc.Driver

spring.shardingsphere.datasource.dsmaster.url=jdbc:mysql://localhost:3306/dsmaster

spring.shardingsphere.datasource.dsmaster.username=root

spring.shardingsphere.datasource.dsmaster.password=root

 

spring.shardingsphere.datasource.dsslave0.type=com.alibaba.druid.pool.DruidDataSource

spring.shardingsphere.datasource.dsslave0.driver-class-name=com.mysql.jdbc.Driver

spring.shardingsphere.datasource.dsslave0.url=jdbc:mysql://localhost:3306/dsslave0

spring.shardingsphere.datasource.dsslave0.username=root

spring.shardingsphere.datasource.dsslave0.password=root

 

spring.shardingsphere.datasource.dsslave1.type=com.alibaba.druid.pool.DruidDataSource

spring.shardingsphere.datasource.dsslave1.driver-class-name=com.mysql.jdbc.Driver

spring.shardingsphere.datasource.dsslave1.url=jdbc:mysql://localhost:3306/dsslave1

spring.shardingsphere.datasource.dsslave1.username=root

spring.shardingsphere.datasource.dsslave1.password=root

 

spring.shardingsphere.masterslave.load-balance-algorithm-type=random

spring.shardingsphere.masterslave.name=health_ms

spring.shardingsphere.masterslave.master-data-source-name=dsmaster

spring.shardingsphere.masterslave.slave-data-source-names=dsslave0,dsslave1

通過這些不同的配置方式,開發人員可以基於自己擅長的或開發框架所要求的方式,靈活完成各項配置工作。在本課程中的后續內容中,我們會組合使用 Yaml 配置和 Spring Boot 配置這兩種配置方式來介紹 ShardingSphere 的具體使用方式。

ShardingSphere 的配置體系是如何實現的?

盡管在日常開發過程中很少使用,但在前面介紹的四種配置方式中,Java 代碼配置的實現方式最容易理解,我們可以通過各個配置類的調用關系來梳理 ShardingSphere 提供的配置功能。所以,為了深入理解配置體系的實現原理,我們還是選擇從 ShardingRuleConfiguration 類進行切入。

ShardingRuleConfiguration 配置體系

對於 ShardingSphere 而言,配置體系的作用本質上就是用來初始化 DataSource 等 JDBC 對象。例如,ShardingDataSourceFactory 就是基於傳入的數據源 Map、ShardingRuleConfiguration 以及 Properties 來創建一個 ShardingDataSource 對象:

image-20201117224107162

在 ShardingSphere 中,所有規則配置類都實現了一個頂層接口 RuleConfiguration。RuleConfiguration 是一個空接口,ShardingRuleConfiguration 就是這個接口的實現類之一,專門用來處理分片引擎的應用場景。下面這段代碼就是 ShardingRuleConfiguration 類的實現過程:

public final class ShardingRuleConfiguration implements RuleConfiguration {

    //表分片規則列表

    private Collection<TableRuleConfiguration> tableRuleConfigs = new LinkedList<>();

    //綁定表規則列表

    private Collection<String> bindingTableGroups = new LinkedList<>();

    //廣播表規則列表

    private Collection<String> broadcastTables = new LinkedList<>();

    //默認數據源

    private String defaultDataSourceName;

    //默認分庫策略

    private ShardingStrategyConfiguration defaultDatabaseShardingStrategyConfig;

    //默認分表策略

    private ShardingStrategyConfiguration defaultTableShardingStrategyConfig;

    //默認自增列值生成器

    private KeyGeneratorConfiguration defaultKeyGeneratorConfig;

    //讀寫分離規則

    private Collection<MasterSlaveRuleConfiguration> masterSlaveRuleConfigs = new LinkedList<>();

    //數據脫敏規則

    private EncryptRuleConfiguration encryptRuleConfig;

}

可以看到,ShardingRuleConfiguration 中包含的就是一系列的配置類定義,通過前面的內容介紹,我們已經明白了這些配置類的作用和使用方法。其中,核心的 TableRuleConfiguration 定義也比較簡單,主要包含了邏輯表、真實數據節點以及分庫策略和分表策略的定義:

public final class TableRuleConfiguration {

    //邏輯表

    private final String logicTable;

    //真實數據節點

    private final String actualDataNodes;

    //分庫策略

    private ShardingStrategyConfiguration databaseShardingStrategyConfig;

    //分表策略

    private ShardingStrategyConfiguration tableShardingStrategyConfig;

    //自增列生成器

    private KeyGeneratorConfiguration keyGeneratorConfig;

    public TableRuleConfiguration(final String logicTable) {

        this(logicTable, null);

    }

    public TableRuleConfiguration(final String logicTable, final String actualDataNodes) {

        Preconditions.checkArgument(!Strings.isNullOrEmpty(logicTable), "LogicTable is required.");

        this.logicTable = logicTable;

        this.actualDataNodes = actualDataNodes;

    }

}

事實上,無論采用哪種配置方式,所有的配置項都是在這些核心配置類的基礎之上進行封裝和轉換。基於 Spring 命名空間和 Spring Boot 配置的使用方式比較常見,這兩種方式的實現原理都依賴於 ShardingSphere 與這兩個框架的集成方式,我會在后續課時中做詳細展開。而 Yaml 配置是 ShardingSphere 非常推崇的一種使用方式,為此,ShardingSphere 在內部對 Yaml 配置的應用場景有專門的處理。

YamlShardingRuleConfiguration 配置體系

在 ShardingSphere 源碼的 sharding-core-common 工程中,存在一個包結構 org.apache.shardingsphere.core.yaml.config,在這個包結構下包含着所有與 Yaml 配置相關的實現類。

與 RuleConfiguration 一樣,ShardingSphere 同樣提供了一個空的 YamlConfiguration 接口。這個接口的實現類非常多,但我們發現其中包含了唯一的一個抽象類 YamlRootRuleConfiguration,顯然,這個類是 Yaml a配置體系中的基礎類。 在這個 YamlRootRuleConfiguration 中,包含着數據源 Map 和 Properties:

image-20201117224355508

在上面這段代碼中,我們發現少了 ShardingRuleConfiguration 的對應類,其實,這個類的定義在 YamlRootRuleConfiguration 的子類 YamlRootShardingConfiguration 中,它的類名 YamlShardingRuleConfiguration 就是在 ShardingRuleConfiguration 上加了一個 Yaml 前綴,如下面這段代碼所示:

image-20201117224547763

接下來,我們來到 YamlShardingRuleConfiguration 類,發現它所包含的變量與 ShardingRuleConfiguration 類中的變量存在一致對應關系,這些 Yaml 配置類都位於 org.apache.shardingsphere.core.yaml.config.sharding 包中:

image-20201117224457264

那么這個 YamlShardingRuleConfiguration 是怎么構建出來的呢?這就要來到 YamlShardingDataSourceFactory 工廠類,這個工廠類實際上是對 ShardingDataSourceFactory 類的進一步封裝,下面這段代碼就演示了這一過程:

public final class YamlShardingDataSourceFactory {

    public static DataSource createDataSource(final File yamlFile) throws SQLException, IOException {

        YamlRootShardingConfiguration config = YamlEngine.unmarshal(yamlFile, YamlRootShardingConfiguration.class);

        return ShardingDataSourceFactory.createDataSource(config.getDataSources(), new ShardingRuleConfigurationYamlSwapper().swap(config.getShardingRule()), config.getProps());

	}

	…

}

可以看到 createDataSource 方法的輸入參數是一個 File 對象,我們通過這個 File 對象構建出 YamlRootShardingConfiguration 對象,然后再通過 YamlRootShardingConfiguration 對象獲取了 ShardingRuleConfiguration 對象,並交由 ShardingDataSourceFactory 完成目標 DataSource 的構建。這里的調用關系有點復雜,我們來梳理整個過程的類層結構,如下圖所示:

image-20201117224748676

顯然,這里引入了兩個新的工具類,YamlEngine 和 YamlSwapper。我們來看一下它們在整個流程中起到的作用。

YamlEngine 和 YamlSwapper

YamlEngine 的作用是將各種形式的輸入內容轉換成一個 Yaml 對象,這些輸入形式包括 File、字符串、byte[] 等。YamlEngine 包含了一批 unmarshal/marshal 方法來完成數據的轉換。以 File 輸入為例,unmarshal 方法通過加載 FileInputStream 來完成 Yaml 對象的構建:

    public static <T extends YamlConfiguration> T unmarshal(final File yamlFile, final Class<T> classType) throws IOException {
        try (
                FileInputStream fileInputStream = new FileInputStream(yamlFile);
                InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream, "UTF-8")
        ) {
            return new Yaml(new Constructor(classType)).loadAs(inputStreamReader, classType);
        }
    }

當在 unmarshal 方法中傳入想要的 classType 時,我們就可以獲取這個 classType 對應的實例。在 YamlShardingDataSourceFactory 中我們傳入了 YamlRootShardingConfiguration 類型,這樣我們就將得到一個 YamlRootShardingConfiguration 的類實例 YamlShardingRuleConfiguration。

在得到 YamlShardingRuleConfiguration 之后,下一步需要實現將 YamlShardingRuleConfiguration 轉換為 ShardingRuleConfiguration。為了完成這種具有對應關系的類地轉換,ShardingSphere 還專門提供了一批轉換器類,ShardingRuleConfigurationYamlSwapper 就是其中之一。ShardingRuleConfigurationYamlSwapper 實現了 YamlSwapper 接口:

public interface YamlSwapper<Y extends YamlConfiguration, T> {

    Y swap(T data);

    T swap(Y yamlConfiguration);

}

可以看到這里提供了一對方法完成兩種數據結構之間的相互轉換,ShardingRuleConfigurationYamlSwapper 中對這兩個方法的實現過程也比較直接。以目標對象為 ShardingRuleConfiguration 的 swap 方法為例,代碼結構基本上就是完成了 YamlShardingRuleConfiguration 與 ShardingRuleConfiguration 中對應字段的一對一轉換

	@Override

    public ShardingRuleConfiguration swap(final YamlShardingRuleConfiguration yamlConfiguration) {

        ShardingRuleConfiguration result = new ShardingRuleConfiguration();

        for (Entry<String, YamlTableRuleConfiguration> entry : yamlConfiguration.getTables().entrySet()) {

            YamlTableRuleConfiguration tableRuleConfig = entry.getValue();

            tableRuleConfig.setLogicTable(entry.getKey());

            result.getTableRuleConfigs().add(tableRuleConfigurationYamlSwapper.swap(tableRuleConfig));

        }

        result.setDefaultDataSourceName(yamlConfiguration.getDefaultDataSourceName());

        result.getBindingTableGroups().addAll(yamlConfiguration.getBindingTables());

        result.getBroadcastTables().addAll(yamlConfiguration.getBroadcastTables());

        if (null != yamlConfiguration.getDefaultDatabaseStrategy()) {

            result.setDefaultDatabaseShardingStrategyConfig(shardingStrategyConfigurationYamlSwapper.swap(yamlConfiguration.getDefaultDatabaseStrategy()));

        }

        if (null != yamlConfiguration.getDefaultTableStrategy()) {

            result.setDefaultTableShardingStrategyConfig(shardingStrategyConfigurationYamlSwapper.swap(yamlConfiguration.getDefaultTableStrategy()));

        }

        if (null != yamlConfiguration.getDefaultKeyGenerator()) {

            result.setDefaultKeyGeneratorConfig(keyGeneratorConfigurationYamlSwapper.swap(yamlConfiguration.getDefaultKeyGenerator()));

        }

        Collection<MasterSlaveRuleConfiguration> masterSlaveRuleConfigs = new LinkedList<>();

        for (Entry<String, YamlMasterSlaveRuleConfiguration> entry : yamlConfiguration.getMasterSlaveRules().entrySet()) {

            YamlMasterSlaveRuleConfiguration each = entry.getValue();

            each.setName(entry.getKey());

            masterSlaveRuleConfigs.add(masterSlaveRuleConfigurationYamlSwapper.swap(entry.getValue()));

        }

        result.setMasterSlaveRuleConfigs(masterSlaveRuleConfigs);

        if (null != yamlConfiguration.getEncryptRule()) {

            result.setEncryptRuleConfig(encryptRuleConfigurationYamlSwapper.swap(yamlConfiguration.getEncryptRule()));

        }

        return result;

    }

這樣,我們就從外部的 Yaml 文件中獲取了一個 ShardingRuleConfiguration 對象,然后可以使用 ShardingDataSourceFactory 工廠類完成目標 DataSource 的創建過程。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM