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

Java 中 CompletableFuture 任务异常回调 exceptionally 方法

2024-07-194.4k 阅读

CompletableFuture 简介

在Java的异步编程领域,CompletableFuture 是一个强大的工具。它引入于Java 8,为处理异步任务和非阻塞编程提供了一种优雅且高效的方式。CompletableFuture 类实现了 Future 接口和 CompletionStage 接口,这使得它既可以像传统的 Future 那样获取异步任务的结果,又能以链式调用的方式处理异步任务的各个阶段。

Future 接口的局限性

在深入了解 CompletableFuture 之前,先回顾一下Java早期的 Future 接口。Future 接口允许我们异步执行任务并获取任务的执行结果。例如,使用 ExecutorService 提交任务后会返回一个 Future 对象:

import java.util.concurrent.*;

public class FutureExample {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService executor = Executors.newSingleThreadExecutor();
        Future<Integer> future = executor.submit(() -> {
            // 模拟耗时操作
            Thread.sleep(2000);
            return 42;
        });

        try {
            System.out.println("等待任务完成...");
            Integer result = future.get();
            System.out.println("任务结果: " + result);
        } finally {
            executor.shutdown();
        }
    }
}

然而,Future 接口存在一些局限性。future.get() 方法是阻塞的,这意味着调用线程会一直等待,直到任务完成并返回结果。而且,Future 没有提供很好的方式来处理异步任务中的异常,也不支持任务完成后的链式调用。

CompletableFuture 的优势

CompletableFuture 弥补了 Future 接口的这些不足。它支持异步任务的链式调用,允许我们在任务完成或异常发生时执行特定的操作。通过 CompletableFuture,我们可以更优雅地处理异步任务的各个阶段,包括任务完成后的处理、异常处理以及任务组合等。例如,下面是一个简单的 CompletableFuture 示例:

import java.util.concurrent.CompletableFuture;

