MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Java CompletableFuture异常处理的最佳实践案例

2021-10-122.4k 阅读

Java CompletableFuture 异常处理概述

在 Java 异步编程中,CompletableFuture 是一个强大的工具,它允许我们以异步方式执行任务,并在任务完成时进行各种处理。然而,异步任务执行过程中难免会遇到异常情况,如何优雅且有效地处理这些异常成为了关键。

CompletableFuture 提供了多种异常处理机制,其核心思想围绕着如何捕获、处理以及传播异步任务中产生的异常。与传统的同步编程异常处理不同,异步编程的异常处理需要考虑到任务的异步特性,避免阻塞主线程,同时确保异常能够得到妥善处理,不影响程序的整体运行。

异常处理的基本方法

  1. 使用 exceptionally 方法 exceptionally 方法是 CompletableFuture 提供的最基本的异常处理方法之一。它允许我们在 CompletableFuture 完成时,如果发生异常,返回一个默认值或者执行一些补救操作。
import java.util.concurrent.CompletableFuture;

public class CompletableFutureExceptionHandling {
    public static void main(String[] args) {
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            if (Math.random() < 0.5) {
                throw new RuntimeException("模拟异常");
            }
            return "任务成功完成";
        });

        future.exceptionally(ex -> {
            System.out.println("捕获到异常: " + ex.getMessage());
            return "默认值";
        }).thenAccept(System.out::println);
    }
}

在上述代码中,supplyAsync 方法异步执行一个任务,该任务有 50% 的概率抛出异常。exceptionally 方法捕获到异常后,打印异常信息并返回一个默认值。最后,thenAccept 方法处理最终的结果,无论是正常结果还是异常处理后的默认值。

  1. 使用 whenComplete 方法 whenComplete 方法会在 CompletableFuture 完成(无论是正常完成还是异常完成)时执行。它接收两个参数,第一个参数是正常完成时的结果,第二个参数是异常(如果有)。
import java.util.concurrent.CompletableFuture;

public class CompletableFutureWhenCompleteExample {
    public static void main(String[] args) {
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            if (Math.random() < 0.5) {
                throw new RuntimeException("模拟异常");
            }
            return "任务成功完成";
        });

        future.whenComplete((result, ex) -> {
            if (ex != null) {
                System.out.println("捕获到异常: " + ex.getMessage());
            } else {
                System.out.println("任务正常完成: " + result);
            }
        });

        // 防止主线程退出
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在这段代码中,whenComplete 方法检查 ex 是否为 null,如果不为 null 则说明任务执行过程中发生了异常,打印异常信息;否则打印正常完成的结果。需要注意的是,由于 whenComplete 方法不会改变 CompletableFuture 的结果,所以异常不会被“消费”,如果后续还有其他依赖于这个 CompletableFuture 的操作,异常仍然会传播。

  1. 使用 handle 方法 handle 方法结合了 whenCompleteexceptionally 的功能。它会在 CompletableFuture 完成时执行,并且可以根据是否发生异常返回不同的结果,从而改变 CompletableFuture 的最终结果。
import java.util.concurrent.CompletableFuture;

public class CompletableFutureHandleExample {
    public static void main(String[] args) {
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            if (Math.random() < 0.5) {
                throw new RuntimeException("模拟异常");
            }
            return "任务成功完成";
        });

        CompletableFuture<String> handledFuture = future.handle((result, ex) -> {
            if (ex != null) {
                System.out.println("捕获到异常: " + ex.getMessage());
                return "默认值";
            }
            return result;
        });

        handledFuture.thenAccept(System.out::println);
    }
}

这里,handle 方法根据 ex 是否为 null 来决定返回正常结果还是默认值,从而改变了 CompletableFuture 的最终结果。后续的 thenAccept 方法处理的就是经过 handle 方法处理后的结果。

异常传播与链式调用

  1. 异常在链式调用中的传播CompletableFuture 在链式调用中发生异常时,异常会自动传播到后续依赖该 CompletableFuture 的操作中。
import java.util.concurrent.CompletableFuture;

