天天看點

微服務限流容錯降級Sentinel實戰

微服務限流容錯降級Sentinel實戰
點贊再看,養成習慣,公衆号搜一搜【一角錢技術】關注更多原創技術文章。本文 GitHub org_hejianhui/JavaStudy 已收錄,有我的系列文章。

一、什麼是雪崩效應?

微服務限流容錯降級Sentinel實戰

業務場景,高并發調用

  1. 正常情況下,微服務A B C D 都是正常的。
  2. 随着時間推移,在某一個時間點 微服務A突然挂了,此時的微服務B 還在瘋狂的調用微服務A,由于A已經挂了,是以B調用A必須等待服務調用逾時。而我們知道每次B -> A 的适合B都會去建立線程(而線程由計算機的資源,比如cpu、記憶體等)。由于是高并發場景,B 就會阻塞大量的線程。那邊B所在的機器就會去建立線程,但是計算機資源是有限的,最後B的伺服器就會當機。(說白了微服務B 活生生的被豬隊友微服務A給拖死了)
  3. 由于微服務A這個豬隊友活生生的把微服務B給拖死了,導緻微服務B也當機了,然後也會導緻微服務 C D 出現類似的情況,最終我們的豬隊友A成功的把微服務 B C D 都拖死了。這種情況也叫做服務雪崩。也有一個專業術語(cascading failures)級聯故障。

二、容錯三闆斧

2.1 逾時

簡單來說就是逾時機制,配置以下逾時時間,假如1秒——每次請求在1秒内必須傳回,否則到點就把線程掐死,釋放資源!

思路:一旦逾時,就釋放資源。由于釋放資源速度較快,應用就不會那麼容易被拖死。

代碼示範:(針對調用方處理)

// 第一步:設定RestTemplate的逾時時間
@Configuration
public class WebConfig {

    @Bean
    public RestTemplate restTemplate() {
        //設定restTemplate的逾時時間
        SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
        requestFactory.setReadTimeout(1000);
        requestFactory.setConnectTimeout(1000);
        RestTemplate restTemplate = new RestTemplate(requestFactory);
        return restTemplate;
    }
}

// 第二步:進行逾時異常處理
try{
    ResponseEntity<ProductInfo> responseEntity= restTemplate.getForEntity(uri+orderInfo.getProductNo(), ProductInfo.class);
    productInfo = responseEntity.getBody();
}catch (Exception e) {
    log.info("調用逾時");
    throw new RuntimeException("調用逾時");
}


// 設定全局異常處理
@ControllerAdvice
public class NiuhExceptionHandler {

    @ExceptionHandler(value = {RuntimeException.class})
    @ResponseBody
    public Object dealBizException() {
        OrderVo orderVo = new OrderVo();
        orderVo.setOrderNo("-1");
        orderVo.setUserName("容錯使用者");
        return orderVo;
    }
}
           

2.2 艙壁隔離模式

微服務限流容錯降級Sentinel實戰
有興趣可以先了解一下船艙構造——一般來說,現代的輪船都會分很多艙室,艙室直接用鋼闆焊死,彼此隔離。這樣即使有某個/某些船艙進水,也不會營銷其它艙室,浮力夠,船不會沉。

代碼中的艙壁隔離(線程池隔離模式)

M類使用線程池1,N類使用線程池2,彼此的線程池不同,并且為每個類配置設定的線程池大小,例如 coreSIze=10。

舉例子:M類調用B服務,N類調用C服務,如果M類和N類使用相同的線程池,那麼如果B服務挂了,N類調用B服務的接口并發又很高,你又沒有任何保護措施,你的服務就很可能被M類拖死。而如果M類有自己的線程池,N類也有自己的線程池,如果B服務挂了,M類頂多是将自己的線程池占滿,不會影響N類的線程池——于是N類依然能正常工作。

思路:不把雞蛋放在一個籃子裡,你有你的線程池,我有我的線程池,你的線程池滿類和我也沒關系,你挂了也和我也沒關系。
微服務限流容錯降級Sentinel實戰

2.3 斷路器模式

現實世界的斷路器大家肯定都很了解,每個人家裡都會有斷路器。斷路器實時監控電路的情況,如果發現電路電流異常,就會跳閘,進而防止電路被燒毀。

軟體世界的斷路器可以這樣了解:實時監測應用,如果發現在一定時間内失敗次數/失敗率達到一定閥值,就“跳閘”,斷路器打開——次數,請求直接傳回,而不去調用原本調用的邏輯。

跳閘一段時間後(例如15秒),斷路器會進入半開狀态,這是一個瞬間态,此時允許一個請求調用該調的邏輯,如果成功,則斷路器關閉,應用正常調用;如果調用依然不成功,斷路器繼續回到打開狀态,過段時間再進入半開狀态嘗試——通過“跳閘”,應用可以保護自己,而且避免資源浪費;而通過半開的設計,可以實作應用的“自我修複”

微服務限流容錯降級Sentinel實戰

三、Sentinel 流量控制、容錯、降級

3.1 什麼是Sentinel?

A lightweight powerful flow control component enabling reliability and monitoring for microservices.(輕量級的流量控制、熔斷降級 Java 庫)

github官網位址:https://github.com/alibaba/Sentinel

wiki:https://github.com/alibaba/Sentinel/wiki/

Hystrix 在 Sentinel 面前就是弟弟

Sentinel的初體驗

niuh04-ms-alibaba-sentinel-helloworld

V1版本:

  • 第一步:添加依賴包
<!--導入Sentinel的相關jar包-->
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-core</artifactId>
    <version>1.7.1</version>
</dependency>
           
  • 第二步:controller
@RestController
@Slf4j
public class HelloWorldSentinelController {

    @Autowired
    private BusiServiceImpl busiService;

    /**
     * 初始化流控規則
     */
    @PostConstruct
    public void init() {

        List<FlowRule> flowRules = new ArrayList<>();

        /**
         * 定義 helloSentinelV1 受保護的資源的規則
         */
        //建立流控規則對象
        FlowRule flowRule = new FlowRule();
        //設定流控規則 QPS
        flowRule.setGrade(RuleConstant.FLOW_GRADE_QPS);
        //設定受保護的資源
        flowRule.setResource("helloSentinelV1");
        //設定受保護的資源的門檻值
        flowRule.setCount(1);

        flowRules.add(flowRule);

        //加載配置好的規則
        FlowRuleManager.loadRules(flowRules);
    }


