學會 CompletableFuture:讓你的代碼免受阻塞之苦!

来源:https://www.cnblogs.com/javastack/archive/2023/10/23/17782960.html
-Advertisement-
Play Games

來源:https://juejin.cn/post/6844904024332828685 寫在前面 通過閱讀本篇文章你將瞭解到: CompletableFuture的使用 CompletableFure非同步和同步的性能測試 已經有了Future為什麼仍需要在JDK1.8中引入Completable ...


來源:https://juejin.cn/post/6844904024332828685

寫在前面

通過閱讀本篇文章你將瞭解到:

  • CompletableFuture的使用
  • CompletableFure非同步和同步的性能測試
  • 已經有了Future為什麼仍需要在JDK1.8中引入CompletableFuture
  • CompletableFuture的應用場景
  • 對CompletableFuture的使用優化

場景說明

查詢所有商店某個商品的價格並返回,並且查詢商店某個商品的價格的API為同步 一個Shop類,提供一個名為getPrice的同步方法

  • 店鋪類:Shop.java
public class Shop {
    private Random random = new Random();
    /**
     * 根據產品名查找價格
     * */
    public double getPrice(String product) {
        return calculatePrice(product);
    }

    /**
     * 計算價格
     *
     * @param product
     * @return
     * */
    private double calculatePrice(String product) {
        delay();
        //random.nextDouble()隨機返回折扣
        return random.nextDouble() * product.charAt(0) + product.charAt(1);
    }

    /**
     * 通過睡眠模擬其他耗時操作
     * */
    private void delay() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

查詢商品的價格為同步方法,並通過sleep方法模擬其他操作。這個場景模擬了當需要調用第三方API,但第三方提供的是同步API,在無法修改第三方API時如何設計代碼調用提高應用的性能和吞吐量,這時候可以使用CompletableFuture類。

推薦一個開源免費的 Spring Boot 實戰項目:

https://github.com/javastacks/spring-boot-best-practice

CompletableFuture使用

Completable是Future介面的實現類,在JDK1.8中引入

  • CompletableFuture的創建:

    說明:

    • 兩個重載方法之間的區別 => 後者可以傳入自定義Executor,前者是預設的,使用的ForkJoinPool

    • supplyAsync和runAsync方法之間的區別 => 前者有返回值,後者無返回值

    • Supplier是函數式介面,因此該方法需要傳入該介面的實現類,追蹤源碼會發現在run方法中會調用該介面的方法。因此使用該方法創建CompletableFuture對象只需重寫Supplier中的get方法,在get方法中定義任務即可。又因為函數式介面可以使用Lambda表達式,和new創建CompletableFuture對象相比代碼會簡潔不少

    • 使用new方法

      CompletableFuture<Double> futurePrice = new CompletableFuture<>();
      
    • 使用CompletableFuture#completedFuture靜態方法創建

      public static <U> CompletableFuture<U> completedFuture(U value) {
          return new CompletableFuture<U>((value == null) ? NIL : value);
      }
      

      參數的值為任務執行完的結果,一般該方法在實際應用中較少應用

    • 使用 CompletableFuture#supplyAsync靜態方法創建 supplyAsync有兩個重載方法:

      //方法一
      public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier) {
          return asyncSupplyStage(asyncPool, supplier);
      }
      //方法二
      public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier,
                                                         Executor executor) {
          return asyncSupplyStage(screenExecutor(executor), supplier);
      }
      
    • 使用CompletableFuture#runAsync靜態方法創建 runAsync有兩個重載方法

      //方法一
      public static CompletableFuture<Void> runAsync(Runnable runnable) {
          return asyncRunStage(asyncPool, runnable);
      }
      //方法二
      public static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor) {
          return asyncRunStage(screenExecutor(executor), runnable);
      }
      
  • 結果的獲取: 對於結果的獲取CompltableFuture類提供了四種方式

    //方式一
    public T get()
    //方式二
    public T get(long timeout, TimeUnit unit)
    //方式三
    public T getNow(T valueIfAbsent)
    //方式四
    public T join()
    

    說明:

    示例:

    • get()和get(long timeout, TimeUnit unit) => 在Future中就已經提供了,後者提供超時處理,如果在指定時間內未獲取結果將拋出超時異常
    • getNow => 立即獲取結果不阻塞,結果計算已完成將返回結果或計算過程中的異常,如果未計算完成將返回設定的valueIfAbsent值
    • join => 方法里不會拋出異常
