Sentinel高級

Sentinel高級

sentinel和springCloud整合

減少開發的複雜度,對大部分的主流框架,例如:Web Servlet、Dubbo、Spring Cloud、gRPC、Spring WebFlux、Reactor等做了適配。只需要引入對應應用的以來即可方便地整合Sentinel。

如果要實現SpringCloud和Sentinel的整合,可以通過引入Spring Cloud Alibaba Sentinel來更方便得整合Sentinel。

Spring Cloud Alibaba是阿里巴巴集團提供的,致力於提供微服務開發的一站式解決方案。Spring Cloud Alibaba默認爲Sentinel整合Servlet、RestTemplate、FeignClient和Spring WebFlux、Sentinel在Spring Cloud生態中,不僅補全了hystrix在Servlet和RestTemplate這一塊的空白,而且完全兼容hystrix在FeignClient種限流降級的用法,並且支持運用時靈活地配置和調整限流降級規則。

需求

使用SpringCloud + Sentinel實現訪問http://localhost:8080/ann路徑的流量控制。

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    id("org.springframework.boot") version "2.3.7.RELEASE"
    id("io.spring.dependency-management") version "1.0.10.RELEASE"
    kotlin("jvm") version "1.3.72"
    kotlin("plugin.spring") version "1.3.72"
    java
}

group = "xyz.ytfs"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_1_8

repositories {
    mavenCentral()
}

extra["springCloudAlibabaVersion"] = "2.2.2.RELEASE"

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("com.alibaba.cloud:spring-cloud-starter-alibaba-sentinel")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    testImplementation("org.springframework.boot:spring-boot-starter-test") {
        exclude(group = "org.junit.vintage", module = "junit-vintage-engine")
    }
}

dependencyManagement {
    imports {
        mavenBom("com.alibaba.cloud:spring-cloud-alibaba-dependencies:${property("springCloudAlibabaVersion")}")
    }
}

tasks.withType<Test> {
    useJUnitPlatform()
}

tasks.withType<KotlinCompile> {
    kotlinOptions {
        freeCompilerArgs = listOf("-Xjsr305=strict")
        jvmTarget = "1.8"
    }
}
@SentinelResource(value = "spring_cloud_sentinel_test", blockHandler = "exceptionHandler")
    @GetMapping("ann")
    fun springCloudSentinelTest(): String {
        return "hello Spring-Cloud-Sentinel_test"
    }


    fun exceptionHandler(bx: BlockException): String {
        return "系統繁忙,請稍後重試"
    }

Sentinel對Feign的支持

Sentinel適配了Feign組件,如果想使用,除了引入spring-cloud-starter-alibaba-sentinel的依賴外還需要2個步驟:

  • 配置文件打開Sentinel對Feign的支持:feign.sentinel.enabled=true
  • 加入spring-cloud-starter-openfeign依賴Sentinel starter中的自動化配置類生效

需求

實現sentinel_feign_client微服務通過Feign訪問sentinel_feign_provider微服務的流量控制

創建spring-cloud-parent父工程

  1. 依賴文件
extra["springCloudVersion"] = "Hoxton.SR9"
extra["springCloudAlibabaVersion"] = "2.2.2.RELEASE"

group = "xyz.ytfs"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_1_8

allprojects {
    repositories {
        maven(url = "http://maven.aliyun.com/nexus/content/groups/public/")
        mavenCentral()
        maven { url = uri("https://repo.spring.io/snapshot") }
        maven { url = uri("https://repo.spring.io/milestone") }
    }
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    testImplementation("org.springframework.boot:spring-boot-starter-test") {
        exclude(group = "org.junit.vintage", module = "junit-vintage-engine")
    }
}

創建eureka-server註冊中心子工程

  1. 依賴添加
dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    implementation("org.springframework.cloud:spring-cloud-starter-netflix-eureka-server")
    testImplementation("org.springframework.boot:spring-boot-starter-test") {
        exclude(group = "org.junit.vintage", module = "junit-vintage-engine")
    }
}