    /**
     * 頻繁請求接口 http://localhost:8080/helloSentinelV1
     * 這種做法的缺點:
     * 1)業務侵入性很大,需要在你的controoler中寫入 非業務代碼..
     * 2)配置不靈活 若需要添加新的受保護資源 需要手動添加 init方法來添加流控規則
     * @return
     */
    @RequestMapping("/helloSentinelV1")
    public String testHelloSentinelV1() {

        Entry entity =null;
        //關聯受保護的資源
        try {
            entity = SphU.entry("helloSentinelV1");
            //開始執行 自己的業務方法
            busiService.doBusi();
            //結束執行自己的業務方法
        } catch (BlockException e) {
            log.info("testHelloSentinelV1方法被流控了");
            return "testHelloSentinelV1方法被流控了";
        }finally {
            if(entity!=null) {
                entity.exit();
            }
        }
        return "OK";
    }
}
           

測試效果:http://localhost:8080/helloSentinelV1

微服務限流容錯降級Sentinel實戰

V1版本的缺陷如下:

  • 業務侵入性很大,需要在你的controoler中寫入 非業務代碼.
  • 配置不靈活 若需要添加新的受保護資源 需要手動添加 init方法來添加流控規則

V2版本:基于V1版本,再添加一個依賴

<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-annotation-aspectj</artifactId>
    <version>1.7.1</version>
</dependency>
           
  • 編寫controller
// 配置一個切面
@Configuration
public class SentinelConfig {

    @Bean
    public SentinelResourceAspect sentinelResourceAspect() {
        return new SentinelResourceAspect();
    }

}

/**
 * 初始化流控規則
 */
@PostConstruct
public void init() {

    List<FlowRule> flowRules = new ArrayList<>();

    /**
     * 定義 helloSentinelV2 受保護的資源的規則
     */
    //建立流控規則對象
    FlowRule flowRule2 = new FlowRule();
    //設定流控規則 QPS
    flowRule2.setGrade(RuleConstant.FLOW_GRADE_QPS);
    //設定受保護的資源
    flowRule2.setResource("helloSentinelV2");
    //設定受保護的資源的門檻值
    flowRule2.setCount(1);

    flowRules.add(flowRule2);
}

/**
 * 頻繁請求接口 http://localhost:8080/helloSentinelV2
 * 優點: 需要配置aspectj的切面SentinelResourceAspect ,添加注解@SentinelResource
 *     解決了v1版本中 sentinel的業務侵入代碼問題,通過blockHandler指定被流控後調用的方法.
 * 缺點: 若我們的controller中的方法逐漸變多,那麼受保護的方法也越來越多,會導緻一個問題
 * blockHandler的方法也會越來越多   引起方法急劇膨脹 怎麼解決
 *
 * 注意點:
 *   blockHandler 對應處理 BlockException 的函數名稱,
 *   可選項。blockHandler 函數通路範圍需要是 public,傳回類型需要與原方法相比對,
 *   參數類型需要和原方法相比對并且最後加一個額外的參數,
 *   類型為 BlockException。blockHandler 函數預設需要和原方法在同一個類中
 * @return
 */
@RequestMapping("/helloSentinelV2")
@SentinelResource(value = "helloSentinelV2",blockHandler ="testHelloSentinelV2BlockMethod")
public String testHelloSentinelV2() {
    busiService.doBusi();
    return "OK";
}

public String testHelloSentinelV2BlockMethod(BlockException e) {
    log.info("testRt流控");
    return "testRt降級 流控...."+e;
}
           

測試效果:http://localhost:8080/helloSentinelV2

微服務限流容錯降級Sentinel實戰

V3版本 基于V2缺點改進

/**
 * 初始化流控規則
 */
@PostConstruct
public void init() {

    List<FlowRule> flowRules = new ArrayList<>();

    /**
     * 定義 helloSentinelV3 受保護的資源的規則
     */
    //建立流控規則對象
    FlowRule flowRule3 = new FlowRule();
    //設定流控規則 QPS
    flowRule3.setGrade(RuleConstant.FLOW_GRADE_QPS);
    //設定受保護的資源
    flowRule3.setResource("helloSentinelV3");
    //設定受保護的資源的門檻值
    flowRule3.setCount(1);



    flowRules.add(flowRule3);
}

/**
 * 我們看到了v2中的缺點,我們通過blockHandlerClass 來指定處理被流控的類
 * 通過testHelloSentinelV3BlockMethod 來指定blockHandlerClass 中的方法名稱
 * ***這種方式 處理異常流控的方法必須要是static的
 * 頻繁請求接口 http://localhost:8080/helloSentinelV3
 * @return
 */
@RequestMapping("/helloSentinelV3")
@SentinelResource(value = "helloSentinelV3",blockHandler = "testHelloSentinelV3BlockMethod",blockHandlerClass = BlockUtils.class)
public String testHelloSentinelV3() {
    busiService.doBusi();
    return "OK";
}

// 異常處理類
@Slf4j
public class BlockUtils {


    public static String testHelloSentinelV3BlockMethod(BlockException e){
        log.info("testHelloSentinelV3方法被流控了");
        return "testHelloSentinelV3方法被流控了";
    }
}

           

測試效果:http://localhost:8080/helloSentinelV3

微服務限流容錯降級Sentinel實戰

缺點:不能動态的添加規則。如何解決問題?

3.2 如何在工程中快速整合Sentinel

<!--加入sentinel-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>


<!--加入actuator-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
           

添加Sentinel後,會暴露/actuator/sentinel 端點http://localhost:8080/actuator/sentinel

而Springboot預設是沒有暴露該端點的,是以我們需要自己配置

server:
  port: 8080
management:
  endpoints:
    web:
      exposure:
        include: '*'
           
微服務限流容錯降級Sentinel實戰

3.3 我們需要整合Sentinel-dashboard(哨兵流量衛兵)

下載下傳位址:https://github.com/alibaba/Sentinel/releases (我這裡版本是:1.6.3)

