6

Java 8 CompletableFuture 对比 ES6 Promise

 2 years ago
source link: https://segmentfault.com/a/1190000040588495
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Java 8 CompletableFuture 对比 ES6 Promise

JavaScript 语言的执行环境是单线程的,异步编程对于 JavaScript 来说必不可少。JavaScript 传统异步解决方案主要是通过回调函数,而回调函数最大的问题就是 Callback Hell。所以 ES6 标准提供的 Promise 对象,专门用于解决异步编程的问题。

而 Java 语言是一个支持多线程的语言,语法以同步为主,在实际开发中很少需要用到大量的异步编程。但是要想追求更高的性能,异步通常是更好的选择。例如 Servlet 3 的异步支持、Spring 5 提供的 Spring WebFlux 等,都是为了追求更高的性能。和 JavaScript 一样,传统的 Callback 方式处理 Java 异步也会有 Callback Hell 问题,所以在 Java 8 中新增了和 ES6 的 Promise 类似的对象: java.util.concurrent.CompletableFuture

创建 Promise

在 JavaScript 中创建 Promise 对象:

const promise = new Promise(function(resolve, reject) {
    if (success){
        // 成功
        resolve(value);  
    } else {
        // 失败
        reject(error);   
    }
});

例如,jQuery 传统的使用回调函数的 ajax 写法是这样的:

$.ajax({
    url: "/url",
    success: function(data) {
        // 成功
    },
    error: function(jqXHR, textStatus, error) {
        // 失败
    }
});

如果要把它封装一下,返回一个 Promise 对象,可以这样来写:

function promisifyAjax(url) {
    const promise = new Promise(function(resolve, reject) {
        $.ajax({
            url: url,
            success: function(data) {
                // 将 Promise 更新为成功状态
                resolve(data);   
            },
            error: function(jqXHR, textStatus, error) {
                // 将 Promise 更新为失败状态
                reject(error);   
            }
        });
    });
    return promise;
}

调用这个封装好的 promisifyAjax 方法:

promisifyAjax("/url").then(function(data) {
    // 成功
}).catch(function(error) {
    // 失败
});

创建 CompletableFuture

在 Java 中创建 CompletableFuture 对象:

CompletableFuture<String> completableFuture = new CompletableFuture<>();
if (success) {
    completableFuture.complete("任务成功");
} else {
    completableFuture.completeExceptionally(new RuntimeException("任务失败"));
}

CompletableFuture 可以使用泛型,用于指定这个任务成功后返回的结果的数据类型。

这里可以用 OkHttp 的异步请求来实战使用一下 CompletableFuture。OkHttp 异步请求的官方示例中使用的异步实现方式是基于 Callback 回调的方式:

private final OkHttpClient client = new OkHttpClient();

public void run() throws Exception {
    Request request = new Request.Builder()
        .url("http://publicobject.com/helloworld.txt")
        .build();
    
    client.newCall(request).enqueue(new Callback() {
        @Override 
        public void onFailure(Call call, IOException e) {
            // HTTP 请求异常
            e.printStackTrace();  
        }
        
        @Override 
        public void onResponse(Call call, Response response) throws IOException {
            try (ResponseBody responseBody = response.body()) {
                if (!response.isSuccessful()) { 
                    // 响应状态码异常
                    throw new IOException("Unexpected code " + response);
                } else {
                    // 成功
                    System.out.println(responseBody.string());  
                }
            }
        }
    });
}

下面将这个异步请求封装为一个返回 CompletableFuture 的方法:

public static CompletableFuture<String> asyncRequest(String url) {
    CompletableFuture<String> completableFuture = new CompletableFuture<>();

    Request request = new Request.Builder()
        .url(url)
        .build();
    
    client.newCall(request).enqueue(new Callback() {
        @Override 
        public void onFailure(Call call, IOException e) {
            // HTTP 请求异常
            completableFuture.completeExceptionally(e);  
        }
        
        @Override 
        public void onResponse(Call call, Response response) throws IOException {
            try (ResponseBody responseBody = response.body()) {
                if (!response.isSuccessful()) { 
                    // 响应状态码异常
                    completableFuture.completeExceptionally(new IOException("Unexpected code " + response));
                } else {   
                    // 成功
                    completableFuture.complete(responseBody.string());
                }
            }
        }
    });
}

使用封装好的 asyncRequest() 方法:

asyncRequest("/url").thenAccept(responseText -> {
    // 成功
}).exceptionally(e -> {
    // 失败
});

可以看到这个写法几乎和 ES6 Promise 写法一样。