dependencyManagement {
    imports {
        mavenBom("com.alibaba.cloud:spring-cloud-alibaba-dependencies:${property("springCloudAlibabaVersion")}")
        mavenBom("org.springframework.cloud:spring-cloud-dependencies:${property("springCloudVersion")}")
    }
}
  1. 啓動類和配置文件的修改
@EnableEurekaServer  //在啓動類上添加此註解,表示開啓eureka註冊中心服務
@SpringBootApplication
class EurekaServerApplication

fun main(args: Array<String>) {
    runApplication<EurekaServerApplication>(*args)
}
# 應用名稱
spring.application.name=eureka-server
server.port=8060

#eureka配置
eureka.client.service-url.defaultZone=http://127.0.0.1:8060/eureka
#不拉去服務
eureka.client.fetch-registry=false
#不註冊自己
eureka.client.register-with-eureka=false

創建sentinel-feign-client

  1. 添加依賴

    implementation("com.alibaba.cloud:spring-cloud-starter-alibaba-sentinel")
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    implementation("org.springframework.cloud:spring-cloud-starter-netflix-eureka-client")
    implementation("org.springframework.cloud:spring-cloud-starter-openfeign")
    testImplementation("org.springframework.boot:spring-boot-starter-test") {
        exclude(group = "org.junit.vintage", module = "junit-vintage-engine")
    }
    
  2. 創建代理的個接口

    @FeignClient(value="sentinel-feign-provider", fallback = FallBackService::class)
    interface ProviderClient {
    
        @GetMapping("hello")
        fun hello(): String
    }
    
  3. 創建controller

    @RestController
    class TestController(val providerClient: ProviderClient) {
    
        @GetMapping("hello")
        fun hello(): String{
            return this.providerClient.hello()
        }
    
    }
    
  4. 創建降級相應示例

    @Service
    /**
    * 實現代理接口
    **/
    class FallBackService : ProviderClient {
    
        override fun hello(): String {
    
            return "系統繁忙,請稍後重試"
        }
    }
    
  5. 配置文件

    # 應用名稱
    spring:
      application:
        name: sentinel-feign-client
      cloud:
        sentinel:
          transport:
            dashboard: localhost:8045
    eureka:
      client:
        service-url:
          defaultZone: http://127.0.0.1:8060/eureka
    server:
      port: 8061
      # 開啓Sentinel對feign的支持
    feign:
      sentinel:
        enabled: true
    
  6. 啓動類添加註解

    @SpringBootApplication
    @EnableFeignClients
    @EnableDiscoveryClient
    class SentinelFeignClientApplication
    
    fun main(args: Array<String>) {
        runApplication<SentinelFeignClientApplication>(*args)
    }
    

創建sentinel-feign-provider

  1. 添加依賴

    dependencies {
        implementation("org.springframework.boot:spring-boot-starter-web")
        implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
        implementation("org.jetbrains.kotlin:kotlin-reflect")
        implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
        implementation("org.springframework.cloud:spring-cloud-starter-netflix-eureka-client")
        implementation("org.springframework.cloud:spring-cloud-starter-openfeign")
        testImplementation("org.springframework.boot:spring-boot-starter-test") {
            exclude(group = "org.junit.vintage", module = "junit-vintage-engine")
        }
    }
    
    dependencyManagement {
        imports {
            mavenBom("org.springframework.cloud:spring-cloud-dependencies:${property("springCloudVersion")}")
            mavenBom("org.springframework.cloud:spring-cloud-dependencies:${property("springCloudVersion")}")
        }
    }
    
  2. 修改配置文件

    # 應用名稱
    spring.application.name=sentinel-feign-provider
    # 應用服務 WEB 訪問端口
    server.port=8062
    
    eureka.client.service-url.defaultZone=http://127.0.0.1:8060/eureka
    
  3. 啓動類增加註解

    @SpringBootApplication
    @EnableDiscoveryClient
    @EnableFeignClients
    class SentinelFeignProviderApplication
    
    fun main(args: Array<String>) {
        runApplication<SentinelFeignProviderApplication>(*args)
    }
    
  4. 提供接口

    @RestController
    class ProviderController {
    
    
       @GetMapping("hello")
        fun hello(): String {
           return "Hello Feign Sentintl"
        }
    
    }
    

運行測試

啓動項目,在Sentinel控制檯中增加關於資源流控規則.Sentinel和Feign整合時,流控規則的編寫形式爲:http請求方式:協議//服務名稱/請求路徑跟參數 例如GET:http://sentinel-feign-provider/hello

image-20210305114854627

Sentinel對Spring Cloud Gateway的支持

從1.6.0版本開始,Sentinel提供了Spring Cloud Gateway的適配模塊,可以提供兩種資源維度的限流:

  • route維度:即在Spring的配置文件種配置的路由條目,資源名對應相應的routeId
  • 自定義API維度:用戶可以利用Sentinel提供的API來自定義一些API分組

微服務網關搭建

在上面基礎上創建

創建子工程sentinel-gateway,在build.gradle.kts中引入依賴

implementation("org.springframework.cloud:spring-cloud-starter-gateway")

整合Sentinel

  1. 導入依賴

    implementation("com.alibaba.cloud:spring-cloud-starter-alibaba-sentinel")
    implementation("com.alibaba.cloud:spring-cloud-alibaba-sentinel-gateway")
    
  2. 創建一個配置類,配置流控降級回調操作

@Configuration
class GatewayConfiguration {


    @PostConstruct
    fun doInit() {
        GatewayCallbackManager.setBlockHandler(BlockRequestHandler {
                serverWebExchange: ServerWebExchange?, throwable: Throwable? ->
            return@BlockRequestHandler ServerResponse.status(200).bodyValue("系統繁忙,請稍後再試!")
         })
    }

}
  1. 路由的配置

    # 配置路由
    spring.cloud.gateway.routes[0].id=sentinel-feign-gateway
    # lb代表的是 Load Balance負載均衡,如果是一個服務(auth-service)多個實例,實現自主分發
    spring.cloud.gateway.routes[0].uri=lb://sentinel-feign-client
    # 匹配路徑
    spring.cloud.gateway.routes[0].predicates[0]=Path=/hello/**
    # 配置Stentinel的控制檯地址
    spring.cloud.sentinel.transport.dashboard=http://localhost:8045
    

流量控制實現

Sentinel的所有規則都可以在內存太中動態的查詢及修改,修改之後立即生效。同時Sentinel也提供相關API,供您來定製自己的規則策略。

Sentinel主要支持一下幾種規則:

  • 流量控制規則
  • 熔斷降級規則
  • 系統保護規則
  • 來源訪問控制規則
  • 動態規劃擴展

流量控制規則實現

流量控制(Flow Control) ,其原理是監控應用流量的QPS或併發線程數等指標,當達到指定的閥值時對流量進行控制,以免被瞬時的流量高峯沖垮,從而保障應用的高可用性。

流量控制主要兩種方式:

  • 併發線程數:併發線程數限流用於保護業務線程數不被耗盡
  • QPS:當QPS超過某個閥值的時候,則採取措施進行流量控制

一條限流規則主要由幾個因素組成,我們可以組合這些元素來實現不同的限流效果:

  • resource:資源名,即限流規則的作用對象
  • count: 限流閾值
  • grade: 限流閾值類型(QPS 或併發線程數)
  • limitApp: 流控針對的調用來源,若爲 default 則不區分調用來源
  • strategy: 調用關係限流策略
  • controlBehavior: 流量控制效果(直接拒絕、Warm Up、勻速排隊)

直接拒絕

直接拒絕:(RuleConstant.CONTROL_BEHAVIOR_DEFAULT)方式是默認的流量控制方式,當QPS超過任意規則的閾值後,新的請求就會被立即拒絕,拒絕方式爲拋出FlowException。這種方式適用於對系統處理能力確切已知的情況下,比如通過壓測確定了系統的準確水位時。

Warm Up

Warm Up(RuleConstant.CONTROL_BEHAVIOR_WARM_UP)方式,即預熱/冷啓動方式。當系統長期處於低水位的情況下,當流量突然增加時,直接把系統拉昇到高水位可能瞬間把系統壓垮。通過"冷啓動",讓通過的流量緩慢增加,在一定時間內逐漸增加到閾值上限,給冷系統一個預熱的時間,避免冷系統被壓垮。

勻速排隊

勻速排隊(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER)方式會嚴格控制請求通過的間隔時間,也即是讓請求以均勻的速度通過,對應的是漏桶算法。該方式的作用如下圖所示:

image-20210305150301683

這種方式主要用於處理間隔性突發的流量,例如消息隊列。想象一下這樣的場景,在某一秒有大量的請求到來,而接下來的幾秒則處於空閒狀態,我們希望系統能夠在接下來的空閒期間逐漸處理這些請求,而不是在第一秒直接拒絕多餘的請求。

注意:勻速排隊模式暫時不支持 QPS > 1000 的場景。

熔斷降級

概述

除了流量控制以外,對調用鏈路中不穩定的資源進行熔斷降級也是保障高可用的重要措施之一。一個服務常常會調用別的模塊,可能是另外的一個遠程服務、數據庫,或者第三方 API 等。例如,支付的時候,可能需要遠程調用銀聯提供的 API;查詢某個商品的價格,可能需要進行數據庫查詢。然而,這個被依賴服務的穩定性是不能保證的。如果依賴的服務出現了不穩定的情況,請求的響應時間變長,那麼調用服務的方法的響應時間也會變長,線程會產生堆積,最終可能耗盡業務自身的線程池,服務本身也變得不可用。

chain

現代微服務架構都是分佈式的,由非常多的服務組成。不同服務之間相互調用,組成複雜的調用鏈路。以上的問題在鏈路調用中會產生放大的效果。複雜鏈路上的某一環不穩定,就可能會層層級聯,最終導致整個鏈路都不可用。因此我們需要對不穩定的弱依賴服務調用進行熔斷降級,暫時切斷不穩定調用,避免局部不穩定因素導致整體的雪崩。熔斷降級作爲保護自身的手段,通常在客戶端(調用端)進行配置。

注意:本文檔針對 Sentinel 1.8.0 及以上版本。1.8.0 版本對熔斷降級特性進行了全新的改進升級,請使用最新版本以更好地利用熔斷降級的能力。

重要的屬性

Field 說明 默認值
resource 資源名,即規則的作用對象
grade 熔斷策略,支持慢調用比例/異常比例/異常數策略 慢調用比例
count 慢調用比例模式下爲慢調用臨界 RT(超出該值計爲慢調用);異常比例/異常數模式下爲對應的閾值
timeWindow 熔斷時長,單位爲 s
minRequestAmount 熔斷觸發的最小請求數,請求數小於該值時即使異常比率超出閾值也不會熔斷(1.7.0 引入) 5
statIntervalMs 統計時長(單位爲 ms),如 60*1000 代表分鐘級(1.8.0 引入) 1000 ms
slowRatioThreshold 慢調用比例閾值,僅慢調用比例模式有效(1.8.0 引入)

熔斷降級策略詳解

Sentinel 提供以下幾種熔斷策略:

  • 慢調用比例 (SLOW_REQUEST_RATIO):選擇以慢調用比例作爲閾值,需要設置允許的慢調用 RT(即最大的響應時間),請求的響應時間大於該值則統計爲慢調用。當單位統計時長(statIntervalMs)內請求數目大於設置的最小請求數目,並且慢調用的比例大於閾值,則接下來的熔斷時長內請求會自動被熔斷。經過熔斷時長後熔斷器會進入探測恢復狀態(HALF-OPEN 狀態),若接下來的一個請求響應時間小於設置的慢調用 RT 則結束熔斷,若大於設置的慢調用 RT 則會再次被熔斷。
  • 異常比例 (ERROR_RATIO):當單位統計時長(statIntervalMs)內請求數目大於設置的最小請求數目,並且異常的比例大於閾值,則接下來的熔斷時長內請求會自動被熔斷。經過熔斷時長後熔斷器會進入探測恢復狀態(HALF-OPEN 狀態),若接下來的一個請求成功完成(沒有錯誤)則結束熔斷,否則會再次被熔斷。異常比率的閾值範圍是 [0.0, 1.0],代表 0% - 100%。
  • 異常數 (ERROR_COUNT):當單位統計時長內的異常數目超過閾值之後會自動進行熔斷。經過熔斷時長後熔斷器會進入探測恢復狀態(HALF-OPEN 狀態),若接下來的一個請求成功完成(沒有錯誤)則結束熔斷,否則會再次被熔斷。

注意異常降級僅針對業務異常,對 Sentinel 限流降級本身的異常(BlockException)不生效。爲了統計異常比例或異常數,需要通過 Tracer.trace(ex) 記錄業務異常。示例:

Entry entry = null;
try {
  entry = SphU.entry(key, EntryType.IN, key);

  // Write your biz code here.
  // <<BIZ CODE>>
} catch (Throwable t) {
  if (!BlockException.isBlockException(t)) {
    Tracer.trace(t);
  }
} finally {
  if (entry != null) {
    entry.exit();
  }
}

開源整合模塊,如 Sentinel Dubbo Adapter, Sentinel Web Servlet Filter 或 @SentinelResource 註解會自動統計業務異常,無需手動調用。

熔斷器事件監聽

Sentinel 支持註冊自定義的事件監聽器監聽熔斷器狀態變換事件(state change event)。示例:

EventObserverRegistry.getInstance().addStateChangeObserver("logging",
    (prevState, newState, rule, snapshotValue) -> {
        if (newState == State.OPEN) {
            // 變換至 OPEN state 時會攜帶觸發時的值
            System.err.println(String.format("%s -> OPEN at %d, snapshotValue=%.2f", prevState.name(),
                TimeUtil.currentTimeMillis(), snapshotValue));
        } else {
            System.err.println(String.format("%s -> %s at %d", prevState.name(), newState.name(),
                TimeUtil.currentTimeMillis()));
        }
    });

代碼實現

//定義熔斷資源和回調函數
@SentinelResource(value = "degrade_rule", blockHandler = "exceptionHandler")
@GetMapping("degrade")
fun ruleHello(): String {
    return "hello rule  sentinel"
}

//降級方法
fun exceptionHandler(e: BlockException): String {
    e.printStackTrace()
    return "系統繁忙,請稍後!,降級"
}

@PostConstruct
fun initDegradeRule() {
    //1、創建存放熔斷規則的集合
    val rules: ArrayList<DegradeRule> = ArrayList()
    //2、創建熔斷規則
    val rule: DegradeRule = DegradeRule()
    //設置熔斷資源名稱
    rule.resource = "degrade_rule"
    //閥值
    rule.count = 0.01
    //降級的時間,單位S
    rule.timeWindow = 10
    //設置熔斷類型
    /**
     * 當資源的平均響應時間超過閥值(DegradeRule中的count以毫秒爲單位)之後,資源進入準降級狀態。
     * 然後持續進入5個請求,他們的RT都持續超過這個閥值,
     * 那麼在接下來的時間窗口(DegradeRule中的timeWindow,以s秒爲單位)之內
     * 將拋出DegradeException
     */
    rule.grade = RuleConstant.DEGRADE_GRADE_RT
    //3、將熔斷規則存入集合
    rules.add(rule)
    //4、加載熔斷規則集合
    DegradeRuleManager.loadRules(rules)
}