public class CompletableFutureExceptionPropagation {
    public static void main(String[] args) {
        CompletableFuture.supplyAsync(() -> {
            if (Math.random() < 0.5) {
                throw new RuntimeException("模拟异常");
            }
            return "初始结果";
        })
       .thenApply(String::toUpperCase)
       .thenApply(result -> result + " 已转换")
       .exceptionally(ex -> {
            System.out.println("捕获到异常: " + ex.getMessage());
            return "默认值";
        })
       .thenAccept(System.out::println);
    }
}

在上述代码中,supplyAsync 方法可能会抛出异常。如果异常发生,它会传播到 thenApply 方法,由于没有在 thenApply 中处理异常,异常继续传播,直到被 exceptionally 方法捕获并处理。

  1. 自定义异常处理策略在链式调用中的应用 我们可以在链式调用的不同阶段应用不同的异常处理策略。例如,在某个中间步骤捕获异常并进行特定处理,然后继续链式调用。
import java.util.concurrent.CompletableFuture;

public class CompletableFutureCustomExceptionHandling {
    public static void main(String[] args) {
        CompletableFuture.supplyAsync(() -> {
            if (Math.random() < 0.5) {
                throw new RuntimeException("模拟异常");
            }
            return "初始结果";
        })
       .thenApply(String::toUpperCase)
       .handle((result, ex) -> {
            if (ex != null) {
                System.out.println("在 thenApply 后捕获到异常: " + ex.getMessage());
                return "临时默认值";
            }
            return result;
        })
       .thenApply(result -> result + " 已处理")
       .exceptionally(ex -> {
            System.out.println("最终捕获到异常: " + ex.getMessage());
            return "最终默认值";
        })
       .thenAccept(System.out::println);
    }
}

在这个例子中,handle 方法在 thenApply 之后捕获异常,并返回一个临时默认值。然后,后续的 thenApply 方法继续处理这个临时默认值,直到最终的 exceptionally 方法再次捕获可能发生的异常并返回最终默认值。

组合多个 CompletableFuture 时的异常处理

  1. allOf 方法的异常处理 allOf 方法用于等待所有给定的 CompletableFuture 都完成。如果其中任何一个 CompletableFuture 抛出异常,allOf 返回的 CompletableFuture 也会以该异常完成。
import java.util.concurrent.CompletableFuture;

public class CompletableFutureAllOfExceptionHandling {
    public static void main(String[] args) {
        CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
            if (Math.random() < 0.5) {
                throw new RuntimeException("未来 1 异常");
            }
            return "未来 1 结果";
        });

        CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
            if (Math.random() < 0.5) {
                throw new RuntimeException("未来 2 异常");
            }
            return "未来 2 结果";
        });

        CompletableFuture<Void> allFuture = CompletableFuture.allOf(future1, future2);

        allFuture.join(); // 等待所有任务完成

        try {
            future1.get();
            future2.get();
            System.out.println("所有任务成功完成");
        } catch (Exception e) {
            System.out.println("捕获到异常: " + e.getMessage());
        }
    }
}

在这段代码中,allOf 方法返回的 CompletableFuture<Void> 会在 future1future2 抛出异常时以异常完成。通过 join 方法等待所有任务完成后,再通过 get 方法获取每个 CompletableFuture 的结果,这样可以捕获到可能发生的异常。

  1. anyOf 方法的异常处理 anyOf 方法用于等待任何一个给定的 CompletableFuture 完成。如果所有的 CompletableFuture 都以异常完成,anyOf 返回的 CompletableFuture 也会以其中一个异常完成(通常是第一个抛出的异常)。
import java.util.concurrent.CompletableFuture;

public class CompletableFutureAnyOfExceptionHandling {
    public static void main(String[] args) {
        CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
            if (Math.random() < 0.5) {
                throw new RuntimeException("未来 1 异常");
            }
            return "未来 1 结果";
        });

        CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
            if (Math.random() < 0.5) {
                throw new RuntimeException("未来 2 异常");
            }
            return "未来 2 结果";
        });

        CompletableFuture<Object> anyFuture = CompletableFuture.anyOf(future1, future2);

        try {
            Object result = anyFuture.get();
            System.out.println("第一个完成的结果: " + result);
        } catch (Exception e) {
            System.out.println("捕获到异常: " + e.getMessage());
        }
    }
}