public class AcquireResultTest {
  public static void main(String[] args) throws ExecutionException, InterruptedException {
      //getNow方法測試
      CompletableFuture<String> cp1 = CompletableFuture.supplyAsync(() -> {
          try {
              Thread.sleep(60 * 1000 * 60 );
          } catch (InterruptedException e) {
              e.printStackTrace();
          }

          return "hello world";
      });

      System.out.println(cp1.getNow("hello h2t"));

      //join方法測試
      CompletableFuture<Integer> cp2 = CompletableFuture.supplyAsync((()-> 1 / 0));
      System.out.println(cp2.join());

      //get方法測試
      CompletableFuture<Integer> cp3 = CompletableFuture.supplyAsync((()-> 1 / 0));
      System.out.println(cp3.get());
  }
}

說明:

  • 第一個執行結果為hello h2t,因為要先睡上1分鐘結果不能立即獲取
  • join方法獲取結果方法里不會拋異常,但是執行結果會拋異常,拋出的異常為CompletionException
  • get方法獲取結果方法里將拋出異常,執行結果拋出的異常為ExecutionException
  • 異常處理: 使用靜態方法創建的CompletableFuture對象無需顯示處理異常,使用new創建的對象需要調用completeExceptionally方法設置捕獲到的異常,舉例說明:
CompletableFuture completableFuture = new CompletableFuture();
new Thread(() -> {
   try {
       //doSomething,調用complete方法將其他方法的執行結果記錄在completableFuture對象中
       completableFuture.complete(null);
   } catch (Exception e) {
       //異常處理
       completableFuture.completeExceptionally(e);
    }
}).start();

同步方法Pick非同步方法查詢所有店鋪某個商品價格

店鋪為一個列表:

private static List<Shop> shopList = Arrays.asList(
        new Shop("BestPrice"),
        new Shop("LetsSaveBig"),
        new Shop("MyFavoriteShop"),
        new Shop("BuyItAll")
);

同步方法:

private static List<String> findPriceSync(String product) {
    return shopList.stream()
            .map(shop -> String.format("%s price is %.2f",
                    shop.getName(), shop.getPrice(product)))  //格式轉換
            .collect(Collectors.toList());
}

非同步方法:

private static List<String> findPriceAsync(String product) {
    List<CompletableFuture<String>> completableFutureList = shopList.stream()
            //轉非同步執行
            .map(shop -> CompletableFuture.supplyAsync(
                    () -> String.format("%s price is %.2f",
                            shop.getName(), shop.getPrice(product))))  //格式轉換
            .collect(Collectors.toList());

    return completableFutureList.stream()
            .map(CompletableFuture::join)  //獲取結果不會拋出異常
            .collect(Collectors.toList());
}

性能測試結果:

Find Price Sync Done in 4141
Find Price Async Done in 1033

非同步執行效率提高四倍

為什麼仍需要CompletableFuture

在JDK1.8以前,通過調用線程池的submit方法可以讓任務以非同步的方式運行,該方法會返回一個Future對象,通過調用get方法獲取非同步執行的結果:

private static List<String> findPriceFutureAsync(String product) {
    ExecutorService es = Executors.newCachedThreadPool();
    List<Future<String>> futureList = shopList.stream().map(shop -> es.submit(() -> String.format("%s price is %.2f",
            shop.getName(), shop.getPrice(product)))).collect(Collectors.toList());

    return futureList.stream()
            .map(f -> {
                String result = null;
                try {
                    result = f.get();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (ExecutionException e) {
                    e.printStackTrace();
                }

                return result;
            }).collect(Collectors.toList());
}

既生瑜何生亮,為什麼仍需要引入CompletableFuture?對於簡單的業務場景使用Future完全沒有,但是想將多個非同步任務的計算結果組合起來,後一個非同步任務的計算結果需要前一個非同步任務的值等等,使用Future提供的那點API就囊中羞澀,處理起來不夠優雅,這時候還是讓CompletableFuture以聲明式的方式優雅的處理這些需求。而且在Future編程中想要拿到Future的值然後拿這個值去做後續的計算任務,只能通過輪詢的方式去判斷任務是否完成這樣非常占CPU並且代碼也不優雅,用偽代碼表示如下:

while(future.isDone()) {
    result = future.get();
    doSomrthingWithResult(result);
}

但CompletableFuture提供了API幫助我們實現這樣的需求

其他API介紹

whenComplete計算結果的處理:

對前面計算結果進行處理,無法返回新值 提供了三個方法:

//方法一
public CompletableFuture<T> whenComplete(BiConsumer<? super T,? super Throwable> action)
//方法二
public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T,? super Throwable> action)
//方法三
public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T,? super Throwable> action, Executor executor)