public class CompletableFutureExample {
    public static void main(String[] args) {
        CompletableFuture.supplyAsync(() -> {
            // 模拟耗时操作
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return 42;
        }).thenAccept(result -> System.out.println("任务结果: " + result));

        // 主线程不会阻塞
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在这个示例中,supplyAsync 方法异步执行一个任务,thenAccept 方法在任务完成后处理结果,并且主线程不会阻塞。这体现了 CompletableFuture 在异步编程中的灵活性和便捷性。

异常处理在 CompletableFuture 中的重要性

在异步编程中,异常处理是至关重要的。由于异步任务在独立的线程中执行,传统的异常处理机制(如 try - catch 块)不能直接应用于异步任务的执行过程。如果异步任务中抛出异常而没有适当的处理,这些异常可能会导致程序出现难以调试的问题,甚至可能导致程序崩溃。

未处理异常的问题

考虑以下简单的 CompletableFuture 示例,其中异步任务抛出异常:

import java.util.concurrent.CompletableFuture;

public class UnhandledExceptionExample {
    public static void main(String[] args) {
        CompletableFuture.supplyAsync(() -> {
            throw new RuntimeException("模拟异常");
        }).thenAccept(result -> System.out.println("任务结果: " + result));

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在这个示例中,supplyAsync 中抛出的 RuntimeException 没有被捕获。虽然程序不会立即崩溃,但异常信息会被打印到标准错误输出,这对于生产环境中的应用程序来说是不可接受的。而且,由于异常没有被正确处理,后续依赖于这个异步任务结果的操作也无法进行,可能会导致程序逻辑出现混乱。

异常处理的目标

CompletableFuture 中进行异常处理的主要目标是确保程序的健壮性和稳定性。我们希望能够捕获异步任务中抛出的异常,进行适当的处理(如记录日志、返回默认值或进行重试等),从而避免异常对整个程序流程的负面影响。CompletableFuture 提供了多种方法来实现这一目标,其中 exceptionally 方法是一种常用且便捷的异常处理方式。

exceptionally 方法详解

exceptionally 方法是 CompletableFuture 类提供的用于处理异步任务异常的方法之一。它允许我们在异步任务抛出异常时,指定一个备用的处理逻辑来返回一个默认值或执行其他恢复操作。

方法签名

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

这个方法接受一个 Function 类型的参数,该 FunctionThrowable 作为输入参数,并返回一个与 CompletableFuture 相同类型或其子类型的值。当异步任务正常完成时,exceptionally 方法不会执行。只有当异步任务抛出异常时,exceptionally 方法中指定的 Function 才会被调用,它会处理异常并返回一个替代值。

工作原理

CompletableFuture 所代表的异步任务正常完成时,其结果会按照正常的链式调用传递给后续的 then 系列方法(如 thenApplythenAccept 等)。然而,如果异步任务在执行过程中抛出异常,该异常会被捕获并传递给 exceptionally 方法中的 Function。这个 Function 可以根据异常类型进行不同的处理,比如记录异常日志、返回默认值等。处理完成后,exceptionally 方法会返回一个新的 CompletableFuture,其结果就是 Function 返回的值。

代码示例

基本异常处理示例

import java.util.concurrent.CompletableFuture;

public class ExceptionallyBasicExample {
    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(result -> System.out.println("最终结果: " + result));

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在这个示例中,supplyAsync 中的任务有50% 的概率抛出异常。如果抛出异常,exceptionally 方法中的 Function 会捕获异常,打印异常信息,并返回一个默认值。无论任务是否成功,thenAccept 方法都会处理最终的结果。

复杂异常处理示例

import java.util.concurrent.CompletableFuture;
import java.util.logging.Level;
import java.util.logging.Logger;

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

    public static void main(String[] args) {
        CompletableFuture.supplyAsync(() -> {
            // 模拟复杂业务逻辑,可能抛出不同类型异常
            if (Math.random() < 0.3) {
                throw new IllegalArgumentException("参数异常");
            } else if (Math.random() < 0.6) {
                throw new RuntimeException("运行时异常");
            }
            return "任务成功";
        }).exceptionally(ex -> {
            if (ex instanceof IllegalArgumentException) {
                LOGGER.log(Level.SEVERE, "参数异常", ex);
                return "参数异常处理结果";
            } else if (ex instanceof RuntimeException) {
                LOGGER.log(Level.SEVERE, "运行时异常", ex);
                return "运行时异常处理结果";
            } else {
                LOGGER.log(Level.SEVERE, "其他异常", ex);
                return "其他异常处理结果";
            }
        }).thenAccept(result -> System.out.println("最终结果: " + result));

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

此示例展示了更复杂的异常处理场景。根据异步任务抛出的不同类型异常,exceptionally 方法中的 Function 进行不同的处理,包括记录不同级别的日志并返回不同的处理结果。

结合其他 CompletableFuture 方法示例

import java.util.concurrent.CompletableFuture;

public class ExceptionallyChainingExample {
    public static void main(String[] args) {
        CompletableFuture.supplyAsync(() -> {
            if (Math.random() < 0.5) {
                throw new RuntimeException("模拟异常");
            }
            return 42;
        }).exceptionally(ex -> {
            System.out.println("捕获到异常: " + ex.getMessage());
            return -1;
        }).thenApply(result -> result * 2).thenAccept(finalResult -> System.out.println("最终结果: " + finalResult));

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在这个示例中,exceptionally 方法与 thenApplythenAccept 方法结合使用。如果异步任务正常完成,thenApply 方法会对结果进行处理;如果任务抛出异常,exceptionally 方法返回默认值,thenApply 方法会对默认值进行处理,最终 thenAccept 方法处理最终结果。

exceptionally 方法与其他异常处理方法的比较

CompletableFuture 还提供了其他一些处理异常的方法,如 handle 方法和 whenComplete 方法。了解 exceptionally 方法与这些方法的区别和适用场景,可以帮助我们在不同的情况下选择最合适的异常处理方式。

与 handle 方法的比较

handle 方法签名为 CompletableFuture handle(BiFunction<? super T, Throwable,? extends U> fn)。它接受一个 BiFunction,该 BiFunction 有两个参数,第一个是任务正常完成时的结果(如果任务正常完成),第二个是任务抛出的异常(如果任务抛出异常)。handle 方法无论任务正常完成还是抛出异常都会执行。

例如:

import java.util.concurrent.CompletableFuture;

public class HandleVsExceptionallyExample {
    public static void main(String[] args) {
        CompletableFuture.supplyAsync(() -> {
            if (Math.random() < 0.5) {
                throw new RuntimeException("模拟异常");
            }
            return "任务成功";
        }).handle((result, ex) -> {
            if (ex != null) {
                System.out.println("捕获到异常: " + ex.getMessage());
                return "默认值";
            }
            return result;
        }).thenAccept(finalResult -> System.out.println("最终结果: " + finalResult));

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

exceptionally 方法相比,handle 方法更灵活,因为它可以同时处理正常结果和异常情况。exceptionally 方法则专注于异常处理,只有在任务抛出异常时才会执行。如果我们只关心异常处理,exceptionally 方法更简洁;如果需要在任务正常完成和异常时都进行不同的处理,handle 方法更合适。

与 whenComplete 方法的比较

whenComplete 方法签名为 CompletableFuture whenComplete(BiConsumer<? super T,? super Throwable> action)。它接受一个 BiConsumer,同样会在任务完成(无论正常完成还是抛出异常)时执行。但是,whenComplete 方法不会返回一个新的 CompletableFuture 来处理结果或异常,它主要用于执行一些最终的操作,如记录日志等。

例如:

import java.util.concurrent.CompletableFuture;

public class WhenCompleteVsExceptionallyExample {
    public static void main(String[] args) {
        CompletableFuture.supplyAsync(() -> {
            if (Math.random() < 0.5) {
                throw new RuntimeException("模拟异常");
            }
            return "任务成功";
        }).whenComplete((result, ex) -> {
            if (ex != null) {
                System.out.println("捕获到异常: " + ex.getMessage());
            } else {
                System.out.println("任务正常完成: " + result);
            }
        }).thenAccept(result -> System.out.println("最终结果: " + result));

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在这个示例中,whenComplete 方法用于打印任务的完成状态(正常或异常),但它不会改变任务的结果。如果需要在异常时返回一个替代值或对异常进行处理并返回新的结果,exceptionally 方法更合适。

实际应用场景

网络请求重试

在进行网络请求时,由于网络不稳定等原因,请求可能会失败。我们可以使用 CompletableFutureexceptionally 方法来实现简单的重试机制。

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

public class NetworkRequestRetryExample {
    public static CompletableFuture<String> makeNetworkRequest(int retryCount) {
        return CompletableFuture.supplyAsync(() -> {
            if (Math.random() < 0.5) {
                throw new RuntimeException("网络请求失败");
            }
            return "网络请求成功";
        }).exceptionally(ex -> {
            if (retryCount > 0) {
                System.out.println("第 " + (4 - retryCount) + " 次重试...");
                return makeNetworkRequest(retryCount - 1).join();
            } else {
                System.out.println("重试次数用尽,请求失败");
                return "请求失败";
            }
        });
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<String> future = makeNetworkRequest(3);
        System.out.println(future.get());
    }
}

在这个示例中,makeNetworkRequest 方法模拟网络请求,有50% 的概率失败。如果请求失败且重试次数未用尽,exceptionally 方法会递归调用 makeNetworkRequest 进行重试。

数据库操作异常处理

在进行数据库操作时,如插入、更新或查询数据,可能会因为各种原因(如数据库连接问题、数据约束违反等)抛出异常。使用 CompletableFutureexceptionally 方法可以有效地处理这些异常。

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.concurrent.CompletableFuture;

public class DatabaseOperationExample {
    private static final String URL = "jdbc:mysql://localhost:3306/mydb";
    private static final String USER = "root";
    private static final String PASSWORD = "password";

    public static CompletableFuture<String> insertData(String data) {
        return CompletableFuture.supplyAsync(() -> {
            try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
                 PreparedStatement pstmt = conn.prepareStatement("INSERT INTO mytable (data) VALUES (?)")) {
                pstmt.setString(1, data);
                pstmt.executeUpdate();
                return "数据插入成功";
            } catch (SQLException ex) {
                throw new RuntimeException("数据库操作异常", ex);
            }
        }).exceptionally(ex -> {
            System.out.println("捕获到数据库操作异常: " + ex.getMessage());
            return "数据插入失败";
        });
    }

    public static void main(String[] args) {
        insertData("示例数据").thenAccept(result -> System.out.println(result));
    }
}

在这个示例中,insertData 方法使用 CompletableFuture 异步执行数据库插入操作。如果操作过程中抛出 SQLExceptionexceptionally 方法会捕获异常并返回错误信息。

服务调用链异常处理

在微服务架构中,一个业务操作可能涉及多个服务的调用,形成服务调用链。当某个服务调用失败时,需要对整个调用链进行适当的异常处理。CompletableFutureexceptionally 方法可以帮助我们优雅地处理这种情况。

import java.util.concurrent.CompletableFuture;

public class ServiceChainExample {
    public static CompletableFuture<String> service1() {
        return CompletableFuture.supplyAsync(() -> {
            if (Math.random() < 0.3) {
                throw new RuntimeException("服务1调用失败");
            }
            return "服务1执行成功";
        });
    }

    public static CompletableFuture<String> service2(String result1) {
        return CompletableFuture.supplyAsync(() -> {
            if (Math.random() < 0.4) {
                throw new RuntimeException("服务2调用失败");
            }
            return result1 + " -> 服务2执行成功";
        });
    }

    public static CompletableFuture<String> service3(String result2) {
        return CompletableFuture.supplyAsync(() -> {
            if (Math.random() < 0.5) {
                throw new RuntimeException("服务3调用失败");
            }
            return result2 + " -> 服务3执行成功";
        });
    }

    public static void main(String[] args) {
        service1()
           .thenCompose(result1 -> service2(result1))
           .thenCompose(result2 -> service3(result2))
           .exceptionally(ex -> {
                System.out.println("服务调用链中捕获到异常: " + ex.getMessage());
                return "服务调用链失败";
            })
           .thenAccept(finalResult -> System.out.println(finalResult));

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在这个示例中,service1service2service3 模拟服务调用,每个服务都有一定概率调用失败。thenCompose 方法用于将多个服务调用连接成一个调用链,exceptionally 方法在整个调用链中任何一个服务抛出异常时捕获并处理异常。

注意事项

异常处理的顺序

在使用 CompletableFuture 的链式调用时,要注意 exceptionally 方法的位置。如果 exceptionally 方法在链式调用的早期位置,它只能捕获在它之前的异步任务抛出的异常。如果后续的 then 系列方法中抛出异常,exceptionally 方法将无法捕获。例如:

import java.util.concurrent.CompletableFuture;

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

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在这个示例中,exceptionally 方法无法捕获 thenApply 中抛出的 RuntimeException,因为 exceptionally 方法在 thenApply 之前。如果希望捕获整个链式调用中的所有异常,可以将 exceptionally 方法放在链式调用的最后。

避免阻塞

虽然 CompletableFuture 旨在实现异步非阻塞编程,但在异常处理过程中,如果不小心使用了阻塞操作(如 future.get()),可能会导致程序性能下降甚至死锁。例如:

import java.util.concurrent.CompletableFuture;

public class AvoidBlockingExample {
    public static void main(String[] args) {
        CompletableFuture.supplyAsync(() -> {
            if (Math.random() < 0.5) {
                throw new RuntimeException("模拟异常");
            }
            return "任务成功";
        }).exceptionally(ex -> {
            try {
                // 这里使用了阻塞操作,不推荐
                CompletableFuture<String> otherFuture = CompletableFuture.supplyAsync(() -> "另一个任务结果");
                return otherFuture.get();
            } catch (Exception e) {
                e.printStackTrace();
                return "默认值";
            }
        }).thenAccept(finalResult -> System.out.println(finalResult));

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

exceptionally 方法中,otherFuture.get() 是一个阻塞操作,这可能会导致当前线程阻塞,影响程序的异步特性。应尽量使用异步的方式处理异常,如使用 thenApplyAsync 等方法。

资源管理

在异步任务中,特别是在处理数据库连接、文件句柄等资源时,要注意资源的正确管理。即使异步任务抛出异常,也需要确保资源被正确关闭。例如,在数据库操作示例中,使用了 try - with - resources 语句来确保数据库连接在操作完成后自动关闭,无论是否抛出异常。在异常处理过程中,也要避免因为异常而导致资源泄漏。

总结

CompletableFutureexceptionally 方法为Java异步编程中的异常处理提供了一种简洁而强大的方式。通过合理使用 exceptionally 方法,我们可以有效地捕获和处理异步任务中抛出的异常,提高程序的健壮性和稳定性。在实际应用中,需要根据具体的业务场景选择合适的异常处理方式,并注意异常处理的顺序、避免阻塞以及正确管理资源等问题。与其他异常处理方法(如 handlewhenComplete)相结合,可以进一步提升异步编程的灵活性和效率。无论是网络请求、数据库操作还是复杂的服务调用链,exceptionally 方法都能在异常处理方面发挥重要作用,帮助我们构建可靠的异步应用程序。