这里,anyOf 方法返回的 CompletableFuture<Object> 会在任何一个 CompletableFuture 完成时完成。通过 get 方法获取结果时,可以捕获到可能发生的异常。

高级异常处理技巧

  1. 异常转换 有时候,我们希望将异步任务中抛出的原始异常转换为另一种类型的异常,以便更好地在业务层面进行处理。
import java.util.concurrent.CompletableFuture;

public class CompletableFutureExceptionTransformation {
    public static void main(String[] args) {
        CompletableFuture.supplyAsync(() -> {
            if (Math.random() < 0.5) {
                throw new RuntimeException("原始异常");
            }
            return "任务成功完成";
        })
       .exceptionally(ex -> {
            throw new CustomBusinessException("转换后的业务异常", ex);
        })
       .thenAccept(result -> System.out.println("结果: " + result))
       .exceptionally(businessEx -> {
            System.out.println("捕获到业务异常: " + businessEx.getMessage());
            return null;
        });
    }

    static class CustomBusinessException extends RuntimeException {
        public CustomBusinessException(String message, Throwable cause) {
            super(message, cause);
        }
    }
}

在这个例子中,exceptionally 方法将原始的 RuntimeException 转换为 CustomBusinessException,后续的 exceptionally 方法专门处理这种业务异常。

  1. 异常日志记录 在处理异步任务异常时,记录详细的异常日志对于调试和问题排查非常重要。
import java.util.concurrent.CompletableFuture;
import java.util.logging.Level;
import java.util.logging.Logger;

public class CompletableFutureExceptionLogging {
    private static final Logger LOGGER = Logger.getLogger(CompletableFutureExceptionLogging.class.getName());

    public static void main(String[] args) {
        CompletableFuture.supplyAsync(() -> {
            if (Math.random() < 0.5) {
                throw new RuntimeException("模拟异常");
            }
            return "任务成功完成";
        })
       .exceptionally(ex -> {
            LOGGER.log(Level.SEVERE, "发生异常", ex);
            return "默认值";
        })
       .thenAccept(System.out::println);
    }
}

这里使用 Java 自带的日志框架 java.util.logging 记录异常信息,exceptionally 方法在捕获异常后记录日志并返回默认值。

  1. 异常重试 对于一些由于临时故障导致的异常,我们可能希望重试异步任务。
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

public class CompletableFutureExceptionRetry {
    public static void main(String[] args) {
        int maxRetries = 3;
        int retryDelay = 1000; // 1 秒

        CompletableFuture<String> future = retryAsync(() -> {
            if (Math.random() < 0.5) {
                throw new RuntimeException("模拟异常");
            }
            return "任务成功完成";
        }, maxRetries, retryDelay);

        try {
            String result = future.get(5, TimeUnit.SECONDS);
            System.out.println("最终结果: " + result);
        } catch (InterruptedException | ExecutionException | TimeoutException e) {
            System.out.println("最终捕获到异常: " + e.getMessage());
        }
    }

    private static CompletableFuture<String> retryAsync(CompletableFutureSupplier<String> supplier, int maxRetries, int retryDelay) {
        CompletableFuture<String> future = new CompletableFuture<>();
        retry(supplier, future, maxRetries, retryDelay, 0);
        return future;
    }

    private static void retry(CompletableFutureSupplier<String> supplier, CompletableFuture<String> future, int maxRetries, int retryDelay, int currentRetry) {
        supplier.get()
               .whenComplete((result, ex) -> {
                    if (ex == null) {
                        future.complete(result);
                    } else if (currentRetry < maxRetries) {
                        try {
                            Thread.sleep(retryDelay);
                        } catch (InterruptedException e) {
                            future.completeExceptionally(e);
                            return;
                        }
                        retry(supplier, future, maxRetries, retryDelay, currentRetry + 1);
                    } else {
                        future.completeExceptionally(ex);
                    }
                });
    }

    @FunctionalInterface
    interface CompletableFutureSupplier<T> {
        CompletableFuture<T> get();
    }
}

在上述代码中,retryAsync 方法实现了异步任务的重试逻辑。如果任务执行过程中抛出异常且重试次数未达到最大重试次数,则等待一段时间后再次尝试执行任务。