說明:

  • BiFunction<? super T,? super U,? extends V> fn參數 => 定義對結果的處理
  • Executor executor參數 => 自定義線程池
  • 以async結尾的方法將會在一個新的線程中執行組合操作

示例:

public class WhenCompleteTest {
    public static void main(String[] args) {
        CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(() -> "hello");
        CompletableFuture<String> cf2 = cf1.whenComplete((v, e) ->
                System.out.println(String.format("value:%s, exception:%s", v, e)));
        System.out.println(cf2.join());
    }
}

thenApply轉換:

將前面計算結果的的CompletableFuture傳遞給thenApply,返回thenApply處理後的結果。可以認為通過thenApply方法實現CompletableFuture<T>CompletableFuture<U>的轉換。白話一點就是將CompletableFuture的計算結果作為thenApply方法的參數,返回thenApply方法處理後的結果 提供了三個方法:

//方法一
public <U> CompletableFuture<U> thenApply(
    Function<? super T,? extends U> fn) {
    return uniApplyStage(null, fn);
}

//方法二
public <U> CompletableFuture<U> thenApplyAsync(
    Function<? super T,? extends U> fn) {
    return uniApplyStage(asyncPool, fn);
}

//方法三
public <U> CompletableFuture<U> thenApplyAsync(
    Function<? super T,? extends U> fn, Executor executor) {
    return uniApplyStage(screenExecutor(executor), fn);
}

說明:

  • Function<? super T,? extends U> fn參數 => 對前一個CompletableFuture 計算結果的轉化操作
  • Executor executor參數 => 自定義線程池
  • 以async結尾的方法將會在一個新的線程中執行組合操作 示例:
public class ThenApplyTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<Integer> result = CompletableFuture.supplyAsync(ThenApplyTest::randomInteger).thenApply((i) -> i * 8);
        System.out.println(result.get());
    }

    public static Integer randomInteger() {
        return 10;
    }
}

這裡將前一個CompletableFuture計算出來的結果擴大八倍

thenAccept結果處理:

thenApply也可以歸類為對結果的處理,thenAccept和thenApply的區別就是沒有返回值 提供了三個方法:

//方法一
public CompletableFuture<Void> thenAccept(Consumer<? super T> action) {
    return uniAcceptStage(null, action);
}

//方法二
public CompletableFuture<Void> thenAcceptAsync(Consumer<? super T> action) {
    return uniAcceptStage(asyncPool, action);
}

//方法三
public CompletableFuture<Void> thenAcceptAsync(Consumer<? super T> action,
                                               Executor executor) {
    return uniAcceptStage(screenExecutor(executor), action);
}

說明:

  • Consumer<? super T> action參數 => 對前一個CompletableFuture計算結果的操作
  • Executor executor參數 => 自定義線程池
  • 同理以async結尾的方法將會在一個新的線程中執行組合操作 示例:
public class ThenAcceptTest {
    public static void main(String[] args) {
        CompletableFuture.supplyAsync(ThenAcceptTest::getList).thenAccept(strList -> strList.stream()
                .forEach(m -> System.out.println(m)));
    }

    public static List<String> getList() {
        return Arrays.asList("a", "b", "c");
    }
}

將前一個CompletableFuture計算出來的結果列印出來

thenCompose非同步結果流水化:

thenCompose方法可以將兩個非同步操作進行流水操作 提供了三個方法:

//方法一
public <U> CompletableFuture<U> thenCompose(
    Function<? super T, ? extends CompletionStage<U>> fn) {
    return uniComposeStage(null, fn);
}

//方法二
public <U> CompletableFuture<U> thenComposeAsync(
    Function<? super T, ? extends CompletionStage<U>> fn) {
    return uniComposeStage(asyncPool, fn);
}

//方法三
public <U> CompletableFuture<U> thenComposeAsync(
    Function<? super T, ? extends CompletionStage<U>> fn,
    Executor executor) {
    return uniComposeStage(screenExecutor(executor), fn);
}

說明:

  • Function<? super T, ? extends CompletionStage<U>> fn參數 => 當前CompletableFuture計算結果的執行
  • Executor executor參數 => 自定義線程池
  • 同理以async結尾的方法將會在一個新的線程中執行組合操作 示例:
public class ThenComposeTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<Integer> result = CompletableFuture.supplyAsync(ThenComposeTest::getInteger)
                .thenCompose(i -> CompletableFuture.supplyAsync(() -> i * 10));
        System.out.println(result.get());
    }

    private static int getInteger() {
        return 666;
    }

    private static int expandValue(int num) {
        return num * 10;
    }
}

執行流程圖:

thenCombine組合結果:

thenCombine方法將兩個無關的CompletableFuture組合起來,第二個Completable並不依賴第一個Completable的結果 提供了三個方法:

//方法一
public <U,V> CompletableFuture<V> thenCombine(
    CompletionStage<? extends U> other,
    BiFunction<? super T,? super U,? extends V> fn) {
    return biApplyStage(null, other, fn);
}
  //方法二
  public <U,V> CompletableFuture<V> thenCombineAsync(
      CompletionStage<? extends U> other,
      BiFunction<? super T,? super U,? extends V> fn) {
      return biApplyStage(asyncPool, other, fn);
  }

  //方法三
  public <U,V> CompletableFuture<V> thenCombineAsync(
      CompletionStage<? extends U> other,
      BiFunction<? super T,? super U,? extends V> fn, Executor executor) {
      return biApplyStage(screenExecutor(executor), other, fn);
  }

說明:

  • CompletionStage<? extends U> other參數 => 新的CompletableFuture的計算結果
  • BiFunction<? super T,? super U,? extends V> fn參數 => 定義了兩個CompletableFuture對象完成計算後如何合併結果,該參數是一個函數式介面,因此可以使用Lambda表達式
  • Executor executor參數 => 自定義線程池
  • 同理以async結尾的方法將會在一個新的線程中執行組合操作

示例:

public class ThenCombineTest {
    private static Random random = new Random();
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<Integer> result = CompletableFuture.supplyAsync(ThenCombineTest::randomInteger).thenCombine(
                CompletableFuture.supplyAsync(ThenCombineTest::randomInteger), (i, j) -> i * j
        );

        System.out.println(result.get());
    }

    public static Integer randomInteger() {
        return random.nextInt(100);
    }
}

將兩個線程計算出來的值做一個乘法在返回 執行流程圖:

allOf&anyOf組合多個CompletableFuture:

方法介紹:

//allOf
public static CompletableFuture<Void> allOf(CompletableFuture<?>... cfs) {
    return andTree(cfs, 0, cfs.length - 1);
}
//anyOf
public static CompletableFuture<Object> anyOf(CompletableFuture<?>... cfs) {
    return orTree(cfs, 0, cfs.length - 1);
}

說明:

  • allOf => 所有的CompletableFuture都執行完後執行計算。
  • anyOf => 任意一個CompletableFuture執行完後就會執行計算

示例:

  • allOf方法測試
public class AllOfTest {
  public static void main(String[] args) throws ExecutionException, InterruptedException {
      CompletableFuture<Void> future1 = CompletableFuture.supplyAsync(() -> {
          System.out.println("hello");
          return null;
      });
      CompletableFuture<Void> future2 = CompletableFuture.supplyAsync(() -> {
          System.out.println("world"); return null;
      });
      CompletableFuture<Void> result = CompletableFuture.allOf(future1, future2);
      System.out.println(result.get());
  }
}

allOf方法沒有返回值,適合沒有返回值並且需要前面所有任務執行完畢才能執行後續任務的應用場景

  • anyOf方法測試
public class AnyOfTest {
  private static Random random = new Random();
  public static void main(String[] args) throws ExecutionException, InterruptedException {
      CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
          randomSleep();
          System.out.println("hello");
          return "hello";});
      CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
          randomSleep();
          System.out.println("world");
          return "world";
      });
      CompletableFuture<Object> result = CompletableFuture.anyOf(future1, future2);
      System.out.println(result.get());
 }

  private static void randomSleep() {
      try {
          Thread.sleep(random.nextInt(10));
      } catch (InterruptedException e) {
          e.printStackTrace();
      }
  }
}

兩個線程都會將結果列印出來,但是get方法只會返回最先完成任務的結果。該方法比較適合只要有一個返回值就可以繼續執行其他任務的應用場景

註意點

很多方法都提供了非同步實現【帶async尾碼】,但是需小心謹慎使用這些非同步方法,因為非同步意味著存在上下文切換,可能性能不一定比同步好。如果需要使用非同步的方法,先做測試,用測試數據說話!!!

CompletableFuture的應用場景