微服務限流容錯降級Sentinel實戰
  • 第一步:執行

    java -jar sentinel-dashboard-1.6.3.jar

     啟動(就是一個SpringBoot工程)
  • 第二步:通路我們的sentinel控制台(1.6版本加入登陸頁面)http://localhost:8080/ ,預設賬戶密碼:sentinel/sentinel
微服務限流容錯降級Sentinel實戰
  • 第三步:我們的微服務 niuh04-ms-alibaba-sentinel-order 整合 sentinel,我們也搭建好了Sentinel控制台,為微服務添加sentinel的控制台位址
spring:
  cloud:
    sentinel:
      transport:
        dashboard: localhost:9999
           

四、Sentinel監控性能名額詳解

4.1 實時監控面闆

在這個面闆中我們監控我們接口的 通過的QPS 和 拒絕的QPS,在沒有設定流控規則,我們是看不到拒絕的QPS。

微服務限流容錯降級Sentinel實戰

4.2 簇點鍊路

用來線上微服務的所監控的API

微服務限流容錯降級Sentinel實戰

4.3 流控設定

簇點鍊路 選擇具體的通路的API,然後點選“流控按鈕”

微服務限流容錯降級Sentinel實戰

含義:

  • 資源名:為我們接口的API /selectOrderInfoById/1
  • 針對來源:這裡是預設的 default(辨別不針對來源),還有一種情況就是假設微服務A需要調用這個資源,微服務B也需要調用這個資源,那麼我們就可以單獨的為微服務A和微服務B進行設定閥值。
  • 閥值類型:分為QPS和線程數,假設閥值為2
    • QPS類型:指的是每秒鐘通路接口的次數 > 2 就進行限流
    • 線程數:為接受請求該資源,配置設定的線程數 > 2 就進行限流
微服務限流容錯降級Sentinel實戰

流控模式

  1. 直接:這種很好了解,就是達到設定的閥值後直接被流控抛出異常

瘋狂的請求這個路徑

微服務限流容錯降級Sentinel實戰
  1. 關聯

業務場景:我們現在有兩個API,第一個是儲存訂單,一個是查詢訂單,假設我們希望有限操作“儲存訂單”

微服務限流容錯降級Sentinel實戰

測試:寫兩個讀寫測試接口

/**
 * 方法實作說明:模仿  流控模式【關聯】  讀接口
 * @author:hejianhui
 * @param orderNo
 * @return:
 * @exception:
 * @date:2019/11/24 22:06
 */
@RequestMapping("/findById/{orderNo}")
public Object findById(@PathVariable("orderNo") String orderNo) {
    log.info("orderNo:{}","執行查詢操作"+System.currentTimeMillis());
    return orderInfoMapper.selectOrderInfoById(orderNo);
}


/**
 * 方法實作說明:模仿流控模式【關聯】   寫接口(優先)
 * @author:hejianhui
 * @return:
 * @exception:
 * @date:2019/11/24 22:07
 */
@RequestMapping("/saveOrder")
public String saveOrder() throws InterruptedException {
    //Thread.sleep(500);
    log.info("執行儲存操作,模仿傳回訂單ID");
    return UUID.randomUUID().toString();
}
           

測試代碼:寫一個for循環一直調用我們的寫接口,讓寫接口QPS達到閥值

public class TestSentinelRule {

    public static void main(String[] args) throws InterruptedException {
        RestTemplate restTemplate = new RestTemplate();
        for(int i=0;i<1000;i++) {
            restTemplate.postForObject("http://localhost:8080/saveOrder",null,String.class);
            Thread.sleep(10);
        }
    }
}
           

此時通路我們的讀接口:此時被限流了。

微服務限流容錯降級Sentinel實戰
  1. 鍊路
用法說明,本地實驗沒成功,用alibaba 未畢業版本0.9.0可以測試出效果,API級别的限制流量
微服務限流容錯降級Sentinel實戰

代碼:

@RequestMapping("/findAll")
public String findAll() throws InterruptedException {
    orderServiceImpl.common();
    return "findAll";
}

@RequestMapping("/findAllByCondtion")
public String findAllByCondtion() {
    orderServiceImpl.common();
    return "findAllByCondition";
}

@Service
public class OrderServiceImpl {

    @SentinelResource("common")
    public String common() {
        return "common";
    }
}

           

根據流控規則來說: 隻會限制/findAll的請求,不會限制/findAllByCondtion規則

流控效果

  1. 快速失敗(直接抛出異常)每秒的QPS 操作過1 就直接抛出異常
源碼:com.alibaba.csp.sentinel.slots.block.flow.controller.DefaultController
微服務限流容錯降級Sentinel實戰
微服務限流容錯降級Sentinel實戰
  1. 預熱(warmUp)

    源碼:com.alibaba.csp.sentinel.slots.block.flow.controller.WarmUpController>

當流量突然增大的時候,我們常常會希望系統從空閑狀态到繁忙狀态的切換的時間長一些。即如果系統在此之前長期處于空閑的狀态,我們希望處理請求的數量是緩步增加,經過預期的時間後,到達系統處理請求個數的最大值。Warm Up (冷啟動,預熱)模式就是為了實作這個目的。

冷加載因子:codeFacotr 預設是3

  • 預設 coldFactor 為3,即請求 QPS 從 threshold / 3 開始,經預熱時長逐漸升至設定的 QPS 閥值。
微服務限流容錯降級Sentinel實戰
微服務限流容錯降級Sentinel實戰

上圖設定:就是QPS從100/3=33開始算, 經過10秒鐘,達到一百的QPS 才進行限制流量。

詳情文檔:https://github.com/alibaba/Sentinel/wiki/限流—冷啟動
  1. 排隊等待

    源碼:com.alibaba.csp.sentinel.slots.block.flow.controller.RateLimiterController

這種方式适合用于請求以突刺狀來到,這個時候我們不希望一下子把所有的請求都通過,這樣可能會把系統壓垮;同時我們也期待系統以穩定的速度,逐漸處理這些請求,以起到“削峰填谷”的效果,而不是拒絕所有請求。

選擇排隊等待的閥值類型必須是****QPS

微服務限流容錯降級Sentinel實戰

