跳到正文
hello world

37. thenCompose:一个异步任务依赖另一个异步任务

发布于阅读量 0

37. thenCompose:一个异步任务依赖另一个异步任务

上一节讲了 thenCombine()

它适合这种场景:

任务一和任务二互不依赖;

两个任务可以同时执行;

等两个任务都完成以后合并结果。

这一节看另一个很常见的场景:

第二个异步任务必须等待第一个异步任务完成;

并且第二个任务需要使用第一个任务的结果。

比如 PDF 水印项目里:

第一步:给 PDF 添加水印,返回水印文件路径;

第二步:把水印文件上传到远程存储,返回下载地址。

上传任务不能提前执行。

因为在水印文件生成之前,我连要上传哪个文件都不知道。

这种前后依赖的异步任务,就适合使用:

thenCompose(...)

thenCompose 解决什么问题

先看两个异步方法。

第一个方法处理 PDF:

private static CompletableFuture<String> addWatermarkAsync(File file) {

    return CompletableFuture.supplyAsync(() -> {
        return "output/test-watermark.pdf";
    });
}

第二个方法上传 PDF:

private static CompletableFuture<String> uploadAsync(String targetPath) {

    return CompletableFuture.supplyAsync(() -> {
        return "https://file.example.com/test-watermark.pdf";
    });
}

上传方法需要水印文件路径。

所以执行顺序必须是:

addWatermarkAsync(file)

↓

拿到 targetPath

↓

uploadAsync(targetPath)

两个步骤都是异步任务。

这时候可以写:

CompletableFuture<String> future = addWatermarkAsync(file)
        .thenCompose(targetPath -> uploadAsync(targetPath));

最后:

String downloadUrl = future.join();

拿到的就是上传后的下载地址。


一个最简单的 thenCompose 示例

新建类:

com.succos.completablefuture.ThenComposeDemo

代码如下:

package com.succos.completablefuture;

import java.util.concurrent.CompletableFuture;

public class ThenComposeDemo {

    public static void main(String[] args) {

        CompletableFuture<String> resultFuture =
                queryUserIdAsync("张三")
                        .thenCompose(userId -> {
                            return queryUserNameAsync(userId);
                        });

        String result = resultFuture.join();

        System.out.println("最终结果:" + result);
    }

    private static CompletableFuture<Integer> queryUserIdAsync(
            String keyword
    ) {
        return CompletableFuture.supplyAsync(() -> {

            System.out.println(Thread.currentThread().getName()
                    + " 开始查询用户 ID");

            sleep(2000);

            System.out.println(Thread.currentThread().getName()
                    + " 用户 ID 查询完成");

            return 1001;
        });
    }

    private static CompletableFuture<String> queryUserNameAsync(
            Integer userId
    ) {
        return CompletableFuture.supplyAsync(() -> {

            System.out.println(Thread.currentThread().getName()
                    + " 根据用户 ID 查询姓名:"
                    + userId);

            sleep(2000);

            return "张三";
        });
    }

    private static void sleep(long millis) {

        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("任务被中断", e);
        }
    }
}

这里的执行顺序很明确:

先查询用户 ID;

拿到用户 ID 以后;

再根据 ID 查询用户姓名。

第二个任务不能和第一个任务同时执行,因为它需要第一个任务返回的 userId


thenCompose 的 Lambda 返回 CompletableFuture

这一点是理解 thenCompose() 的关键。

这里写的是:

.thenCompose(userId -> {
    return queryUserNameAsync(userId);
});

queryUserNameAsync() 返回的是:

CompletableFuture<String>

所以 thenCompose() 里面返回的不是普通字符串,而是另一个异步任务。

最终得到的类型仍然是:

CompletableFuture<String>

也就是说,thenCompose() 把两个前后依赖的异步任务连接成了一条流程。


PDF 水印和文件上传示例

回到 PDF 项目。

流程是:

添加水印;

拿到水印文件路径;

上传到远程存储;

拿到远程下载地址。

新建类:

com.succos.completablefuture.ThenComposePdfDemo

代码如下:

package com.succos.completablefuture;