实战案例:电商订单处理系统中的异步操作异常处理

假设我们正在开发一个电商订单处理系统,其中涉及多个异步操作,如库存检查、支付处理、订单生成等。每个操作都可能出现异常,我们需要确保整个订单处理流程的可靠性和稳定性。

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

public class EcommerceOrderProcessing {
    public static void main(String[] args) {
        CompletableFuture<String> orderProcessingFuture = checkInventory()
               .thenCompose(inventoryStatus -> processPayment())
               .thenCompose(paymentStatus -> generateOrder());

        try {
            String finalStatus = orderProcessingFuture.get();
            System.out.println("订单处理成功: " + finalStatus);
        } catch (InterruptedException | ExecutionException e) {
            System.out.println("订单处理失败: " + e.getMessage());
        }
    }

    private static CompletableFuture<String> checkInventory() {
        return CompletableFuture.supplyAsync(() -> {
            if (Math.random() < 0.3) {
                throw new RuntimeException("库存不足");
            }
            return "库存充足";
        });
    }

    private static CompletableFuture<String> processPayment() {
        return CompletableFuture.supplyAsync(() -> {
            if (Math.random() < 0.2) {
                throw new RuntimeException("支付失败");
            }
            return "支付成功";
        });
    }

    private static CompletableFuture<String> generateOrder() {
        return CompletableFuture.supplyAsync(() -> {
            if (Math.random() < 0.1) {
                throw new RuntimeException("订单生成失败");
            }
            return "订单已生成";
        });
    }
}

在这个基础版本中,每个异步操作都可能抛出异常,但没有进行异常处理。接下来,我们添加异常处理逻辑。

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

public class EcommerceOrderProcessingWithExceptionHandling {
    public static void main(String[] args) {
        CompletableFuture<String> orderProcessingFuture = checkInventory()
               .handle((inventoryStatus, ex) -> {
                    if (ex != null) {
                        System.out.println("库存检查异常: " + ex.getMessage());
                        return "库存检查失败";
                    }
                    return inventoryStatus;
                })
               .thenCompose(inventoryStatus -> {
                    if ("库存检查失败".equals(inventoryStatus)) {
                        return CompletableFuture.completedFuture(inventoryStatus);
                    }
                    return processPayment();
                })
               .handle((paymentStatus, ex) -> {
                    if (ex != null) {
                        System.out.println("支付处理异常: " + ex.getMessage());
                        return "支付处理失败";
                    }
                    return paymentStatus;
                })
               .thenCompose(paymentStatus -> {
                    if ("支付处理失败".equals(paymentStatus)) {
                        return CompletableFuture.completedFuture(paymentStatus);
                    }
                    return generateOrder();
                })
               .handle((orderStatus, ex) -> {
                    if (ex != null) {
                        System.out.println("订单生成异常: " + ex.getMessage());
                        return "订单生成失败";
                    }
                    return orderStatus;
                });

        try {
            String finalStatus = orderProcessingFuture.get();
            System.out.println("订单最终状态: " + finalStatus);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }

    private static CompletableFuture<String> checkInventory() {
        return CompletableFuture.supplyAsync(() -> {
            if (Math.random() < 0.3) {
                throw new RuntimeException("库存不足");
            }
            return "库存充足";
        });
    }

    private static CompletableFuture<String> processPayment() {
        return CompletableFuture.supplyAsync(() -> {
            if (Math.random() < 0.2) {
                throw new RuntimeException("支付失败");
            }
            return "支付成功";
        });
    }

    private static CompletableFuture<String> generateOrder() {
        return CompletableFuture.supplyAsync(() -> {
            if (Math.random() < 0.1) {
                throw new RuntimeException("订单生成失败");
            }
            return "订单已生成";
        });
    }
}

在这个改进版本中,我们在每个异步操作后使用 handle 方法捕获异常,并进行相应的处理。如果某个操作失败,后续操作会根据失败状态进行处理,避免无效的操作执行,同时记录异常信息,提高系统的可维护性。

与传统异常处理的对比