存在IO密集型的任務可以選擇CompletableFuture,IO部分交由另外一個線程去執行。Logback、Log4j2非同步日誌記錄的實現原理就是新起了一個線程去執行IO操作,這部分可以以CompletableFuture.runAsync(()->{ioOperation();})的方式去調用。如果是CPU密集型就不推薦使用了推薦使用並行流

優化空間

supplyAsync執行任務底層實現:

public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier) {
    return asyncSupplyStage(asyncPool, supplier);
}
static <U> CompletableFuture<U> asyncSupplyStage(Executor e, Supplier<U> f) {
    if (f == null) throw new NullPointerException();
    CompletableFuture<U> d = new CompletableFuture<U>();
    e.execute(new AsyncSupply<U>(d, f));
    return d;
}

底層調用的是線程池去執行任務,而CompletableFuture中預設線程池為ForkJoinPool

private static final Executor asyncPool = useCommonPool ?
        ForkJoinPool.commonPool() : new ThreadPerTaskExecutor();

ForkJoinPool線程池的大小取決於CPU的核數。CPU密集型任務線程池大小配置為CPU核心數就可以了,但是IO密集型,線程池的大小由CPU數量 * CPU利用率 * (1 + 線程等待時間/線程CPU時間)確定。而CompletableFuture的應用場景就是IO密集型任務,因此預設的ForkJoinPool一般無法達到最佳性能,我們需自己根據業務創建線程池。

近期熱文推薦:

1.1,000+ 道 Java面試題及答案整理(2022最新版)

2.勁爆!Java 協程要來了。。。

3.Spring Boot 2.x 教程,太全了!

4.別再寫滿屏的爆爆爆炸類了,試試裝飾器模式,這才是優雅的方式!!

5.《Java開發手冊(嵩山版)》最新發佈,速速下載!

覺得不錯,別忘了隨手點贊+轉發哦!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 飛碼是京東科技研發的低代碼產品,可使營銷運營域下web頁面快速搭建。飛碼是單web頁面搭建工具,從創建頁面到監測再到投產的一站式解決方案 ...
  • 接上一節:從零用VitePress搭建博客教程(5) - 如何自定義頁面模板、給頁面添加獨有的className和使頁面標題變成側邊目錄? 九、第三方組件庫的使用 我們經常看見UI組件庫的文檔,這裡我們就用element-plus第三方組件庫為例子,搭建組件庫文檔 examples:作為組件庫示例目 ...
  • Hope is a good thing, maybe the best of things, and no good thing ever dies. 希望是件美麗的東西,也許是最好的東西,而美好的東西是永遠不會消逝的。 大家好,我是勇哥 。 1024 , 程式員節,圓了我一個小小的夢。 花了半年 ...
  • 1 我的高價位同事 我有個互聯網前同事快40了,之前35k,裁員後找了很久工作。最後18k入職一家公司繼續乾。只要降低預期就行了。8k不行就4k,4k不行就1k。那樣了是不是還不如開滴滴、送外賣、做物流。那樣多數人會選擇開滴滴去單的。熬吧,人口今年開始負增長了。捲王,工賊,潤的潤,捲的捲,都是個人選 ...
  • JUC前置知識 JUC概述 在開發語言中,線程部分是重點,JUC是關於線程的。JUC是java.util.concurrent工具包的簡稱。這是一個處理線程的工具包,JDK1.5開始出現的。 線程和進程 線程和進程的概念 進程(process): 是電腦的程式關於某數據集合上的一次允許活動,是操作 ...
  • 為何強大 記錄全面: 包含請求路徑、請求方法、客戶端IP、設備標識、荷載數據、文件上傳、請求頭、業務邏輯處理時間、業務邏輯所耗記憶體、用戶id、以及響應數據。 配置簡單: 預設不需要寫任何邏輯可開箱即用,靠前4個方法,就可指定某些url不記錄日誌,或不記錄某些請求頭,不記錄某些荷載數據,或決定是否返回 ...
  • Python 沒有內置支持數組,但可以使用 Python 列表來代替。 數組 本頁將向您展示如何使用列表作為數組,但要在 Python 中使用數組,您需要導入一個庫,比如 NumPy 庫。數組用於在一個變數中存儲多個值: 示例,創建一個包含汽車名稱的數組: cars = ["Ford", "Volv ...
  • 網上搜索類似的文章有很多,但是一味的複製粘貼總會出現各種奇葩問題,最後然並卵!今天特意自己研究琢磨一下,將最終結果分享給大家,100%親測可用。 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...