【2023年】CompletableFutureの利用コードケース紹介~インターンシップ利用シナリオ~(ナニーレベルチュートリアル)

1. はじめに

1。概要

CompletableFutureこれは Java 8 で導入された非同期プログラミング ツールであり、java.util.concurrent パッケージに含まれています。これは、非同期タスクを実行し、タスクの完了時に適切なアクションを実行するための便利な方法を提供します。

  • CompletableFutureFutureこれは権利の拡大と強化であり、CompletableFuture達成されることもあれば失敗することもありますFutureこれは、非同期タスクの完了の処理、例外処理、複数の非同期タスクの結果の結合など、非同期操作の結果を処理するための一般的なメカニズムを提供します。
  • また、CompletionStage を実装することで、タスクをオーケストレーションする機能が実現され、特定のステージが実行されると、後続のステージを下位に実行できます。
  • 非同期実行時、スレッド プールが定義されていない場合は、デフォルトのスレッド プールが使用されますForkJoinPool.commonPool()が、実際の開発では、デフォルトのスレッド プールのスレッドがいっぱいになることによるブロックの問題を避けるために、スレッド プールを自分で定義することをお勧めします。

2. 一般的に使用される方法

CompletableFutureメソッドはAsyncで終了しません。これは、アクションが同じスレッドを使用して実行することを意味します。Async他のスレッドを使用して実行することもできます (同じスレッド プールが使用されている場合は、同じスレッドによって実行用に選択されることもあります)。

メソッド名 説明する 戻り値 静的メソッド
供給非同期 戻り値 CompletableFuture を使用してタスクを非同期的に実行する 持っている はい
runAsync 戻り値なしでタスクを非同期実行する CompletableFuture なし はい
参加する CompletableFuture の戻り値を非同期で取得し、未チェック例外をスローします。 持っている いいえ
得る CompletableFuture の戻り値を非同期で取得します。スローされるのはチェック例外であり、手動で処理する必要があります (スローまたはキャッチを試行)。 持っている いいえ
完了時 CompletableFuture が完了した後 (正常または異常)、指定された操作を実行するために使用されます。 持っている いいえ
例外的に CompletableFuture タスクは、例外が発生すると実行され、例外が発生しない場合は実行されません。 持っている いいえ
次に適用する 現在のタスクが完了したら、関数を実行して新しい CompletableFuture を返します。 持っている いいえ
次に作成します 現在のタスクが完了したら、関数を実行し、新しい CompletableFuture を返します (thenApply の入力パラメータとは異なります)。 持っている いいえ
その後受け入れます 前のタスクが完了したら、戻り値なしでコンシューマ関数を実行します。 なし いいえ
その後両方を受け入れます 両方の CompletableFuture が実行されると、指定された操作が実行され、戻り値はありません。 なし いいえ
それから実行します 前段のタスクの実行が完了すると、入力パラメータのないタスクが非同期で実行されます。 なし いいえ
次に結合します 2 つの CompletableFuture の結果を結合し、新しい CompletableFuture を返します。 持っている いいえ
どちらかに適用 最初に実行された 2 つのタスクを比較して処理し、処理に使用された方のタスクの結果を使用して、新しい結果を返します。 持っている いいえ
どちらかを受け入れる 最初に実行された 2 つのタスクを比較して処理し、処理に使用された方のタスクの結果を使用します。結果は直接消費され、新しい結果は返されません。 なし いいえ
どちらかの後に実行 比較および処理される 2 つのタスクのうち、どちらが先に実行され、直接次の操作に進み、前のステップの実行結果は気にせず、戻りません。 なし いいえ
すべての 今後のタスク オブジェクトの配列を受け取り、すべてのタスクが完了すると新しいタスクを返します。 なし はい
のいずれか 今後のタスク オブジェクトの配列を受け取ります。タスクが完了すると、タスクが返されます。 持っている はい

これらのメソッドは、CompletableFuture インスタンスを作成、結合、処理して非同期プログラミングを可能にするために使用されます。各メソッドには異なる機能と用途があり、特定のニーズに応じて適切なメソッドを選択して構築できます。

2. メソッドの使用

1. 非同期操作

1.1. タスクの作成 (runAsync | SupplyAsync)

runAsync

runAsync()戻り値はありません。パラメータは Runnable 関数タイプで、スレッド プールを指定できます。指定しない場合は、デフォルトのスレッド プール ForkJoinPool.commonPool() が内部で使用されます。


        Runnable runnable = () -> {
    
    

            System.out.println("无返回值的异步任务"+Thread.currentThread().getName());
        };