  1. 同步与异步的差异 传统的同步编程中,异常会立即中断当前线程的执行,调用栈会展开,直到异常被捕获或导致程序终止。而在 CompletableFuture 的异步编程中,异常不会立即中断主线程,而是在 CompletableFuture 完成时进行处理。这使得异步任务可以在后台继续执行,不阻塞主线程的其他操作。
  2. 异常传播机制 传统同步编程中,异常通过调用栈向上传播,直到被捕获。在 CompletableFuture 中,异常通过 CompletableFuture 的链式调用传播,直到遇到合适的异常处理方法,如 exceptionallyhandle 等。这种传播机制更加灵活,允许我们在不同的阶段对异常进行处理。
  3. 处理复杂异步流程的优势 在处理复杂的异步流程时,CompletableFuture 的异常处理机制能够更好地管理多个异步任务之间的依赖关系和异常处理。例如,在组合多个 CompletableFuture 时,allOfanyOf 方法提供了针对多个异步任务的异常处理策略,而传统的同步编程在处理这种情况时会更加复杂,需要手动管理线程和异常处理逻辑。

常见问题与解决方法

  1. 异常丢失问题 有时候,在复杂的 CompletableFuture 链式调用中,异常可能会丢失,导致程序出现难以调试的问题。这通常是因为没有正确处理异常或者异常处理方法没有正确设置。 解决方法:确保在每个可能抛出异常的 CompletableFuture 操作后都设置合适的异常处理方法,如 exceptionallyhandle 等。同时,在组合多个 CompletableFuture 时,注意检查异常是否正确传播和处理。
  2. 异常处理不及时 如果在异步任务完成后没有及时处理异常,可能会导致异常长时间未被发现,影响系统的稳定性。 解决方法:在 CompletableFuture 完成后,尽快使用合适的异常处理方法进行处理。可以使用 whenCompletehandle 等方法在任务完成时立即检查异常并进行处理。
  3. 异常处理逻辑混乱 随着异步任务复杂度的增加,异常处理逻辑可能变得混乱,难以维护。 解决方法:采用模块化的方式处理异常,将不同类型的异常处理逻辑封装成独立的方法或类。同时,在代码中添加清晰的注释,说明每个异常处理步骤的目的和作用。

性能考虑

  1. 异常处理对性能的影响 虽然 CompletableFuture 的异常处理机制提供了强大的功能,但异常处理本身会带来一定的性能开销。例如,创建和处理异常对象、执行异常处理逻辑等都会消耗一定的系统资源。
  2. 优化建议 为了减少异常处理对性能的影响,尽量避免在异步任务中频繁抛出异常。可以在任务执行前进行充分的检查,避免可能导致异常的情况发生。同时,对于不可避免的异常,尽量简化异常处理逻辑,避免复杂的计算和操作。另外,合理使用缓存和重试机制,减少由于临时故障导致的异常重试次数,提高系统的整体性能。

总结最佳实践

  1. 选择合适的异常处理方法 根据具体的业务需求和异常处理逻辑,选择合适的异常处理方法,如 exceptionally 用于简单的异常捕获并返回默认值,handle 用于根据异常情况返回不同结果,whenComplete 用于在任务完成时无论是否异常都执行一些操作。
  2. 确保异常传播与处理的正确性 在链式调用和组合多个 CompletableFuture 时,确保异常能够正确传播和处理。合理使用异常处理方法,避免异常丢失或未被正确处理的情况。
  3. 记录详细的异常日志 在异常处理过程中,记录详细的异常日志,包括异常类型、异常信息、发生异常的任务等,以便于调试和问题排查。
  4. 考虑异常重试和业务补偿 对于由于临时故障或可恢复原因导致的异常,考虑实现异常重试机制。同时,根据业务需求,设计合适的业务补偿机制,确保在异常发生时系统的一致性和可靠性。
  5. 性能优化 尽量减少异常的发生,简化异常处理逻辑,以降低异常处理对系统性能的影响。

通过遵循这些最佳实践,可以在使用 CompletableFuture 进行异步编程时,有效地处理异常,提高程序的稳定性、可维护性和性能。在实际开发中,根据具体的业务场景和需求,灵活运用这些方法,打造健壮的异步应用程序。