黑白名單控制

很多時候,我們需要根據調用來源來判斷該次請求是否允許放行,這時候可以使用 Sentinel 的來源訪問控制(黑白名單控制)的功能。來源訪問控制根據資源的請求來源(origin)限制資源是否通過,若配置白名單則只有請求來源位於白名單內時纔可通過;若配置黑名單則請求來源位於黑名單時不通過,其餘的請求通過。

調用方信息通過 ContextUtil.enter(resourceName, origin) 方法中的 origin 參數傳入。

規則配置

來源訪問控制規則(AuthorityRule)非常簡單,主要有以下配置項:

  • resource:資源名,即限流規則的作用對象。
  • limitApp:對應的黑名單/白名單,不同 origin 用 , 分隔,如 appA,appB
  • strategy:限制模式,AUTHORITY_WHITE 爲白名單模式,AUTHORITY_BLACK 爲黑名單模式,默認爲白名單模式。

示例

比如我們希望控制對資源 test 的訪問設置白名單,只有來源爲 appAappB 的請求才可通過,則可以配置如下白名單規則:

AuthorityRule rule = new AuthorityRule();
rule.setResource("test");
rule.setStrategy(RuleConstant.AUTHORITY_WHITE);
rule.setLimitApp("appA,appB");
AuthorityRuleManager.loadRules(Collections.singletonList(rule));

