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

Java CompletableFuture exceptionally异常处理深度剖析

2023-02-054.3k 阅读

Java CompletableFuture exceptionally 异常处理基础

在 Java 中,CompletableFuture 为异步编程提供了强大的支持。exceptionally 方法是 CompletableFuture 处理异常的重要手段之一。

exceptionally 方法允许我们在 CompletableFuture 执行过程中发生异常时,返回一个默认值或者执行一段恢复逻辑。它的基本签名如下:

public CompletableFuture<T> exceptionally(Function<Throwable,? extends T> fn)

其中,fn 是一个 Function,它接收 Throwable 作为参数,并返回一个与 CompletableFuture 期望类型相同或者兼容的结果。

来看一个简单的示例:

import java.util.concurrent.CompletableFuture;

public class CompletableFutureExceptionallyExample {
    public static void main(String[] args) {
        CompletableFuture.supplyAsync(() -> {
            if (Math.random() < 0.5) {
                throw new RuntimeException("模拟异常");
            }
            return "正常结果";
        })
       .exceptionally(ex -> {
            System.out.println("捕获到异常: " + ex.getMessage());
            return "异常时的默认值";
        })
       .thenAccept(System.out::println);
    }
}

在上述代码中,supplyAsync 异步执行一个任务,该任务有 50% 的概率抛出异常。如果发生异常,exceptionally 方法中的逻辑会被执行,打印异常信息并返回默认值。

异常传播机制

理解 CompletableFuture 中异常的传播机制对于深入掌握 exceptionally 至关重要。当 CompletableFuture 执行链中的某个阶段抛出异常时,异常会沿着执行链向后传播,直到遇到 exceptionally 或者 whenComplete 等能够处理异常的方法。

例如:

import java.util.concurrent.CompletableFuture;

public class ExceptionPropagationExample {
    public static void main(String[] args) {
        CompletableFuture.supplyAsync(() -> {
            throw new RuntimeException("初始异常");
        })
       .thenApply(result -> {
            // 此方法不会被执行,因为前面已经抛出异常
            return result + " 处理后";
        })
       .exceptionally(ex -> {
            System.out.println("捕获到初始异常: " + ex.getMessage());
            return "异常处理结果";
        })
       .thenApply(finalResult -> {
            return finalResult + " 再次处理";
        })
       .thenAccept(System.out::println);
    }
}

在这个例子中,supplyAsync 抛出的异常会跳过 thenApply(因为异常发生时,后续的 thenApply 不会被执行),直到 exceptionally 处被捕获处理。然后,exceptionally 返回的结果会继续进入后续的 thenApply 进行处理。

多层嵌套的 CompletableFuture 异常处理

在实际应用中,我们经常会遇到多层嵌套的 CompletableFuture 场景。这种情况下,异常处理变得更加复杂,但 exceptionally 依然能够有效地发挥作用。

import java.util.concurrent.CompletableFuture;

public class NestedCompletableFutureExceptionExample {
    public static void main(String[] args) {
        CompletableFuture.supplyAsync(() -> {
            return CompletableFuture.supplyAsync(() -> {
                if (Math.random() < 0.5) {
                    throw new RuntimeException("内层异常");
                }
                return "内层正常结果";
            });
        })
       .thenCompose(innerFuture -> innerFuture)
       .exceptionally(ex -> {
            System.out.println("捕获到内层异常: " + ex.getMessage());
            return "内层异常默认值";
        })
       .thenAccept(System.out::println);
    }
}

在上述代码中,外层的 CompletableFuture 返回一个内层的 CompletableFuture。通过 thenCompose 方法将内层的 CompletableFuture 展开。如果内层 CompletableFuture 抛出异常,exceptionally 方法能够捕获并处理该异常。

exceptionally 与 whenComplete 的区别

whenComplete 也是 CompletableFuture 中用于处理结果(包括正常结果和异常情况)的方法,但它与 exceptionally 有明显的区别。

whenComplete 的签名为:

public CompletableFuture<T> whenComplete(BiConsumer<? super T,? super Throwable> action)

它接收一个 BiConsumer,该 BiConsumer 既可以处理正常的结果,也可以处理异常。然而,whenComplete 不会阻止异常的传播,它只是在 CompletableFuture 完成(正常完成或者异常完成)时执行传入的 BiConsumer

对比来看:

import java.util.concurrent.CompletableFuture;

public class ExceptionallyVsWhenCompleteExample {
    public static void main(String[] args) {
        CompletableFuture.supplyAsync(() -> {
            throw new RuntimeException("异常");
        })
       .whenComplete((result, ex) -> {
            if (ex != null) {
                System.out.println("whenComplete 捕获到异常: " + ex.getMessage());
            } else {
                System.out.println("正常结果: " + result);
            }
        })
       .thenApply(result -> {
            // 这里依然会抛出异常,因为 whenComplete 没有处理异常传播
            return result + " 处理后";
        })
       .exceptionally(ex -> {
            System.out.println("exceptionally 捕获到异常: " + ex.getMessage());
            return "异常处理结果";
        })
       .thenAccept(System.out::println);
    }
}

在这个例子中,whenComplete 打印了异常信息,但异常依然传播到了后续的 thenApply,导致 thenApply 抛出异常,最终由 exceptionally 捕获处理。

异常处理中的线程模型

CompletableFuture 异常处理过程中,线程模型也是需要关注的要点。exceptionally 方法的执行线程取决于 CompletableFuture 执行链的上下文。

通常情况下,如果 CompletableFuture 是通过 supplyAsync 或者 runAsync 等异步方法创建的,那么异常处理逻辑(exceptionally 中的代码)会在 ForkJoinPool.commonPool() 线程池中执行。但如果 CompletableFuture 是在特定线程上下文中创建并执行的,exceptionally 也会在相应的线程上下文中执行。

例如:

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

public class ExceptionHandlingThreadModelExample {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService executor = Executors.newSingleThreadExecutor();
        CompletableFuture.supplyAsync(() -> {
            throw new RuntimeException("异常");
        }, executor)
       .exceptionally(ex -> {
            System.out.println("当前线程: " + Thread.currentThread().getName());
            return "异常处理结果";
        })
       .get();
        executor.shutdown();
    }
}

在上述代码中,supplyAsync 使用了自定义的单线程执行器 executorexceptionally 中的代码会在这个单线程执行器的线程中执行,通过打印当前线程名称可以验证这一点。

复杂业务场景下的异常处理策略

在实际的复杂业务场景中,我们需要根据业务需求制定合理的异常处理策略。

比如,在一个涉及多个服务调用的微服务架构中,可能需要根据不同类型的异常进行不同的处理。对于网络异常,可能需要进行重试;对于业务逻辑异常,可能需要返回特定的错误信息给客户端。

import java.util.concurrent.CompletableFuture;

public class ComplexBusinessExceptionHandlingExample {
    public static void main(String[] args) {
        CompletableFuture.supplyAsync(() -> {
            if (Math.random() < 0.3) {
                throw new RuntimeException("网络异常");
            } else if (Math.random() < 0.6) {
                throw new IllegalArgumentException("业务逻辑异常");
            }
            return "正常结果";
        })
       .exceptionally(ex -> {
            if (ex instanceof RuntimeException) {
                // 模拟网络异常重试
                System.out.println("捕获到网络异常,进行重试");
                return retryNetworkOperation();
            } else if (ex instanceof IllegalArgumentException) {
                // 处理业务逻辑异常
                System.out.println("捕获到业务逻辑异常,返回特定错误信息");
                return "业务逻辑错误,请检查输入";
            }
            return "其他异常默认处理";
        })
       .thenAccept(System.out::println);
    }

    private static String retryNetworkOperation() {
        // 简单模拟重试逻辑
        for (int i = 0; i < 3; i++) {
            try {
                if (Math.random() > 0.5) {
                    return "重试成功";
                }
            } catch (Exception e) {
                // 捕获重试过程中的异常
            }
        }
        return "重试失败";
    }
}