上圖設定:單機閥值為10,表示每秒通過的請求個數是10,也就是每個請求平均間隔恒定為 1000 / 10 = 100 ms,每一個請求的最長等待時間(maxQueueingTimeMs)為 20 * 1000ms = 20s。,超過20s就丢棄請求。

詳情文檔:https://github.com/alibaba/Sentinel/wiki/流量控制-勻速排隊模式

4.4 降級規則

rt(平均響應時間)

微服務限流容錯降級Sentinel實戰

平均響應時間(DEGRADE_GRADE_RT):當 1s 内持續進入5個請求,對應時刻的平均響應時間(秒級)均超過閥值(count,以 ms 為機關),那麼在接下來的時間視窗(DegradeRule 中的 timeWindow,以 s 為機關)之内,對這個方法的調用都會自動地熔斷(抛出 DegradeException)。

注意:Sentinel 預設同級的 RT 上限是4900ms,超出此閥值都會算做4900ms,若需要變更此上限可以通過啟動配置項:-Dcsp.sentinel.statistic.max.rt=xxx 來配置

異常比例(DEGRADE_GRADE_EXCEPTION_RATIO)

當資源的每秒請求量 >= 5,并且每秒異常總數占通過量的比值超過閥值(DegradeRule 中的 count)之後,資源進入降級狀态,即在接下的時間視窗(DegradeRule 中的 timeWindow,以 s 為機關)之内,對這個方法的調用都會自動地傳回。異常比例的閥值範圍是 [0.0, 1.0],代表 0% ~ 100% 。

異常數(DEGRADE_GRADE_EXCEPTION_COUNT)

當資源近千分之的異常數目超過閥值之後會進行熔斷。注意由于統計時間視窗是分鐘級别的,若 timeWindow 小于 60s,則結束熔斷狀态後仍可能再進入熔斷狀态。

微服務限流容錯降級Sentinel實戰

4.5 熱點參數

業務場景:秒殺業務,比如商場做促銷秒殺,針對蘋果11(商品id=1)進行9.9秒殺活動,那麼這個時候,我們去請求訂單接口(商品id=1)的請求流量十分大,我們就可以通過熱點參數規則來控制 商品id=1 的請求的并發量。而其他正常商品的請求不會受到限制。那麼這種熱點參數規則使用。

微服務限流容錯降級Sentinel實戰
微服務限流容錯降級Sentinel實戰

五、Sentinel-dashboard 控制台 和 我們的微服務通信原理

5.1 控制台如何擷取到微服務的監控資訊?

5.2 在控制台配置規則,如何把規則推送給微服務的?

微服務限流容錯降級Sentinel實戰

我們通過觀察到sentinel-dashboard的機器清單上觀察注冊服務微服務資訊。我們的 控制台就可以通過這些微服務的注冊資訊跟我們的具體的微服務進行通信.

微服務限流容錯降級Sentinel實戰

5.3 微服務整合sentinel時候的提供的一些接口API位址: http://localhost:8720/api

微服務限流容錯降級Sentinel實戰

5.4 我們可以通過代碼設定規則(我們這裡用流控規則為例)

@RestController
public class AddFlowLimitController {

    @RequestMapping("/addFlowLimit")
    public String addFlowLimit() {
        List<FlowRule> flowRuleList = new ArrayList<>();

        FlowRule flowRule = new FlowRule("/testAddFlowLimitRule");

        //設定QPS門檻值
        flowRule.setCount(1);

        //設定流控模型為QPS模型
        flowRule.setGrade(RuleConstant.FLOW_GRADE_QPS);

        flowRuleList.add(flowRule);

        FlowRuleManager.loadRules(flowRuleList);

        return "success";

    }

    @RequestMapping("/testAddFlowLimitRule")
    public String testAddFlowLimitRule() {
        return "testAddFlowLimitRule";
    }
}
           

添加效果截圖: 執行:http://localhost:8080/addFlowLimit

微服務限流容錯降級Sentinel實戰

Sentinel具體配置項:https://github.com/alibaba/Sentinel/wiki/啟動配置項

5.5 對SpringMVC端點保護關閉(一般應用場景是做壓測需要關閉)

spring:
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
    sentinel:
      transport:
        dashboard: localhost:9999
      filter:
        enabled: true  #關閉Spring mvc的端點保護
           

那麼我們的這種類型的接口 不會被sentinel保護

微服務限流容錯降級Sentinel實戰

隻有加了

@SentinelResource

 的注解的資源才會被保護

微服務限流容錯降級Sentinel實戰

六、Ribbon整合Sentinel

6.1 第一步:加配置

<!--加入ribbon-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>

<!--加入sentinel-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>


<!--加入actuator-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
           

6.2 第二步:加注解

在我們的RestTemplate元件上添加@SentinelRestTemplate注解。并且我們可以通過在@SentinelRestTemplate 同樣的可以指定我們的 blockHandlerClass、fallbackClass、blockHandler、fallback 這四個屬性

@Configuration
public class WebConfig {

    @Bean
    @LoadBalanced
    @SentinelRestTemplate(
            blockHandler = "handleException",blockHandlerClass = GlobalExceptionHandler.class,
            fallback = "fallback",fallbackClass = GlobalExceptionHandler.class

    )
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

*****************全局異常處理類*****************
@Slf4j
public class GlobalExceptionHandler {


