跳到正文
hello world

35. exceptionally、handle、whenComplete 的区别

发布于阅读量 0

35. exceptionally、handle、whenComplete 的区别

前面已经把 CompletableFuture 的正常流程跑通了:

异步处理 PDF;
拿到输出路径;
生成下载地址;
保存处理结果;
等待任务完成。

但真实任务不可能永远成功。

PDF 水印处理过程中,可能遇到很多问题:

源文件不存在;
PDF 文件已经损坏;
文件没有读取权限;
输出目录没有写入权限;
磁盘空间不足;
处理过程中发生异常;
上传远程存储失败。

所以异步流程不能只写成功逻辑,还要考虑异常怎么处理。

CompletableFuture 里常用的异常处理方法有三个:

exceptionally(...)
handle(...)
whenComplete(...)

这三个方法看起来很像,都能拿到异常,但用途并不一样。

我现在这样区分:

exceptionally:任务失败时,返回一个兜底结果。

handle:无论成功还是失败,都把结果重新转换一次。

whenComplete:观察任务执行结果,通常用于记录日志和做收尾,不改变原来的结果。

先准备一个可能失败的 PDF 任务

为了方便观察,我先写一个模拟 PDF 处理的方法:

private static String processPdf(boolean success) {

    System.out.println(Thread.currentThread().getName()
            + " 开始处理 PDF");

    sleep(2000);

    if (!success) {
        throw new RuntimeException("PDF 文件已经损坏");
    }

    return "output/test-watermark.pdf";
}

传入 true 时,任务正常返回输出路径。

传入 false 时,任务抛出异常。

后面就用这个方法观察三个异常处理方法的区别。


exceptionally:失败时返回兜底结果

exceptionally() 只在前面的任务发生异常时执行。

基本写法如下:

CompletableFuture<String> future = CompletableFuture
        .supplyAsync(() -> processPdf(false))
        .exceptionally(ex -> {
            System.out.println("PDF 处理失败:" + ex.getMessage());

            return "output/default-watermark.pdf";
        });

如果前面的任务成功,exceptionally() 不会执行。

如果前面的任务失败,exceptionally() 会接收到异常,并返回一个新的结果。

这里返回的是:

output/default-watermark.pdf

因此,后面调用:

String result = future.join();

拿到的是兜底结果,而不是继续抛出原来的业务异常。


exceptionally 完整示例

新建类:

com.succos.completablefuture.ExceptionallyDemo

代码如下:

package com.succos.completablefuture;

import java.util.concurrent.CompletableFuture;

public class ExceptionallyDemo {