import java.io.File;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class ThenComposePdfDemo {

    public static void main(String[] args) {

        ThreadPoolExecutor pdfExecutor = new ThreadPoolExecutor(
                3,
                3,
                60,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(100),
                new NamedThreadFactory("pdf-worker"),
                new ThreadPoolExecutor.CallerRunsPolicy()
        );

        ThreadPoolExecutor uploadExecutor = new ThreadPoolExecutor(
                3,
                3,
                60,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(100),
                new NamedThreadFactory("upload-worker"),
                new ThreadPoolExecutor.CallerRunsPolicy()
        );

        File file = new File("input/test.pdf");

        CompletableFuture<String> resultFuture =
                addWatermarkAsync(file, pdfExecutor)
                        .thenCompose(targetPath -> {
                            return uploadAsync(
                                    targetPath,
                                    uploadExecutor
                            );
                        });

        String downloadUrl = resultFuture.join();

        System.out.println("--------------------------------");
        System.out.println("最终下载地址:" + downloadUrl);

        pdfExecutor.shutdown();
        uploadExecutor.shutdown();
    }

    private static CompletableFuture<String> addWatermarkAsync(
            File file,
            ThreadPoolExecutor pdfExecutor
    ) {
        return CompletableFuture.supplyAsync(() -> {

            System.out.println(Thread.currentThread().getName()
                    + " 开始添加水印:"
                    + file.getName());

            sleep(3000);

            String targetPath = "output/"
                    + file.getName()
                    .replace(".pdf", "-watermark.pdf");

            System.out.println(Thread.currentThread().getName()
                    + " 水印处理完成:"
                    + targetPath);

            return targetPath;

        }, pdfExecutor);
    }

    private static CompletableFuture<String> uploadAsync(
            String targetPath,
            ThreadPoolExecutor uploadExecutor
    ) {
        return CompletableFuture.supplyAsync(() -> {

            System.out.println(Thread.currentThread().getName()
                    + " 开始上传:"
                    + targetPath);

            sleep(2000);

            String downloadUrl =
                    "https://file.example.com/test-watermark.pdf";

            System.out.println(Thread.currentThread().getName()
                    + " 上传完成:"
                    + downloadUrl);

            return downloadUrl;

        }, uploadExecutor);
    }

    private static void sleep(long millis) {

        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("任务被中断", e);
        }
    }

    static class NamedThreadFactory implements ThreadFactory {

        private final String prefix;

        private final AtomicInteger threadNumber =
                new AtomicInteger(1);

        public NamedThreadFactory(String prefix) {
            this.prefix = prefix;
        }

        @Override
        public Thread newThread(Runnable r) {

            Thread thread = new Thread(r);

            thread.setName(
                    prefix
                            + "-"
                            + threadNumber.getAndIncrement()
            );

            return thread;
        }
    }
}

运行时可能看到:

pdf-worker-1 开始添加水印:test.pdf

等待 3 秒...

pdf-worker-1 水印处理完成:output/test-watermark.pdf

upload-worker-1 开始上传:output/test-watermark.pdf

等待 2 秒...

upload-worker-1 上传完成:https://file.example.com/test-watermark.pdf

最终下载地址:https://file.example.com/test-watermark.pdf

这里能看出:

上传任务没有提前开始;

必须等水印任务返回文件路径以后;

才会进入 uploadExecutor 执行上传任务。

为什么不用 thenApply

看到这里可能会有一个问题。

前面已经学过 thenApply(),它也能拿到上一步的结果。

那为什么不这样写:

CompletableFuture<CompletableFuture<String>> future =
        addWatermarkAsync(file, pdfExecutor)
                .thenApply(targetPath -> {
                    return uploadAsync(
                            targetPath,
                            uploadExecutor
                    );
                });

这段代码可以编译。

但它的返回类型变成了:

CompletableFuture<CompletableFuture<String>>

也就是:

一个 CompletableFuture 里面;

又包了一个 CompletableFuture。

想拿到真正下载地址,就要写:

String downloadUrl = future
        .join()
        .join();

需要连续 join() 两次。

这就不太自然。


thenApply 会产生嵌套 Future

先看结构:

addWatermarkAsync(...)
        .thenApply(targetPath -> uploadAsync(targetPath));

上一步返回:

String targetPath

Lambda 里返回:

CompletableFuture<String>

thenApply() 的作用是:

把 Lambda 的返回值当成普通结果包装起来。

所以最终类型就是:

CompletableFuture<CompletableFuture<String>>

可以把它想成:

外层 Future 完成后;

得到的结果不是下载地址;

而是另一个 Future。

所以还要再等一次。


thenCompose 会自动展开嵌套

如果换成:

addWatermarkAsync(...)
        .thenCompose(targetPath -> uploadAsync(targetPath));

最终类型就是:

CompletableFuture<String>

thenCompose() 会把内层的 CompletableFuture<String> 展开。

可以简单理解成:

thenApply:

CompletableFuture<CompletableFuture<String>>

thenCompose:

CompletableFuture<String>

这就是 thenCompose() 存在的意义。