動態規則

規則

Sentinel 的理念是開發者只需要關注資源的定義,當資源定義成功後可以動態增加各種流控降級規則。Sentinel 提供兩種方式修改規則:

  • 通過 API 直接修改 (loadRules)
  • 通過 DataSource 適配不同數據源修改

手動通過 API 修改比較直觀,可以通過以下幾個 API 修改不同的規則:

FlowRuleManager.loadRules(List<FlowRule> rules); // 修改流控規則
DegradeRuleManager.loadRules(List<DegradeRule> rules); // 修改降級規則

手動修改規則(硬編碼方式)一般僅用於測試和演示,生產上一般通過動態規則源的方式來動態管理規則。

DataSource 擴展

上述 loadRules() 方法只接受內存態的規則對象,但更多時候規則存儲在文件、數據庫或者配置中心當中。DataSource 接口給我們提供了對接任意配置源的能力。相比直接通過 API 修改規則,實現 DataSource 接口是更加可靠的做法。

我們推薦通過控制檯設置規則後將規則推送到統一的規則中心,客戶端實現 ReadableDataSource 接口端監聽規則中心實時獲取變更,流程如下:

image-20210311224330771

DataSource 擴展常見的實現方式有:

  • 拉模式:客戶端主動向某個規則管理中心定期輪詢拉取規則,這個規則中心可以是 RDBMS、文件,甚至是 VCS 等。這樣做的方式是簡單,缺點是無法及時獲取變更;
  • 推模式:規則中心統一推送,客戶端通過註冊監聽器的方式時刻監聽變化,比如使用 Nacos、Zookeeper 等配置中心。這種方式有更好的實時性和一致性保證。