//			非简写方式
        CompletableFuture.runAsync(runnable).join();

供給非同期

supplyAsync戻り値があり、その他の使い方は基本的に runAsync() と同じで、返す際に型を指定できます。

//			简写方式
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    
    
            System.out.println("有返回值的异步任务"+Thread.currentThread().getName());
            try {
    
    
                Thread.sleep(5000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            return "Hello World!";
        }, Executors.newFixedThreadPool(1));   //指定线程池
        String join = future.join();
        System.out.println(Thread.currentThread().getName()+"结果"+join);

1.2. 結果を取得する (get | join)

get() と join はどちらも CompletableFuture の戻り値を非同期で取得するために使用されます。join() メソッドは未チェック例外 (つまり、未チェック例外) をスローしますが、開発者にそれをスローするよう強制するものではありません。get() メソッドはチェックされた例外をスローします。ExecutionException と InterruptedException はユーザーが手動で処理する必要があります (スローまたはキャッチを試行)。

1.3. 例外処理 (whenComplete | 例外的に)

完了時

完了後に指定された操作を実行するために使用されますCompletableFuture(正常完了または異常完了)。正常にCompletableFuture完了したか例外が発生したかにかかわらず、完了時に追加の操作を実行するのに便利なツール (finally 部分に相当)、例外と結果が返されます。一方、もう一方は null になります。

  • シナリオ: ログを記録し、リソースをクリーンアップする
  • コード
         CompletableFuture.supplyAsync(() -> {
    
    
            System.out.println("无返回值的异步任务线程===="+Thread.currentThread().getName());
            return "----德玛西亚!-----";
        }).whenComplete((u, e) -> {
    
     //(u:没有异常时返回的结果,e:有异常时返回的结果)
            System.out.println("future线程======" + Thread.currentThread().getName());

            System.out.println("执行结果:"+u);
            System.out.println("异常输出:" + e);
        }).join();
  • ログ出力
    ここに画像の説明を挿入します

例外的に