它专门处理“上一步完成后,再返回一个新的异步任务”的场景。


thenApply 和 thenCompose 怎么区分

我现在主要看 Lambda 返回什么。

如果 Lambda 返回普通结果:

.thenApply(path -> {
    return buildDownloadUrl(path);
})

其中:

buildDownloadUrl(path)

返回普通 String

就用 thenApply()

如果 Lambda 返回另一个异步任务:

.thenCompose(path -> {
    return uploadAsync(path);
})

其中:

uploadAsync(path)

返回 CompletableFuture<String>

就用 thenCompose()

可以直接记成:

返回普通值:thenApply;

返回 CompletableFuture:thenCompose。

一个直观对比

普通转换:

CompletableFuture<String> urlFuture =
        pathFuture.thenApply(path -> {
            return "http://localhost/" + path;
        });

这里是:

String

↓

String

thenApply()

异步转换:

CompletableFuture<String> urlFuture =
        pathFuture.thenCompose(path -> {
            return uploadAsync(path);
        });

这里是:

String

↓

CompletableFuture<String>

thenCompose()


thenCompose 和 thenCombine 的区别

这两个方法名字比较像,但场景完全不同。

thenCombine() 是两个独立任务:

任务一 ─┐
        ├─ 都完成后合并结果
任务二 ─┘

例如:

处理 PDF;

查询用户信息;

两个任务可以同时执行。

代码:

pdfFuture.thenCombine(
        userFuture,
        (path, userName) -> {
            return userName + ":" + path;
        }
);

thenCompose() 是前后依赖:

任务一完成

↓

拿到任务一结果

↓

才能创建或执行任务二

例如:

处理 PDF;

拿到文件路径;

才能上传这个文件。

代码:

pdfFuture.thenCompose(
        path -> uploadAsync(path)
);

我会这样区分:

两个任务能不能同时开始?

能:可能适合 thenCombine;

不能,第二个依赖第一个结果:用 thenCompose。

thenComposeAsync 的区别

thenCompose() 也有带 Async 的版本:

thenComposeAsync(...)

还可以指定执行器:

thenComposeAsync(
        result -> nextAsyncTask(result),
        executor
)

不过这里要分清两个执行过程。

例如:

pdfFuture.thenComposeAsync(
        targetPath -> uploadAsync(
                targetPath,
                uploadExecutor
        ),
        composeExecutor
);

composeExecutor 控制的是:

创建和返回 uploadAsync 这个后续任务的 Lambda 在哪里执行。

uploadAsync() 内部真正的上传任务,还是由它自己指定的 uploadExecutor 执行。

如果 Lambda 里只是调用一下 uploadAsync(),逻辑非常轻,通常没有必要额外使用 thenComposeAsync()

直接写:

thenCompose(targetPath -> uploadAsync(
        targetPath,
        uploadExecutor
))

就够了。


不要为了异步而重复包装

有时会看到这种写法:

CompletableFuture
        .supplyAsync(() -> addWatermark(file), pdfExecutor)
        .thenCompose(targetPath -> {
            return CompletableFuture.supplyAsync(() -> {
                return upload(targetPath);
            }, uploadExecutor);
        });

这本身没问题。

因为第二步确实是另一个异步任务。

但如果 uploadAsync() 已经封装好了:

private CompletableFuture<String> uploadAsync(
        String path
) {
    return CompletableFuture.supplyAsync(() -> {
        return upload(path);
    }, uploadExecutor);
}

那外面直接写:

.thenCompose(this::uploadAsync)

就可以了。

没有必要层层再包一遍。


方法引用写法

假设方法是:

private static CompletableFuture<String> uploadAsync(
        String targetPath
) {
    return CompletableFuture.supplyAsync(() -> {
        return "https://file.example.com/test.pdf";
    });
}

原来的写法:

.thenCompose(targetPath -> {
    return uploadAsync(targetPath);
})

可以简化成:

.thenCompose(
        ThenComposePdfDemo::uploadAsync
)

如果是实例方法,也可以写:

.thenCompose(this::uploadAsync)

不过学习阶段我觉得 Lambda 更直观。

能明确看到:

上一步的 targetPath;

被传给了 uploadAsync。

多级异步依赖

thenCompose() 可以连续使用。

例如完整 PDF 流程:

添加水印;

上传文件;

保存数据库;

发送通知。

假设每一步都返回 CompletableFuture,可以写成:

CompletableFuture<NotifyResult> future =
        addWatermarkAsync(file)
                .thenCompose(targetPath -> {
                    return uploadAsync(targetPath);
                })
                .thenCompose(downloadUrl -> {
                    return saveTaskAsync(downloadUrl);
                })
                .thenCompose(taskId -> {
                    return notifyUserAsync(taskId);
                });

整个流程是串行依赖的。

但是每一步本身都由各自的线程池异步执行。

最后:

NotifyResult result = future.join();

就能拿到整条异步链的最终结果。


异步不等于并行

这里需要再强调一次。

这几个任务虽然都是异步任务:

添加水印;

上传文件;

保存数据库;

发送通知。

但它们不能并行执行。

因为后一步依赖前一步的结果。

执行顺序仍然是:

添加水印完成;

上传才能开始;

上传完成;

保存数据库才能开始;

数据库保存完成;

通知才能开始。

所以:

异步描述的是任务不一定由当前线程执行;

并行描述的是多个任务是否能同时执行。

thenCompose() 连接的是异步任务,但它们之间仍然是串行依赖。


thenCompose 中的异常传播

如果第一个任务失败:

CompletableFuture<String> future =
        addWatermarkAsync(file)
                .thenCompose(path -> uploadAsync(path));

那么 uploadAsync() 不会执行。

因为前一步没有正常返回 path

最终调用:

future.join();

会抛出异常。

如果第一步成功,但上传失败,最终结果同样会异常完成。

也就是说,异常会顺着整条链向后传播。


在链尾统一处理异常

可以在最后统一写:

CompletableFuture<PdfTaskResult> future =
        addWatermarkAsync(file, pdfExecutor)
                .thenCompose(targetPath -> {
                    return uploadAsync(
                            targetPath,
                            uploadExecutor
                    );
                })
                .thenApply(downloadUrl -> {
                    return PdfTaskResult.success(
                            file.getName(),
                            downloadUrl
                    );
                })
                .exceptionally(ex -> {
                    return PdfTaskResult.fail(
                            file.getName(),
                            getErrorMessage(ex)
                    );
                });

这里的流程是:

水印成功后上传;

上传成功后组装成功结果;

任何一步异常,都在 exceptionally 中转成失败结果。

这种写法比较适合完整业务链。


完整的异常处理示例

CompletableFuture<PdfTaskResult> future =
        addWatermarkAsync(file, pdfExecutor)
                .thenCompose(targetPath -> {

                    System.out.println("水印文件生成完成,准备上传:"
                            + targetPath);

                    return uploadAsync(
                            targetPath,
                            uploadExecutor
                    );
                })
                .thenApply(downloadUrl -> {

                    System.out.println("文件上传完成:"
                            + downloadUrl);

                    return PdfTaskResult.success(
                            file.getName(),
                            downloadUrl
                    );
                })
                .whenComplete((result, ex) -> {

                    if (ex != null) {
                        System.out.println("整条流程执行异常:"
                                + getErrorMessage(ex));
                    } else {
                        System.out.println("整条流程执行完成");
                    }
                })
                .exceptionally(ex -> PdfTaskResult.fail(
                        file.getName(),
                        getErrorMessage(ex)
                ));

这里:

thenCompose 连接两个异步任务;

thenApply 把下载地址转换成结果对象;

whenComplete 记录整个流程的执行情况;

exceptionally 负责最终兜底。

thenCompose 的典型场景

我觉得它比较适合这些业务:

先查询用户,再根据用户查询订单;

先创建订单,再异步发起支付;

先生成文件,再上传文件;

先获取 token,再调用需要 token 的接口;

先保存任务,再根据任务 ID 启动后续处理;

先添加 PDF 水印,再上传到远程存储。

这些场景都有共同特点:

第二个异步任务,需要第一个异步任务的返回值。

这一节小结

这一节我主要记住几点:

1. thenCompose 用来连接两个前后依赖的异步任务;
2. 第二个任务需要使用第一个任务的结果;
3. thenCompose 的 Lambda 返回的是另一个 CompletableFuture;
4. thenApply 返回异步任务时,会产生嵌套的 CompletableFuture;
5. thenCompose 会自动展开嵌套,得到一个普通的 CompletableFuture<T>;
6. 返回普通值用 thenApply,返回 CompletableFuture 用 thenCompose;
7. 两个任务互不依赖并且最后合并,用 thenCombine;
8. thenCompose 连接的是异步任务,但任务之间仍然是串行依赖。

用一句话总结:

上一步完成后,还要根据它的结果启动另一个异步任务,就用 thenCompose。

下一节继续看 anyOf

前面的 allOf 是等待所有任务完成,而 anyOf 是多个任务里只要有一个先完成,就可以先拿它的结果继续执行。