Sentinel 目前支持以下數據源擴展:

示例

1、啓動本地的nacos

nacos下載地址

啓動文件在``nacos/bin`目錄下面

startup.cmd -m standalone :代表單機啓動的意思

2、向nacos中添加限制規則

/**
 * 向nacos中發送配置
 */
fun send() {
    val remoteAddress = "localhost"
    val groupId = "Sentinel:Demo"
    val dataId = "com.alibaba.csp.sentinel.demo.flow.rule"
    val rule = """[
                  {
                    "resource": "TestResource",
                    "controlBehavior": 0,
                    "count": 5.0,
                    "grade": 1,
                    "limitApp": "default",
                    "strategy": 0
                  }
                ]"""
    val configService = NacosFactory.createConfigService(remoteAddress)
    println(configService.publishConfig(dataId, groupId, rule))
}

3、從nacos中讀取配置規則

// remoteAddress 代表 Nacos 服務端的地址
val remoteAddress = "127.0.0.1"

// groupId 和 dataId 對應 Nacos 中相應配置
val groupId = "Sentinel:Demo"

val dataId = "com.alibaba.csp.sentinel.demo.flow.rule"

/**
 * 加載規則
 */
fun loadRules() {
    val flowRuleDataSource: NacosDataSource<List<FlowRule?>> = NacosDataSource<List<FlowRule?>>(
        remoteAddress, groupId, dataId
    ) { source: String? ->
        JSON.parseObject<List<FlowRule?>>(
            source,
            object : TypeReference<List<FlowRule?>?>() {})
    }
    FlowRuleManager.register2Property(flowRuleDataSource.property)
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章