Exceptionally を併用可能: 例外が発生した場合のみ、Exceptionally 内のタスクが実行されます (catch 部分に相当)

  • コード:
        CompletableFuture<String>  future = CompletableFuture.supplyAsync(() -> {
    
    
                try {
    
    
                    Thread.sleep(100);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                Integer i= 10/0;    //模拟一个异常
                return "Hello World!";
         }).whenCompleteAsync((u, e) -> {
    
       //(u:没有异常时返回的结果,e:有异常时返回的结果)

            try {
    
    
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException ex) {
    
    
                ex.printStackTrace();
            }
            System.out.println("执行结果:"+u); 
            System.out.println("异常输出:" + e);
            
        }).exceptionally(t -> {
    
        //进行异常处理,无异常时不会执行下面的处理

            System.out.println("执行失败进行异常处理!");
            return "异常XXXX--" + t;
        });

        System.out.println("结果:"+future.join());
  • ログ出力:
    • 例外発生時の出力:ここに画像の説明を挿入します
    • 例外がない場合の出力:
      ここに画像の説明を挿入します

2. シナリオの使用

2.1. 結果の変換 (thenApply | thenCompose)

thenApplyと はthenComposeどちらもチェーン操作に使用されますが、パラメーターの入力方法が異なります。
機能: 通常、非同期処理が必要な一部の結果を処理するために使用されます。このメソッドは、Future の結果の型を変更しません。実行方法はパイプラインのようなもので、ステップごとに実行されます。使い方はストリームと同じです。これは、いくつかの中間操作を実行するために使用されます。たとえば
、文字列の結果を大文字または小文字に変換したり、数値の計算を実行したりできます。例: 未来 1 -> 将来 2 -> 将来 3

次に適用する

thenApply: 関数をパラメータとして受け取り、その関数を使用してCompletableFuture前の呼び出しの結果を処理し、処理結果を含​​む Future オブジェクトを返します。

  • コード:
        CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {
    
    
            int result = new Random().nextInt(5) ;
            try {
    
    
                TimeUnit.SECONDS.sleep(result);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            System.out.println("建立人物模型,耗时:"+result);
            return result;
        }, Executors.newFixedThreadPool(5));

//        thenApply: 接收一个函数作为参数,使用该函数处理上一个CompletableFuture调用的结果,并返回一个具有处理结果的Future对象

        CompletableFuture<Integer> future2 = future1.thenApply(number -> {
    
      //把future1的结果作为入参
            int result = new Random().nextInt(5) ;
            try {
    
    
                TimeUnit.SECONDS.sleep(result);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }

            System.out.println("加载模型骨骼,耗时:"+result);
            return result+number;
        });
        
        System.out.println("全部加载完成,耗时:"+future2.join()+"y");
  • ログ出力:
    ここに画像の説明を挿入します

次に作成します

thenCompose: パラメーターはCompletableFutureインスタンスを返す関数であり (このインスタンスのパラメーターは前の計算ステップの結果です)、他の使用法には違いはありませんthenApply

  • コード:
//			拼接前面thenApply部分的代码
        CompletableFuture<Integer> future3 = future2.thenCompose(param ->
                CompletableFuture.supplyAsync(() -> {
    
    
                    int result = new Random().nextInt(5) ;
                    try {
    
    
                        TimeUnit.SECONDS.sleep(result);
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }

                    System.out.println("加载模型皮肤,耗时:"+result);
                    return result+param;
                })
        );
          不简写的方式
//        CompletableFuture<Integer> future4 = future2.thenCompose(new Function<Integer, CompletionStage<Integer>>() {
    
    
//            @Override
//            public CompletionStage<Integer> apply(Integer param) {
    
    
//                return CompletableFuture.supplyAsync(new Supplier<Integer>() {
    
    
//                    @Override
//                    public Integer get() {
    
    
//                        int result = new Random().nextInt(5);
//                        try {
    
    
//                            TimeUnit.SECONDS.sleep(result);
//                        } catch (InterruptedException e) {
    
    
//                            e.printStackTrace();
//                        }
//                        System.out.println("加载模型皮肤,耗时:" + result);
//                        return result + param;
//                    }
//                });
//            }
//        });

        System.out.println("全部加载完成,耗时:"+future3.join()+"y");
  • ログ出力:
    ここに画像の説明を挿入します

2.2. 結果の消費 (thenAccept | thenAccept Both | thenRun)

その後受け入れます

thenAcceptこれは、前の Future タスクの結果を非同期的に処理し、値を返さずに結果を消費するために使用されます
。たとえば、一部の操作を完了するには前提条件が必要で、非同期実行では現在のスレッドをブロックする必要がなく、次の方法で同時に処理できます。非同期実行複数のタスク

    public static void main(String[] args) {
    
    
//        消费之前,有返回Integer
        CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {
    
    
            try {
    
    
                Thread.sleep(1000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            int number = new Random().nextInt(10);
            System.out.println("加载商城页面" + number);
            return number;
        });
        System.out.println("商城页面加载成功:==="+future1.join()+"===继续加载商城内置页面!");


        CompletableFuture<Void> future2 = future1.thenAcceptAsync(number -> {
    
      //结果消费掉之后会没有返回值
            try {
    
    
                Thread.sleep(2000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            int n2 = number * 2;
            System.out.println("加载商城英雄页面:" + n2);
        });
        CompletableFuture<Void> future3 = future1.thenAcceptAsync(number -> {
    
      //结果消费掉之后会没有返回值
            try {
    
    
                Thread.sleep(2000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            int n2 = number * 3;
            System.out.println("加载商城皮肤页面:" + n2);
        });
        future2.join();
        future3.join();
        System.out.println("商城内置页面也加载成功:");
    }

その後両方の非同期を受け入れます

thenAcceptBothAsyncこれは、2 つの先物の結果を消費します。x は現在のもので、y は入力パラメーターです
。戻り値はありません。主に、前の 2 つのタスクを完了する必要がある場合に使用され、その後、前の 2 つのタスクの結果を消費します。 thenAccept Bothタスクを実行する(ただし、このメソッドの先行タスクの実行順序は確認できない)

  • コード:
        //        消费之前,有返回Integer
        CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {
    
    
            int number = new Random().nextInt(10);
            System.out.println("工商银行储蓄卡余额:" + number);
            return number;
        });
        CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> {
    
    
            int number = new Random().nextInt(10);
            System.out.println("工商银行信用卡余额:" + number);
            return number;
        });

//        thenAcceptBothAsync会消费两个future的结果,x当前的,y入参的,无返回值
//                作用:主要用于当需要两个前置任务完成后,
//                  再消费两个前置任务的结果去执行thenAcceptBoth的任务(需要注意的是该方法的前置任务的执行顺序是无法确认的)

         future1.thenAcceptBothAsync(future2, (x, y) ->  //会消费掉两个前置任务的结果
                System.out.println("工商银行总余额:" + (x + y))
        );
  • ログ出力:
    ここに画像の説明を挿入します

それから実行します

thenRunこれは主に、前のタスクが完了した後に入力パラメーターなしでタスクを非同期に実行するために使用されます。通常は、クリーニング、ロギング、リソースのクローズ、ロックの解放などに使用されます。

  • コード:
        CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {
    
    
            System.out.println(Thread.currentThread().getName());
            int number = new Random().nextInt(10);

            System.out.println("初始值:" + number);

            return number;
        });

//        thenRun:主要在前任务执行完成后异步执行一个不带任何输入参数的任务;一般用于某些清理,日志记录,关闭资源、释放锁等
        future1.thenRun(()->{
    
    
            System.out.println(Thread.currentThread().getName()+"执行run关闭资源");
        });
  • ログ出力:
    ここに画像の説明を挿入します

2.3. タスクの組み合わせ (その後Combine)

次に結合します

thenCombine:thenAcceptBoth使用と同様に、2 つのタスク前フューチャーの結果が結合されて最終処理に使用されますが、結合はthenCombine処理されてから新しい値が返されます
シナリオ: 一般に、並列データの取得と結合、および一部のデータの変換と結合に使用されます

  • コード
        int i = 10;
        CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {
    
    
            int number = new Random().nextInt(10) * i;
            System.out.println("获取用户的信息:" + number);
            return number;
        });

        CompletableFuture<Integer> future2 =CompletableFuture.supplyAsync(()->{
    
    
        int number = new Random().nextInt(10) * i;
        System.out.println("获取用户的订单信息:"+number);
        return number;
        });
        
        //不简写写法
//        CompletableFuture<Integer> result = future1
//                .thenCombine(future2, new BiFunction<Integer, Integer, Integer>() {
    
    
//                    @Override
//                    public Integer apply(Integer x, Integer y) {
    
    
//                        return x + y;
//                    }
//                });

//       
        CompletableFuture<Integer> future3 = future1.thenCombine(future2, (x, y) -> {
    
    
            Integer sum = x + y;
            System.out.println("进行合并处理:" + sum);
            return sum;
        });
        System.out.println("最终处理结果信息:"+future3.get());

  • ログ出力:
    ここに画像の説明を挿入します

2.4. タスクの比較 (applyToEither | acceptEither | runAfterEither)

どちらかに適用

applyToEither: 主に、最初に実行されたタスクの結果を比較して処理するために使用されます。
シナリオapplyToEither: 2 つの異なるリモート サーバーからデータを取得する必要があるとします)、または 2 つの通信プロトコル (SSH、HTTP) を使用してデータを取得する必要があるとします。処理結果を取得するための最速の方法を非同期的に選択できます。

  • コード:
        int i= 250;
        CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {
    
    
            int number = new Random().nextInt(5);
            try {
    
    
                TimeUnit.SECONDS.sleep(number);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            System.out.println("使用银河计算机计算结果为:" + i+",耗时:"+number+"秒");
            return number;
        });

        CompletableFuture<Integer> future2 =CompletableFuture.supplyAsync(()->{
    
    
            int number = new Random().nextInt(5);
            try {
    
    
                TimeUnit.SECONDS.sleep(number);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            System.out.println("使用长城计算机计算结果为:" + i+",耗时:"+number+"秒");
            return number;
        });
		//applyToEither会得到最先执行完成的结果,进行处理,并返回一个新的结果
        CompletableFuture<String> future3 = future1.applyToEither(future2, (x) -> {
    
    
            System.out.println("结果计算成功" );
            return "最终结果执行耗时:"+x ;
        });

        System.out.println(future3.join());
        try {
    
    
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
  • ログ出力:
    ここに画像の説明を挿入します

どちらかを受け入れる

acceptEitherAsync:applyToEither意味と使用法は同じですが、acceptEither は値を返さずに前のタスクの結果を直接消費します
シナリオ: 2 つの非同期操作のいずれかが完了したときに、いくつかの副作用を実行するのに適しています。

  • コード:
        int i= 10;
        CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
    
    
            int number = new Random().nextInt(5);
            try {
    
    
                TimeUnit.SECONDS.sleep(number);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            System.out.println("使用银河计算机计算结果为:" + i+",耗时:"+number+"秒");
            return "银河计算机";
        });

        CompletableFuture<String> future2 =CompletableFuture.supplyAsync(()->{
    
    
            int number = new Random().nextInt(5);
            try {
    
    
                TimeUnit.SECONDS.sleep(number);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            System.out.println("使用长城计算机计算结果为:" + i+",耗时:"+number+"秒");
            return "长城计算机";
        });

//        acceptEitherAsync:和applyToEither意思和使用都一样,只不过acceptEither会把前置任务的结果直接消费掉,不会有返回值
        future1.acceptEitherAsync(future2, (x) -> {
    
    
            System.out.println("最终处理结果采用:" + x);
        }).join();


        try {
    
    
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
  • ログ出力:
    ここに画像の説明を挿入します

どちらかの後に実行

runAfterEither:前の 2 つと同様に、2 つのプリタスクを比較してどちらが先に実行されるかを確認してから次のステップに進みますが、プリタスクの処理結果は考慮されません (入力runAfterEitherパラメーターはありません)。
での使用に適した戻り値はありません。 2 つの非同期操作のいずれかが完了した後、結果に依存しない何らかの操作を実行します。

  • コード:
        int i= 10;
        CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {
    
    
            int number = new Random().nextInt(5);
            try {
    
    
                TimeUnit.SECONDS.sleep(number);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            System.out.println("通过http传输数据:" + i+",耗时:"+number+"秒");
            return number;
        });


        CompletableFuture<Integer> future2 =CompletableFuture.supplyAsync(()->{
    
    
            int number = new Random().nextInt(5);
            try {
    
    
                TimeUnit.SECONDS.sleep(number);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            System.out.println("通过web socket传输数据:" + i+",耗时:"+number+"秒");
            return number;
        });
		//没有参数也没有返回值
        future1.runAfterEither(future2,()->{
    
    
            System.out.println("任务传输成功!");
        }).join();
  • ログ出力:
    ここに画像の説明を挿入します

2.4. バッチ処理タスク (allOf | anyOf)

すべての

allOf戻り値はありません。将来のタスク オブジェクトの配列をパラメータとして受け取り、新しいオブジェクトを返しますCompletableFutureこの新しいオブジェクトは、
CompletableFuture<Void>渡されたすべてのオブジェクトが完了するCompletableFutureまで完了せず、戻り値はありません。
シナリオ: 一連の独立した非同期タスクを並行して実行し、すべてのタスクが完了したら次の操作に進みます。複数の非同期操作が完了するまで待ってから、何らかの集計操作などを実行します。

-コード:

        CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
    
    
            int number = new Random().nextInt(5);
            try {
    
    
                TimeUnit.SECONDS.sleep(number);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            System.out.println(i+"写入mysql成功,耗时:"+number);
            return "mysql";
        });

        CompletableFuture<Void> future2 = CompletableFuture.runAsync(() -> {
    
    
            int number = new Random().nextInt(5);
            try {
    
    
                TimeUnit.SECONDS.sleep(number);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            System.out.println(i+"写入sqlserver成功,耗时:"+number);

        });
        CompletableFuture.allOf(future1, future2).join();
        System.out.println("写入数据库成功,执行后续。。。。");
  • ログ出力:
    ここに画像の説明を挿入します

のいずれか

anyOfこれはCompletableFuture静的メソッドであり、主に複数の Future タスクを受信し、最初に実行されたタスクの結果を選択して使用するために使用されます; 静的anyOfメソッドであり、複数の Future タスクを受信できます;
シナ​​リオ: 複数のタスクが存在する場合にも適用できます独立したタスクの最速の結果を選択するシナリオ、または一定期間内に複数のタスクのいずれかを完了し、タイムアウトが発生したときに特定の操作を実行する場合に使用されるタイムアウト シナリオ。

  • コード:
        CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
    
    
            int number = new Random().nextInt(5);
            try {
    
    
                TimeUnit.SECONDS.sleep(number);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            System.out.println("乌龟到达终点,耗时:"+number+"秒");
            return "乌龟";
        });

        CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
    
    
            int number = new Random().nextInt(5);
            try {
    
    
                TimeUnit.SECONDS.sleep(number);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            System.out.println("兔子到达终点,耗时:"+number+"秒");
            return "兔子";
        });
        CompletableFuture<String> future3 = CompletableFuture.supplyAsync(() -> {
    
    
            int number = new Random().nextInt(5);
            try {
    
    
                TimeUnit.SECONDS.sleep(number);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            System.out.println("蜗牛到达终点,耗时:"+number+"秒");
            return "蜗牛";
        });
        
        CompletableFuture<Object> result = CompletableFuture.anyOf(future1, future2 ,future3);
        System.out.println("获得第一名是:"+result.join());

        try {
    
    
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }

  • ログ出力: —
    ここに画像の説明を挿入します
    導入部分の一部は https://blog.csdn.net/sermonlizhi/article/details/ から引用

おすすめ

転載: blog.csdn.net/weixin_52315708/article/details/132665368