在这个例子中,根据不同类型的异常,exceptionally 执行了不同的处理逻辑。对于网络异常进行了重试,对于业务逻辑异常返回了特定的错误信息。

与其他异常处理机制的结合使用

CompletableFutureexceptionally 可以与 Java 其他的异常处理机制相结合,以实现更强大和灵活的异常处理。

比如,与 try - catch 块结合:

import java.util.concurrent.CompletableFuture;

public class CombineWithTryCatchExample {
    public static void main(String[] args) {
        try {
            CompletableFuture.supplyAsync(() -> {
                throw new RuntimeException("异常");
            })
           .exceptionally(ex -> {
                System.out.println("exceptionally 捕获到异常: " + ex.getMessage());
                return "异常处理结果";
            })
           .get();
        } catch (Exception e) {
            // 这里可以捕获 get 方法抛出的异常,例如 InterruptedException
            System.out.println("try - catch 捕获到异常: " + e.getMessage());
        }
    }
}

在这个例子中,exceptionally 处理了 CompletableFuture 执行过程中的异常,而外部的 try - catch 块捕获了 get 方法可能抛出的其他异常,如 InterruptedException

还可以与自定义的异常处理框架相结合。例如,使用 Spring Boot 中的全局异常处理机制与 CompletableFutureexceptionally 结合。在 Spring Boot 应用中,可以定义一个全局异常处理器来处理所有未捕获的异常,同时在 CompletableFuture 中使用 exceptionally 进行局部的异常处理。

性能考量与优化

在使用 CompletableFutureexceptionally 进行异常处理时,性能也是一个需要考虑的因素。过多的异常处理逻辑或者复杂的恢复操作可能会影响程序的性能。

为了优化性能,首先要尽量减少异常的发生。在业务逻辑中进行充分的输入校验和合理性检查,避免不必要的异常。对于可能频繁发生的异常,可以考虑采用更高效的处理方式,如缓存异常处理结果,避免重复计算。

另外,合理利用线程池也能够提升性能。如果 CompletableFuture 执行链中的任务较为复杂且耗时,使用自定义的线程池并根据任务特点进行参数调优,可以避免线程资源的过度竞争和浪费。

例如,在高并发场景下,可以根据系统的 CPU 核心数和内存情况,调整 ForkJoinPool 的并行度,以充分利用系统资源,提升 CompletableFuture 包括异常处理在内的整体执行效率。

异常处理的最佳实践

  1. 明确异常类型:在 exceptionally 中,尽量明确处理的异常类型,避免使用宽泛的 Throwable。这样可以更精准地处理不同类型的异常,并且代码逻辑更加清晰。
  2. 日志记录:在异常处理过程中,务必进行详细的日志记录。记录异常的堆栈信息、发生的时间、相关的业务参数等,以便于调试和问题排查。
  3. 避免过度处理:不要在 exceptionally 中进行过于复杂和耗时的操作。如果需要复杂的恢复逻辑,可以将其封装成独立的方法,并在必要时进行异步调用。
  4. 测试异常处理:编写全面的单元测试来验证异常处理逻辑的正确性。测试不同类型的异常以及异常处理后的结果是否符合预期。

总结与展望

CompletableFutureexceptionally 方法为 Java 异步编程中的异常处理提供了强大而灵活的机制。通过深入理解其原理、异常传播机制、线程模型以及与其他异常处理机制的结合使用,开发者能够在复杂的业务场景中构建健壮、高效的异步应用程序。

随着 Java 技术的不断发展,异步编程的需求将越来越广泛,CompletableFuture 及其异常处理机制也将不断演进和完善。未来,我们可以期待更简洁、高效的异常处理方式,以及与新的 Java 特性更好的融合,为开发者提供更优质的编程体验。同时,在面对日益复杂的业务场景时,开发者需要不断探索和实践,以充分发挥 exceptionally 等异常处理机制的优势,打造稳定可靠的软件系统。