上面基于 jQuery.ajax() 函数封装并返回 Promise 对象,是为了学习 Promise 对象是如何创建的。实际上,已经有非常优秀的开源项目 Axios,它是一个基于 Promise 的 HTTP 客户端,既支持浏览器 Ajax,又支持 Node.js:

axios.get('/user?ID=12345')
    .then(function (response) {
        // handle success
        console.log(response);
    })
    .catch(function (error) {
        // handle error
        console.log(error);
    });

同样,在 Java 11 版本中新增了 java.net.http.HttpClient,纯天然支持 CompletableFuture

public static void main(String[] args) throws InterruptedException {
    HttpClient client = HttpClient.newBuilder().build();
    HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("http://xxx.com/"))
            .build();
    CompletableFuture<HttpResponse<String>> responseFuture = client.sendAsync(request, HttpResponse.BodyHandlers.ofString());
    
    responseFuture.thenAccept(response -> {
        System.out.println(response.body());
    }).exceptionally(e -> {
        System.out.println(e);
        return null;
    });

    // 防止主线程结束后程序停止
    Thread.sleep(Integer.MAX_VALUE);
}

then 的链式调用

ES6 Promise 的 then 方法的返回值同样是一个 Promise,所以可以链式调用:

axios.get('/request1')
    .then(function (response) {
        // 把第一个请求的结果作为第二个请求的参数,并且返回一个新的 Promise 作为下一个 then 的结果
        const newPromise = axios.get('/request2?param=' + response);
        return newPromise;
    })
    .then(function (response) {
        // 输出第二个请求的结果
        console.log(response);
    })
    .catch(function (error) {
        console.log(error);
    });

Java CompletableFuture 可通过 thenCompose 方法来实现多个 CompletableFuture 链式调用:

public static void main(String[] args) throws InterruptedException {
    HttpClient client = HttpClient.newBuilder().build();
    HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("http://foo.com/"))
            .build();
    CompletableFuture<HttpResponse<String>> responseFuture = client.sendAsync(request, HttpResponse.BodyHandlers.ofString());

    responseFuture.thenCompose(response -> {
        // 把第一个请求的结果作为第二个请求的参数
        HttpRequest request2 = HttpRequest.newBuilder()
                .uri(URI.create("http://foo.com/?param=" + response.body()))
                .build();
        // 这里必须返回一个新的 CompletableFuture 作为下一个 then 的结果
        CompletableFuture<HttpResponse<String>> responseFuture2 = client.sendAsync(request2, HttpResponse.BodyHandlers.ofString());
        return responseFuture2;
    }).thenAccept(response -> {
        // 输出第二个请求的结果
        System.out.println(response);
    }).exceptionally(e -> {
        e.printStackTrace();
        return null;
    });

    // 防止主线程结束后程序停止
    Thread.sleep(Integer.MAX_VALUE); 
}

Promise 中的工具方法:

  • Promise.all() 用于将多个 Promise 包装成一个新的 Promise,相当于让所有任务同时进行,当所有任务都成功后,新的 Promise 才会变为成功状态,只要有一个任务失败,新的 Promise 就会变为失败状态

    // 同时执行 3 个异步任务
    const allPromise = Promise.all([promise1, promise2, promise3]);
    allPromise.then(([result1, result2, result3]) => {
        // 3 个异步任务全部成功后,这里可以拿到所有任务的结果
    }).catch(err => {
        // 只要有一个任务失败,最终结果就是失败
    });
  • Promise.race() 用于让多个任务同时进行,只要有一个任务执行完成后(无论成功还是失败),返回的新的 Promise 就会跟随着变更状态

    // 发起 HTTP 请求,5 秒内没有响应则超时失败
    Promise.race([
        httpGet('http://example.com/file.txt'),
        delay(5000).then(function () {
            throw new Error('timeout');
        })
    ])

CompletableFuture 中也提供了上述类似的静态方法:

static CompletableFuture<Void> allOf(CompletableFuture<?>... cfs)
static CompletableFuture<Object> anyOf(CompletableFuture<?>... cfs)

其中,CompletableFuture 的 allOf() 方法类似于 Promise.all()anyOf() 方法类似于 Promise.race()

其中有一个区别是 CompletableFuture 的 allOf() 方法返回的是一个 CompletableFuture<Void>,也就是拿不到异步任务的执行结果。如果想要像 Promise.all() 一样拿到每一个任务的执行结果,可以对这个方法再进行一下封装:

public static <T> CompletableFuture<List<T>> all(CompletableFuture<T> ... cfs) {
    return CompletableFuture.allOf(cfs)
            .thenApply(v -> Stream.of(cfs)
                    .map(future -> future.join())
                    .collect(Collectors.toList()));
}

关注我的公众号

扫码关注


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK