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