    public static void main(String[] args) {

        CompletableFuture<String> future = CompletableFuture
                .supplyAsync(() -> processPdf(false))
                .exceptionally(ex -> {
                    System.out.println("PDF 处理失败");
                    System.out.println("失败原因:" + getErrorMessage(ex));

                    return "output/default-watermark.pdf";
                });

        String result = future.join();

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

    private static String processPdf(boolean success) {

        System.out.println(Thread.currentThread().getName()
                + " 开始处理 PDF");

        sleep(2000);

        if (!success) {
            throw new RuntimeException("PDF 文件已经损坏");
        }

        return "output/test-watermark.pdf";
    }

    private static String getErrorMessage(Throwable throwable) {

        Throwable cause = throwable;

        while (cause.getCause() != null) {
            cause = cause.getCause();
        }

        return cause.getMessage();
    }

    private static void sleep(long millis) {

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

运行后可能输出:

ForkJoinPool.commonPool-worker-1 开始处理 PDF
PDF 处理失败
失败原因:PDF 文件已经损坏
最终结果:output/default-watermark.pdf

这里最重要的变化是:

原来的异常被 exceptionally 转换成了一个正常结果。

因此,后面的异步流程还可以继续执行。


exceptionally 适合什么场景

exceptionally() 比较适合单独处理失败情况。

例如:

接口调用失败后返回缓存数据;
文件处理失败后返回失败结果对象;
非核心通知发送失败后记录日志;
下载地址生成失败后返回默认提示。

PDF 批量处理中,可以这样写:

CompletableFuture<PdfTaskResult> future = CompletableFuture
        .supplyAsync(() -> {
            String targetPath = addWatermark(file);

            return PdfTaskResult.success(
                    file.getName(),
                    targetPath
            );
        }, pdfExecutor)
        .exceptionally(ex -> PdfTaskResult.fail(
                file.getName(),
                getErrorMessage(ex)
        ));

正常情况下返回成功结果。

发生异常时返回失败结果。

这样每个 PDF 最终都会得到一个 PdfTaskResult,不会因为某个文件失败,导致整个批量任务直接中断。


handle:成功和失败都会执行

handle()exceptionally() 最大的不同是:

exceptionally 只在失败时执行;

handle 无论成功还是失败都会执行。

基本写法如下:

CompletableFuture<String> future = CompletableFuture
        .supplyAsync(() -> processPdf(false))
        .handle((result, ex) -> {

            if (ex != null) {
                return "处理失败:" + getErrorMessage(ex);
            }

            return "处理成功:" + result;
        });

handle() 会接收两个参数:

result:前一步正常完成时的结果;

ex:前一步异常完成时的异常。

任务成功时:

result 有值;
ex 为 null。

任务失败时:

result 通常为 null;
ex 有值。

handle 完整示例

新建类:

com.succos.completablefuture.HandleDemo

代码如下:

package com.succos.completablefuture;

import java.util.concurrent.CompletableFuture;

public class HandleDemo {

    public static void main(String[] args) {

        CompletableFuture<String> future = CompletableFuture
                .supplyAsync(() -> processPdf(false))
                .handle((result, ex) -> {

                    if (ex != null) {
                        System.out.println("任务执行失败");

                        return "失败结果:"
                                + getErrorMessage(ex);
                    }

                    System.out.println("任务执行成功");

                    return "成功结果:" + result;
                });

        String finalResult = future.join();

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

    private static String processPdf(boolean success) {

        sleep(2000);

        if (!success) {
            throw new RuntimeException("PDF 文件解析失败");
        }

        return "output/test-watermark.pdf";
    }

    private static String getErrorMessage(Throwable throwable) {

        Throwable cause = throwable;

        while (cause.getCause() != null) {
            cause = cause.getCause();
        }

        return cause.getMessage();
    }

    private static void sleep(long millis) {

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

如果任务成功,最终返回:

成功结果:output/test-watermark.pdf

如果任务失败,最终返回:

失败结果:PDF 文件解析失败

所以 handle() 不只是异常兜底。

它更像是对前一步的执行结果做一次统一转换。


handle 可以改变结果类型

假设前面的异步任务返回的是文件路径:

CompletableFuture<String>

经过 handle() 后,可以转换成:

CompletableFuture<PdfTaskResult>

例如:

CompletableFuture<PdfTaskResult> future = CompletableFuture
        .supplyAsync(() -> addWatermark(file), pdfExecutor)
        .handle((targetPath, ex) -> {

            if (ex != null) {
                return PdfTaskResult.fail(
                        file.getName(),
                        getErrorMessage(ex)
                );
            }

            return PdfTaskResult.success(
                    file.getName(),
                    targetPath
            );
        });

这里前面的结果类型是 String

经过 handle() 后,结果类型变成了 PdfTaskResult

这非常适合批量任务。

因为无论 PDF 处理成功还是失败,最后都能得到同一种结果对象。


exceptionally 和 handle 怎么选

如果只是需要在失败时补一个兜底结果,我更倾向于用:

exceptionally(...)

例如:

.exceptionally(ex -> PdfTaskResult.fail(
        file.getName(),
        getErrorMessage(ex)
))

这段代码读起来很直接:

前面正常执行;

如果发生异常,就返回失败结果。

如果希望在同一个位置统一处理成功和失败,我会用:

handle(...)

例如:

.handle((targetPath, ex) -> {

    if (ex != null) {
        return PdfTaskResult.fail(
                file.getName(),
                getErrorMessage(ex)
        );
    }

    return PdfTaskResult.success(
            file.getName(),
            targetPath
    );
})

两种写法都可以。

区别主要在于代码组织方式。


whenComplete:观察结果,但不改变结果

whenComplete() 也能拿到正常结果和异常:

whenComplete((result, ex) -> {
    // 观察结果
})

它和 handle() 看起来很像。

但两者最大的区别是:

handle 可以返回新的结果;

whenComplete 通常不改变原来的结果。

whenComplete() 更适合做这些事情:

记录成功日志;
记录失败日志;
统计任务耗时;
更新监控指标;
清理临时资源;
观察任务最终状态。

whenComplete 成功示例

新建类:

com.succos.completablefuture.WhenCompleteDemo

代码如下:

package com.succos.completablefuture;

import java.util.concurrent.CompletableFuture;

public class WhenCompleteDemo {

    public static void main(String[] args) {

        CompletableFuture<String> future = CompletableFuture
                .supplyAsync(() -> processPdf(true))
                .whenComplete((result, ex) -> {

                    System.out.println("任务已经结束");

                    if (ex != null) {
                        System.out.println("处理失败:"
                                + getErrorMessage(ex));
                    } else {
                        System.out.println("处理成功:" + result);
                    }
                });

        String result = future.join();

        System.out.println("join 拿到的结果:" + result);
    }

    private static String processPdf(boolean success) {

        sleep(2000);

        if (!success) {
            throw new RuntimeException("PDF 水印添加失败");
        }

        return "output/test-watermark.pdf";
    }

    private static String getErrorMessage(Throwable throwable) {

        Throwable cause = throwable;

        while (cause.getCause() != null) {
            cause = cause.getCause();
        }

        return cause.getMessage();
    }

    private static void sleep(long millis) {

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

任务成功时,whenComplete() 可以读取输出路径并记录日志。

但最后:

future.join();

拿到的仍然是原来的结果:

output/test-watermark.pdf

whenComplete 不会自动恢复异常

把任务改成失败:

processPdf(false)

whenComplete() 可以看到异常:

.whenComplete((result, ex) -> {

    if (ex != null) {
        System.out.println("处理失败:"
                + getErrorMessage(ex));
    }
})

但它只是看到了异常,并没有处理掉异常。

后面继续调用:

future.join();

仍然会抛出 CompletionException

所以要记住:

whenComplete 能观察异常;

但默认不会把异常转换成正常结果。

whenComplete 不能通过修改参数改变结果

下面这种写法没有作用:

CompletableFuture<String> future = CompletableFuture
        .supplyAsync(() -> processPdf(false))
        .whenComplete((result, ex) -> {

            if (ex != null) {
                result = "output/default.pdf";
            }
        });

虽然在 Lambda 里给 result 重新赋值了,但不会改变 CompletableFuture 的最终结果。

因为 whenComplete() 没有返回新的业务结果。

如果希望失败后返回默认路径,应该使用:

.exceptionally(ex -> "output/default.pdf")

或者:

.handle((result, ex) -> {

    if (ex != null) {
        return "output/default.pdf";
    }

    return result;
})

用 whenComplete 记录 PDF 处理耗时

whenComplete() 很适合记录任务执行情况。

例如:

long start = System.currentTimeMillis();

CompletableFuture<String> future = CompletableFuture
        .supplyAsync(() -> addWatermark(file), pdfExecutor)
        .whenComplete((targetPath, ex) -> {

            long cost = System.currentTimeMillis() - start;

            if (ex != null) {
                System.out.println(file.getName()
                        + " 处理失败,耗时:"
                        + cost
                        + " ms,原因:"
                        + getErrorMessage(ex));
            } else {
                System.out.println(file.getName()
                        + " 处理成功,耗时:"
                        + cost
                        + " ms,输出路径:"
                        + targetPath);
            }
        });

这里 whenComplete() 只负责记录:

任务是否成功;
任务耗时;
输出路径;
异常原因。

不会改变任务本来的返回结果。


三个方法放在一起看

方法 成功时执行 失败时执行 能否返回新结果 常见用途
exceptionally 异常兜底
handle 统一转换成功和失败结果
whenComplete 日志、监控和收尾

可以直接这样记:

只处理失败:exceptionally;

成功失败都重新整理:handle;

只观察执行情况:whenComplete。

用同步代码类比

exceptionally() 类似:

try {
    return processPdf();
} catch (Exception e) {
    return defaultResult;
}

handle() 类似:

try {
    String result = processPdf();

    return convertSuccess(result);
} catch (Exception e) {
    return convertFail(e);
}

whenComplete() 类似:

try {
    String result = processPdf();

    logSuccess(result);

    return result;
} catch (Exception e) {
    logFail(e);

    throw e;
}

这样理解会更直观。


whenComplete 和 exceptionally 配合使用

如果既想记录原始异常,又想返回失败结果,可以这样写:

CompletableFuture<PdfTaskResult> future = CompletableFuture
        .supplyAsync(() -> {
            String targetPath = addWatermark(file);

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

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

执行顺序是:

异步处理 PDF;

whenComplete 记录成功或失败日志;

如果发生异常,exceptionally 再把异常转换成失败结果对象。

最终调用:

PdfTaskResult result = future.join();

仍然能拿到一个结果对象。


方法顺序会影响异常处理结果

例如:

future
        .exceptionally(ex -> "默认结果")
        .whenComplete((result, ex) -> {
            System.out.println("result:" + result);
            System.out.println("ex:" + ex);
        });

因为异常已经被 exceptionally() 转换成了正常结果,后面的 whenComplete() 可能看到:

result:默认结果
ex:null

如果顺序改成:

future
        .whenComplete((result, ex) -> {
            System.out.println("原始异常:" + ex);
        })
        .exceptionally(ex -> "默认结果");

那么前面的 whenComplete() 可以看到原始异常,后面的 exceptionally() 再负责兜底。

所以异常处理链的顺序不能随便放。

如果既要记录原始异常,又要返回兜底结果,我一般会写成:

.whenComplete(...)
.exceptionally(...)

先观察,再恢复。


批量 PDF 处理中的完整示例

新建类:

com.succos.completablefuture.PdfExceptionHandleDemo

代码如下:

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 PdfExceptionHandleDemo {

    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()
        );

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

        long start = System.currentTimeMillis();

        CompletableFuture<PdfTaskResult> future = CompletableFuture
                .supplyAsync(() -> processPdf(file), pdfExecutor)
                .whenComplete((targetPath, ex) -> {

                    long cost = System.currentTimeMillis() - start;

                    if (ex != null) {
                        System.out.println(file.getName()
                                + " 处理失败,耗时:"
                                + cost
                                + " ms,原因:"
                                + getErrorMessage(ex));
                    } else {
                        System.out.println(file.getName()
                                + " 处理成功,耗时:"
                                + cost
                                + " ms");
                    }
                })
                .handle((targetPath, ex) -> {

                    if (ex != null) {
                        return PdfTaskResult.fail(
                                file.getName(),
                                getErrorMessage(ex)
                        );
                    }

                    return PdfTaskResult.success(
                            file.getName(),
                            targetPath
                    );
                });

        PdfTaskResult result = future.join();

        System.out.println("--------------------------------");
        System.out.println("文件名:" + result.getFileName());
        System.out.println("是否成功:" + result.isSuccess());
        System.out.println("输出路径:" + result.getTargetPath());
        System.out.println("处理信息:" + result.getMessage());

        pdfExecutor.shutdown();
    }

    private static String processPdf(File file) {

        System.out.println(Thread.currentThread().getName()
                + " 开始处理:"
                + file.getName());

        sleep(2000);

        if (!file.exists()) {
            throw new RuntimeException("源 PDF 文件不存在");
        }

        return "output/"
                + file.getName().replace(".pdf", "-watermark.pdf");
    }

    private static String getErrorMessage(Throwable throwable) {

        Throwable cause = throwable;

        while (cause.getCause() != null) {
            cause = cause.getCause();
        }

        return cause.getMessage();
    }

    private static void sleep(long millis) {

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

    static class PdfTaskResult {

        private boolean success;

        private String fileName;

        private String targetPath;

        private String message;

        public static PdfTaskResult success(
                String fileName,
                String targetPath
        ) {
            PdfTaskResult result = new PdfTaskResult();

            result.success = true;
            result.fileName = fileName;
            result.targetPath = targetPath;
            result.message = "处理成功";

            return result;
        }

        public static PdfTaskResult fail(
                String fileName,
                String message
        ) {
            PdfTaskResult result = new PdfTaskResult();

            result.success = false;
            result.fileName = fileName;
            result.message = message;

            return result;
        }

        public boolean isSuccess() {
            return success;
        }

        public String getFileName() {
            return fileName;
        }

        public String getTargetPath() {
            return targetPath;
        }

        public String getMessage() {
            return message;
        }
    }

    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;
        }
    }
}

这个例子里:

whenComplete 负责记录执行日志和耗时;

handle 负责把成功和失败统一转换成 PdfTaskResult;

join 最后一定能拿到一个 PdfTaskResult。

这比较适合批量 PDF 处理。


这一节小结

这一节我主要记住几点:

1. exceptionally 只在异常时执行,适合返回兜底结果;
2. handle 无论成功还是失败都会执行,适合统一转换结果;
3. whenComplete 无论成功还是失败都会执行,但通常不改变原结果;
4. whenComplete 适合记录日志、耗时、监控和收尾;
5. whenComplete 看到了异常,不代表异常已经被处理;
6. 批量 PDF 可以用 exceptionally 或 handle,把异常转换成失败结果对象;
7. 方法调用顺序会影响后面的步骤能否看到原始异常;
8. 日志应该保留完整异常,返回给用户的信息可以适当简化。

用一句话总结:

失败时兜底用 exceptionally,成功失败都要转换用 handle,只想观察执行情况用 whenComplete。

下一节继续看 thenCombine

前面处理的都是一条异步任务链。下一步开始处理两个独立任务之间的关系:当两个异步任务都完成以后,怎么把它们的结果合并起来。