    /**
     * 限流後處理方法
     * @param request
     * @param body
     * @param execution
     * @param ex
     * @return
     */
    public static SentinelClientHttpResponse handleException(HttpRequest request,
                                                             byte[] body, ClientHttpRequestExecution execution, BlockException ex)  {

        ProductInfo productInfo = new ProductInfo();
        productInfo.setProductName("被限制流量拉");
        productInfo.setProductNo("-1");
        ObjectMapper objectMapper = new ObjectMapper();

        try {
            return new SentinelClientHttpResponse(objectMapper.writeValueAsString(productInfo));
        } catch (JsonProcessingException e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 熔斷後處理的方法
     * @param request
     * @param body
     * @param execution
     * @param ex
     * @return
     */
    public static SentinelClientHttpResponse fallback(HttpRequest request,
                                                      byte[] body, ClientHttpRequestExecution execution, BlockException ex) {
        ProductInfo productInfo = new ProductInfo();
        productInfo.setProductName("被降級拉");
        productInfo.setProductNo("-1");
        ObjectMapper objectMapper = new ObjectMapper();

        try {
            return new SentinelClientHttpResponse(objectMapper.writeValueAsString(productInfo));
        } catch (JsonProcessingException e) {
            e.printStackTrace();
            return null;
        }
    }
}
           

6.3 第三步:添加配置

什麼時候關閉:一般在我們的自己測試業務功能是否正常的情況,關閉該配置

#是否開啟@SentinelRestTemplate注解
resttemplate:
  sentinel:
    enabled: true
           

七、OpenFeign整合我們的Sentinel

7.1 第一步:加配置

在niuh05-ms-alibaba-feignwithsentinel-order上 pom.xml中添加配置

<!--加入sentinel-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>


<!--加入actuator-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

<dependency>
    <groupId>com.niuh</groupId>
    <artifactId>niuh03-ms-alibaba-feign-api</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>
           

7.2 第二步:在Feign的聲明式接口上添加fallback屬性或者 fallbackFactory屬性

  • 為我們添加fallback屬性的api
@FeignClient(name = "product-center",fallback = ProductCenterFeignApiWithSentinelFallback.class)
public interface ProductCenterFeignApiWithSentinel {

    /**
     * 聲明式接口,遠端調用http://product-center/selectProductInfoById/{productNo}
     * @param productNo
     * @return
     */
    @RequestMapping("/selectProductInfoById/{productNo}")
    ProductInfo selectProductInfoById(@PathVariable("productNo") String productNo) throws InterruptedException;
}
           

我們feign的限流降級接口(通過fallback沒有辦法擷取到異常的)

@Component
public class ProductCenterFeignApiWithSentinelFallback implements ProductCenterFeignApiWithSentinel {
    @Override
    public ProductInfo selectProductInfoById(String productNo) {
        ProductInfo productInfo = new ProductInfo();
        productInfo.setProductName("預設商品");
        return productInfo;
    }
}
           
  • 為我們添加fallbackFactory屬性的api
package com.niuh.feignapi.sentinel;

import com.niuh.entity.ProductInfo;
import com.niuh.handler.ProductCenterFeignApiWithSentielFallbackFactoryasdasf;
import com.niuh.handler.ProductCenterFeignApiWithSentinelFallback;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;

/**
 * Created by hejianhui on 2019/11/22.
 */

@FeignClient(name = "product-center",fallbackFactory = ProductCenterFeignApiWithSentielFallbackFactoryasdasf.class)
public interface ProductCenterFeignApiWithSentinel {

    /**
     * 聲明式接口,遠端調用http://product-center/selectProductInfoById/{productNo}
     * @param productNo
     * @return
     */
    @RequestMapping("/selectProductInfoById/{productNo}")
    ProductInfo selectProductInfoById(@PathVariable("productNo") String productNo) throws InterruptedException;
}

           

通過FallbackFactory屬性可以處理我們的異常

@Component
@Slf4j
public class ProductCenterFeignApiWithSentielFallbackFactoryasdasf implements FallbackFactory<ProductCenterFeignApiWithSentinel> {
    @Override
    public ProductCenterFeignApiWithSentinel create(Throwable throwable) {
        return new ProductCenterFeignApiWithSentinel(){

            @Override
            public ProductInfo selectProductInfoById(String productNo) {
                ProductInfo productInfo = new ProductInfo();
                if (throwable instanceof FlowException) {
                    log.error("流控了....{}",throwable.getMessage());
                    productInfo.setProductName("我是被流控的預設商品");
                }else {
                    log.error("降級了....{}",throwable.getMessage());
                    productInfo.setProductName("我是被降級的預設商品");
                }

                return productInfo;
            }
        };
    }
}
           

八、Sentinel 規則持久化

Sentinel-dashboard 配置的規則,在我們的微服務以及控制台重新開機的時候就清空了,因為它是基于記憶體的。

微服務限流容錯降級Sentinel實戰

8.1 原生模式

Dashboard 的推送規則方式是通過 API 将規則推送至用戶端并直接更新到記憶體。

微服務限流容錯降級Sentinel實戰

優缺點:這種做法的好處是簡單,無依賴;壞處是應用重新開機規則就會消失,僅用于簡單測試,不能用于生産環境。

8.2 Pull拉模式

微服務限流容錯降級Sentinel實戰

首先 Sentinel 控制台通過 API 将規則推送至用戶端并更新到記憶體中,接着注冊的寫資料源會将新的規則儲存到本地的檔案中。使用 pull 模式的資料源時一般不需要對 Sentinel 控制台進行改造。

這種實作方法好處是簡單,不引入新的依賴,壞處是無法保證監控資料的一緻性

用戶端Sentinel的改造(拉模式)

通過SPI擴充機制進行擴充,我們寫一個拉模式的實作類 com.niuh.persistence.PullModeByFileDataSource ,然後在工廠目錄下建立 META-INF/services/com.alibaba.csp.sentinel.init.InitFun檔案。

微服務限流容錯降級Sentinel實戰

檔案的内容就是寫我們的拉模式的實作類:

微服務限流容錯降級Sentinel實戰
代碼在niuh05-ms-alibaba-sentinelrulepersistencepull-order 工程的persistence包下。

8.3 Push推模式(以Nacos為例,生産推薦使用)

微服務限流容錯降級Sentinel實戰

原理簡述

  • 控制台推送規則:
    • 将規則推送至Nacos擷取其他遠端配置中心
    • Sentinel用戶端連接配接Nacos,擷取規則配置;并監聽Nacos配置變化,如果發送變化,就更新本地緩存(進而讓本地緩存總是和Nacos一緻)
  • 控制台監聽Nacos配置變化,如果發送變化就更新本地緩存(進而讓控制台本地緩存和Nacos一緻)

改造方案

微服務改造方案

  • 第一步:在niuh05-ms-alibaba-sentinelrulepersistencepush-order工程加入依賴
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
           
  • 第二步:加入yml的配置
spring:
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
    sentinel:
      transport:
        dashboard: localhost:9999
        #namespace: bc7613d2-2e22-4292-a748-48b78170f14c  #指定namespace的id
      datasource:
        # 名稱随意
        flow:
          nacos:
            server-addr: 47.111.191.111:8848
            dataId: ${spring.application.name}-flow-rules
            groupId: SENTINEL_GROUP
            rule-type: flow
        degrade:
          nacos:
            server-addr: localhost:8848
            dataId: ${spring.application.name}-degrade-rules
            groupId: SENTINEL_GROUP
            rule-type: degrade
        system:
          nacos:
            server-addr: localhost:8848
            dataId: ${spring.application.name}-system-rules
            groupId: SENTINEL_GROUP
            rule-type: system
        authority:
          nacos:
            server-addr: localhost:8848
            dataId: ${spring.application.name}-authority-rules
            groupId: SENTINEL_GROUP
            rule-type: authority
        param-flow:
          nacos:
            server-addr: localhost:8848
            dataId: ${spring.application.name}-param-flow-rules
            groupId: SENTINEL_GROUP
            rule-type: param-flow
           

Sentinel-dashboard改造方案

<!-- for Nacos rule publisher sample -->
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-datasource-nacos</artifactId>
   <!-- <scope>test</scope>--> // 需要把test注釋掉
</dependency>
           

控制台改造主要是為規則實作:

  • DynamicRuleProvider :從Nacos上讀取配置
  • DynamicRulePublisher :将規則推送到Nacis上

在sentinel-dashboard工程目錄com.alibaba.csp.sentinel.dashboard.rule 下建立一 個Nacos的包,然後把我們的各個場景的配置規則類寫到該包下.

微服務限流容錯降級Sentinel實戰
微服務限流容錯降級Sentinel實戰

我們以ParamFlowRuleController(熱點參數流控類作為修改作為示範)

/**
 * @author Eric Zhao
 * @since 0.2.1
 */
@RestController
@RequestMapping(value = "/paramFlow")
public class ParamFlowRuleController {

    private final Logger logger = LoggerFactory.getLogger(ParamFlowRuleController.class);

    @Autowired
    private SentinelApiClient sentinelApiClient;
    @Autowired
    private AppManagement appManagement;
    @Autowired
    private RuleRepository<ParamFlowRuleEntity, Long> repository;

    @Autowired
    @Qualifier("niuhHotParamFlowRuleNacosPublisher")
    private DynamicRulePublisher<List<ParamFlowRuleEntity>> rulePublisher;

    @Autowired
    @Qualifier("niuhHotParamFlowRuleNacosProvider")
    private DynamicRuleProvider<List<ParamFlowRuleEntity>> ruleProvider;

    @Autowired
    private AuthService<HttpServletRequest> authService;

    private boolean checkIfSupported(String app, String ip, int port) {
        try {
            return Optional.ofNullable(appManagement.getDetailApp(app))
                .flatMap(e -> e.getMachine(ip, port))
                .flatMap(m -> VersionUtils.parseVersion(m.getVersion())
                    .map(v -> v.greaterOrEqual(version020)))
                .orElse(true);
            // If error occurred or cannot retrieve machine info, return true.
        } catch (Exception ex) {
            return true;
        }
    }

    @GetMapping("/rules")
    public Result<List<ParamFlowRuleEntity>> apiQueryAllRulesForMachine(HttpServletRequest request,
                                                                        @RequestParam String app,
                                                                        @RequestParam String ip,
                                                                        @RequestParam Integer port) {
        AuthUser authUser = authService.getAuthUser(request);
        authUser.authTarget(app, PrivilegeType.READ_RULE);
        if (StringUtil.isEmpty(app)) {
            return Result.ofFail(-1, "app cannot be null or empty");
        }
        if (StringUtil.isEmpty(ip)) {
            return Result.ofFail(-1, "ip cannot be null or empty");
        }
        if (port == null || port <= 0) {
            return Result.ofFail(-1, "Invalid parameter: port");
        }
        if (!checkIfSupported(app, ip, port)) {
            return unsupportedVersion();
        }
        try {
/*            return sentinelApiClient.fetchParamFlowRulesOfMachine(app, ip, port)
                .thenApply(repository::saveAll)
                .thenApply(Result::ofSuccess)
                .get();*/
            List<ParamFlowRuleEntity> rules = ruleProvider.getRules(app);
            rules = repository.saveAll(rules);
            return Result.ofSuccess(rules);
        } catch (ExecutionException ex) {
            logger.error("Error when querying parameter flow rules", ex.getCause());
            if (isNotSupported(ex.getCause())) {
                return unsupportedVersion();
            } else {
                return Result.ofThrowable(-1, ex.getCause());
            }
        } catch (Throwable throwable) {
            logger.error("Error when querying parameter flow rules", throwable);
            return Result.ofFail(-1, throwable.getMessage());
        }
    }

    private boolean isNotSupported(Throwable ex) {
        return ex instanceof CommandNotFoundException;
    }

    @PostMapping("/rule")
    public Result<ParamFlowRuleEntity> apiAddParamFlowRule(HttpServletRequest request,
                                                           @RequestBody ParamFlowRuleEntity entity) {
        AuthUser authUser = authService.getAuthUser(request);
        authUser.authTarget(entity.getApp(), PrivilegeType.WRITE_RULE);
        Result<ParamFlowRuleEntity> checkResult = checkEntityInternal(entity);
        if (checkResult != null) {
            return checkResult;
        }
        if (!checkIfSupported(entity.getApp(), entity.getIp(), entity.getPort())) {
            return unsupportedVersion();
        }
        entity.setId(null);
        entity.getRule().setResource(entity.getResource().trim());
        Date date = new Date();
        entity.setGmtCreate(date);
        entity.setGmtModified(date);
        try {
            entity = repository.save(entity);
            //publishRules(entity.getApp(), entity.getIp(), entity.getPort()).get();
            publishRules(entity.getApp());
            return Result.ofSuccess(entity);
        } catch (ExecutionException ex) {
            logger.error("Error when adding new parameter flow rules", ex.getCause());
            if (isNotSupported(ex.getCause())) {
                return unsupportedVersion();
            } else {
                return Result.ofThrowable(-1, ex.getCause());
            }
        } catch (Throwable throwable) {
            logger.error("Error when adding new parameter flow rules", throwable);
            return Result.ofFail(-1, throwable.getMessage());
        }
    }

    private <R> Result<R> checkEntityInternal(ParamFlowRuleEntity entity) {
        if (entity == null) {
            return Result.ofFail(-1, "bad rule body");
        }
        if (StringUtil.isBlank(entity.getApp())) {
            return Result.ofFail(-1, "app can't be null or empty");
        }
        if (StringUtil.isBlank(entity.getIp())) {
            return Result.ofFail(-1, "ip can't be null or empty");
        }
        if (entity.getPort() == null || entity.getPort() <= 0) {
            return Result.ofFail(-1, "port can't be null");
        }
        if (entity.getRule() == null) {
            return Result.ofFail(-1, "rule can't be null");
        }
        if (StringUtil.isBlank(entity.getResource())) {
            return Result.ofFail(-1, "resource name cannot be null or empty");
        }
        if (entity.getCount() < 0) {
            return Result.ofFail(-1, "count should be valid");
        }
        if (entity.getGrade() != RuleConstant.FLOW_GRADE_QPS) {
            return Result.ofFail(-1, "Unknown mode (blockGrade) for parameter flow control");
        }
        if (entity.getParamIdx() == null || entity.getParamIdx() < 0) {
            return Result.ofFail(-1, "paramIdx should be valid");
        }
        if (entity.getDurationInSec() <= 0) {
            return Result.ofFail(-1, "durationInSec should be valid");
        }
        if (entity.getControlBehavior() < 0) {
            return Result.ofFail(-1, "controlBehavior should be valid");
        }
        return null;
    }

    @PutMapping("/rule/{id}")
    public Result<ParamFlowRuleEntity> apiUpdateParamFlowRule(HttpServletRequest request,
                                                              @PathVariable("id") Long id,
                                                              @RequestBody ParamFlowRuleEntity entity) {
        AuthUser authUser = authService.getAuthUser(request);
        if (id == null || id <= 0) {
            return Result.ofFail(-1, "Invalid id");
        }
        ParamFlowRuleEntity oldEntity = repository.findById(id);
        if (oldEntity == null) {
            return Result.ofFail(-1, "id " + id + " does not exist");
        }
        authUser.authTarget(oldEntity.getApp(), PrivilegeType.WRITE_RULE);
        Result<ParamFlowRuleEntity> checkResult = checkEntityInternal(entity);
        if (checkResult != null) {
            return checkResult;
        }
        if (!checkIfSupported(entity.getApp(), entity.getIp(), entity.getPort())) {
            return unsupportedVersion();
        }
        entity.setId(id);
        Date date = new Date();
        entity.setGmtCreate(oldEntity.getGmtCreate());
        entity.setGmtModified(date);
        try {
            entity = repository.save(entity);
            //publishRules(entity.getApp(), entity.getIp(), entity.getPort()).get();
            publishRules(entity.getApp());
            return Result.ofSuccess(entity);
        } catch (ExecutionException ex) {
            logger.error("Error when updating parameter flow rules, id=" + id, ex.getCause());
            if (isNotSupported(ex.getCause())) {
                return unsupportedVersion();
            } else {
                return Result.ofThrowable(-1, ex.getCause());
            }
        } catch (Throwable throwable) {
            logger.error("Error when updating parameter flow rules, id=" + id, throwable);
            return Result.ofFail(-1, throwable.getMessage());
        }
    }

    @DeleteMapping("/rule/{id}")
    public Result<Long> apiDeleteRule(HttpServletRequest request, @PathVariable("id") Long id) {
        AuthUser authUser = authService.getAuthUser(request);
        if (id == null) {
            return Result.ofFail(-1, "id cannot be null");
        }
        ParamFlowRuleEntity oldEntity = repository.findById(id);
        if (oldEntity == null) {
            return Result.ofSuccess(null);
        }
        authUser.authTarget(oldEntity.getApp(), PrivilegeType.DELETE_RULE);
        try {
            repository.delete(id);
            /*publishRules(oldEntity.getApp(), oldEntity.getIp(), oldEntity.getPort()).get();*/
            publishRules(oldEntity.getApp());
            return Result.ofSuccess(id);
        } catch (ExecutionException ex) {
            logger.error("Error when deleting parameter flow rules", ex.getCause());
            if (isNotSupported(ex.getCause())) {
                return unsupportedVersion();
            } else {
                return Result.ofThrowable(-1, ex.getCause());
            }
        } catch (Throwable throwable) {
            logger.error("Error when deleting parameter flow rules", throwable);
            return Result.ofFail(-1, throwable.getMessage());
        }
    }

    private CompletableFuture<Void> publishRules(String app, String ip, Integer port) {
        List<ParamFlowRuleEntity> rules = repository.findAllByMachine(MachineInfo.of(app, ip, port));
        return sentinelApiClient.setParamFlowRuleOfMachine(app, ip, port, rules);
    }

    private void publishRules(String app) throws Exception {
        List<ParamFlowRuleEntity> rules = repository.findAllByApp(app);
        rulePublisher.publish(app, rules);
    }

    private <R> Result<R> unsupportedVersion() {
        return Result.ofFail(4041,
            "Sentinel client not supported for parameter flow control (unsupported version or dependency absent)");
    }

    private final SentinelVersion version020 = new SentinelVersion().setMinorVersion(2);
}

           

8.4 阿裡雲的 AHAS

  • 開通位址:https://ahas.console.aliyun.com/
  • 開通規則說明:https://help.aliyun.com/document_detail/90323.html

第一步:通路 https://help.aliyun.com/document_detail/90323.html

微服務限流容錯降級Sentinel實戰

第二步:免費開通

微服務限流容錯降級Sentinel實戰

第三步:開通

微服務限流容錯降級Sentinel實戰

第四步:接入應用

微服務限流容錯降級Sentinel實戰

第五步:點選接入SDK

微服務限流容錯降級Sentinel實戰

第六步:加入我們的應用

微服務限流容錯降級Sentinel實戰

以niuh05-ms-alibaba-sentinelrulepersistence-ahas-order工程為例

  • 加入ahas的依賴
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>spring‐boot‐starter‐ahas‐sentinel‐client</artifactId> 4 <version>1.5.0</version>
</dependency>
           
  • 加入配置:yml的配置
ahas.namespace: default
project.name: order-center
ahas.license: b833de8ab5f34e4686457ecb2b60fa46
           
  • 測試接口
@SentinelResource("hot-param-flow-rule")
@RequestMapping("/testHotParamFlowRule")
public OrderInfo testHotParamFlowRule(@RequestParam("orderNo") String orderNo) {
    return orderInfoMapper.selectOrderInfoById(orderNo);
}
           

第一次通路接口:

微服務限流容錯降級Sentinel實戰

AHas控制台出現我們的微服務

微服務限流容錯降級Sentinel實戰

添加我們直接的流控規則

微服務限流容錯降級Sentinel實戰

瘋狂重新整理我們的測試接口:

微服務限流容錯降級Sentinel實戰

九、Sentinel 線上環境的優化

9.1 優化錯誤頁面

  • 流控錯誤頁面
微服務限流容錯降級Sentinel實戰
  • 降級錯誤頁面
微服務限流容錯降級Sentinel實戰

發現這兩種錯誤都是醫院,顯然這裡我們需要優化

UrlBlockHandler

 提供了一個接口,我們需要實作這個接口

/**
* @vlog: 高于生活,源于生活
* @desc: 類的描述:處理流控,降級規則
* @author: hejianhui
* @createDate: 2019/12/3 16:40
* @version: 1.0
*/
@Component
public class NiuhUrlBlockHandler implements UrlBlockHandler {

    public static final Logger log = LoggerFactory.getLogger(NiuhUrlBlockHandler.class);

    @Override
    public void blocked(HttpServletRequest request, HttpServletResponse response, BlockException ex) throws IOException {

        if(ex instanceof FlowException) {
            log.warn("觸發了流控");
            warrperResponse(response,ErrorEnum.FLOW_RULE_ERR);
        }else if(ex instanceof ParamFlowException) {
            log.warn("觸發了參數流控");
            warrperResponse(response,ErrorEnum.HOT_PARAM_FLOW_RULE_ERR);
        }else if(ex instanceof AuthorityException) {
            log.warn("觸發了授權規則");
            warrperResponse(response,ErrorEnum.AUTH_RULE_ERR);
        }else if(ex instanceof SystemBlockException) {
            log.warn("觸發了系統規則");
            warrperResponse(response,ErrorEnum.SYS_RULE_ERR);
        }else{
            log.warn("觸發了降級規則");
            warrperResponse(response,ErrorEnum.DEGRADE_RULE_ERR);
        }
    }


    private void warrperResponse(HttpServletResponse httpServletResponse, ErrorEnum errorEnum) throws IOException {
        httpServletResponse.setStatus(500);
        httpServletResponse.setCharacterEncoding("UTF-8");
        httpServletResponse.setHeader("Content-Type","application/json;charset=utf-8");
        httpServletResponse.setContentType("application/json;charset=utf-8");

        ObjectMapper objectMapper = new ObjectMapper();
        String errMsg =objectMapper.writeValueAsString(new ErrorResult(errorEnum));
        httpServletResponse.getWriter().write(errMsg);
    }

}
           

優化後:

  • 流控規則提示:
微服務限流容錯降級Sentinel實戰
  • 降級規則提示:
微服務限流容錯降級Sentinel實戰

9.2 針對來源編碼實作

微服務限流容錯降級Sentinel實戰

Sentinel 提供了一個

RequestOriginParser

 接口,我們可以在這裡實作編碼從請求頭中區分來源

/**
* @vlog: 高于生活,源于生活
* @desc: 類的描述:區分來源接口
* @author: hejianhui
* @createDate: 2019/12/4 13:13
* @version: 1.0
*/
/*@Component*/
@Slf4j
public class NiuhRequestOriginParse implements RequestOriginParser {

    @Override
    public String parseOrigin(HttpServletRequest request) {
        String origin = request.getHeader("origin");
        if(StringUtils.isEmpty(origin)) {
            log.warn("origin must not null");
            throw new IllegalArgumentException("request origin must not null");
        }
        return origin;
    }
}
           

配置設定區分來源為:yijiaoqian

微服務限流容錯降級Sentinel實戰
微服務限流容錯降級Sentinel實戰

9.3 解決RestFul風格的請求

例如:/selectOrderInfoById/2 、 /selectOrderInfoById/1 需要轉為/selectOrderInfoById/{number}
/**
* @vlog: 高于生活,源于生活
* @desc: 類的描述:解決RestFule風格的請求
 *       eg:    /selectOrderInfoById/2     /selectOrderInfoById/1 需要轉為/selectOrderInfoById/{number}
* @author: hejianhui
* @createDate: 2019/12/4 13:28
* @version: 1.0
*/
@Component
@Slf4j
public class NiuhUrlClean implements UrlCleaner {
    @Override
    public String clean(String originUrl) {
        log.info("originUrl:{}",originUrl);

        if(StringUtils.isEmpty(originUrl)) {
            log.error("originUrl not be null");
            throw new IllegalArgumentException("originUrl not be null");
        }
        return replaceRestfulUrl(originUrl);
    }

    /**
     * 方法實作說明:把/selectOrderInfoById/2 替換成/selectOrderInfoById/{number}
     * @author:hejianhui
     * @param sourceUrl 目标url
     * @return: 替換後的url
     * @exception:
     * @date:2019/12/4 13:46
     */
    private String replaceRestfulUrl(String sourceUrl) {
        List<String> origins = Arrays.asList(sourceUrl.split("/"));
        StringBuffer targetUrl = new StringBuffer("/");

        for(String str:origins) {
            if(NumberUtils.isNumber(str)) {
                targetUrl.append("/{number}");
            }else {
                targetUrl.append(str);
            }

        }
        return targetUrl.toString();
    }
}

           

PS:以上代碼送出在 Github :https://github.com/Niuh-Study/niuh-cloud-alibaba.git

文章持續更新,可以公衆号搜一搜「 一角錢技術 」第一時間閱讀, 本文 GitHub org_hejianhui/JavaStudy 已經收錄,歡迎 Star。

繼續閱讀