書接上回,我們聊了為什么要有異步編程模式以及異步編程的魅力。
本文主要聊一聊異步編程中的promise模式;promise模式廣泛應用于前端開發
,所以故事先從前端開發講起。
因為前端的ajax技術,與服務端通信皆是基于異步編程,需要處理各種回調;
回調本身并不可怕,但是如果需要對多個回調結果進行compose
, 那就比較麻煩了,會出現所謂的回調地獄;在異步編程方面,前端同學先痛起來了,所以,在這個方向上,前端同學確實走的也比較靠前,模式也比較成熟。
那么對于java而言,先不談協程
的玩法,異步編程可選的模式就不多了,其中一個便是promise模式,以及接下來會進行介紹的reactor模式。
接下來將按照案例對promise模式進行安利。
(特別提示,本系列文章皆是采用kotlin編寫,寫過kotlin之后,真的很難切回java,由儉入奢易,由奢入儉難啊)
案例一
有一遠程服務名曰“計算器”,提供加法和減法兩個http接口
- 計算 n + m :/add?a=n&b=m
- 計算 n - m :/sub?a=n&b=m
基于上面的計算器服務,需要在我們的服務中開發一個接口,計算m + n - l
, 采用異步代碼該如何寫呢?此處基于vert.x
進行示范
override fun start(startPromise: Promise<Void>?) {
webClient = WebClient.create(vertx)
var eventBus = vertx.eventBus()
eventBus.consumer<JsonObject>("calc.promise"){ msg ->
var msgBody = msg.body()
var m = msgBody.getInteger("m", 0)
var n = msgBody.getInteger("n", 0)
var l = msgBody.getInteger("l", 0)
webClient.get(7777, "pi", "/add?a=$m&b=$n").send{
if (it.succeeded()) {
var `m + n` = it.result().bodyAsString().toInt()
// 此處是vertx的webClient,是一個異步的http client
// 7777是端口,pi是host(在下部署在樹莓派上做的測試),后面是uri
webClient.get(7777, "pi", "/sub?a=$`m + n`&b=$l").send{
if (it.succeeded()) {
var `m + n - l` = it.result().bodyAsString().toInt()
replyHandler(`m + n - l`.toString())
} else {
replyHandler("calc error: m + n - l")
}
}
} else {
replyHandler("calc error: m + n")
}
}
}
}
上面是一段基于callback的寫法,略微有一些回調地獄
的迷之縮進
那么如果采用promise模式,在本案例的場景下,是可以緩解回調地獄
的迷之縮進
,大家感受一下(此處同樣也是使用了vert.x里的promise類):
override fun start(startPromise: Promise<Void>?) {
webClient = WebClient.create(vertx)
var eventBus = vertx.eventBus()
eventBus.consumer<JsonObject>("calc.promise"){ msg ->
var msgBody = msg.body()
var m = msgBody.getInteger("a", 0)
var n = msgBody.getInteger("b", 0)
var l = msgBody.getInteger("c", 0)
add(m, n).future().onSuccess {
sub(it, l).future().onSuccess {
msg.reply(it.toString())
}
}
}
}
fun add(a: Int, b: Int) : Promise<Int> {
var promise = Promise.promise<Int>()
webClient.get(7777, "pi", "/add?a=$a&b=$b").send{
if (it.succeeded()) {
var addResult = it.result().bodyAsString().toInt()
promise.complete(addResult)
} else {
promise.fail("calc failed $a add $b")
}
}
return promise
}
fun sub(a: Int, b: Int) : Promise<Int> {
var promise = Promise.promise<Int>()
webClient.get(7777, "pi", "/sub?a=$a&b=$b").send{
if (it.succeeded()) {
var addResult = it.result().bodyAsString().toInt()
promise.complete(addResult)
} else {
promise.fail("calc failed $a sub $b")
}
}
return promise
}
上面這種寫法是基于promise模式的,雖然有所緩解,但主要功效還是在把一些代碼抽離了,核心的計算邏輯,依然還是迷之縮進
的,莫著急,后面還有內容。
此處先仔細看下這兩段代碼,vertx原生提供的io.vertx.core.Promise
類,就是用來做promise模式的, 那么promise相比于callback,改變到底是啥?經過日夜揣摩,在下得出結論:
當調用一個異步方法時,promise 模式可以把針對該方法的執行結果的處理邏輯從入參(回調函數),變為返回值;這樣帶來的編程變化是:基于返回值的處理,更方便的支持鏈式寫法,把縮進改為一條鏈
這個話題先聊到這里,稍微找一找promise的感覺,咱們接著案例往下聊
案例二
基于計算器服務,實現一個接口: a + ((b -c)+ d) -e -f + g
先看看基于回調怎么寫:
//源碼地址:https://github.com/HongkaiWen/vertx-kotlin/blob/promise/src/main/kotlin/com/github/hongkaiwen/reactor/vk/calc/CallbackVerticle.kt
class CallbackVerticle : AbstractVerticle(){
lateinit var webClient: WebClient
override fun start(startPromise: Promise<Void>?) {
webClient = WebClient.create(vertx)
var eventBus = vertx.eventBus()
eventBus.consumer<JsonObject>("calc.callback"){
var msgBody = it.body()
var a = msgBody.getInteger("a", 0)
var b = msgBody.getInteger("b", 0)
var c = msgBody.getInteger("c", 0)
var d = msgBody.getInteger("d", 0)
var e = msgBody.getInteger("e", 0)
var f = msgBody.getInteger("f", 0)
var g = msgBody.getInteger("g", 0)
calc(a, b, c, d, e, f, g){reply ->
it.reply(reply)
}
}
}
fun calc(a: Int, b: Int, c: Int, d: Int, e: Int, f: Int, g: Int, replyHandler: (String) -> Unit){
webClient.get(7777, "pi", "/sub?a=$b&b=$c").send{
if (it.succeeded()) {
var `b-c` = it.result().bodyAsString().toInt()
webClient.get(7777, "pi", "/add?a=$`b-c`&b=$d").send{
if (it.succeeded()) {
var `(b-c)+d` = it.result().bodyAsString().toInt()
webClient.get(7777, "pi", "/add?a=$a&b=$`(b-c)+d`").send{
if (it.succeeded()) {
var `a+(b-c)+d` = it.result().bodyAsString().toInt()
webClient.get(7777, "pi", "/sub?a=$`a+(b-c)+d`&b=$e").send{
if (it.succeeded()) {
var `a+(b-c)+d-e` = it.result().bodyAsString().toInt()
webClient.get(7777, "pi", "/sub?a=$`a+(b-c)+d-e`&b=$f").send{
if (it.succeeded()) {
var `a+(b-c)+d-e-f` = it.result().bodyAsString().toInt()
webClient.get(7777, "pi", "/add?a=$`a+(b-c)+d-e-f`&b=$g").send{
if (it.succeeded()) {
var `a+(b-c)+d-e-f+g` = it.result().bodyAsString().toInt()
replyHandler(`a+(b-c)+d-e-f+g`.toString())
} else {
replyHandler("calc error: a + (b - c) + d -e -f + g")
}
}
} else {
replyHandler("calc error: a + (b - c) + d -e -f")
}
}
} else {
replyHandler("calc error: a + (b - c) + d -e")
}
}
} else {
replyHandler("calc error: a + (b - c) + d")
}
}
} else {
replyHandler("calc error: (b - c) + d")
}
}
} else {
replyHandler("calc error: b - c")
}
}
}
}
上面這段代碼,實在讓人吐血,終于知道啥叫地獄
了, 針對上面的方法,基于promise來做改進,效果如何呢:
//源碼地址:https://github.com/HongkaiWen/vertx-kotlin/blob/promise/src/main/kotlin/com/github/hongkaiwen/reactor/vk/calc/PromiseVerticle.kt
class PromiseVerticle : AbstractVerticle(){
lateinit var webClient: WebClient
override fun start(startPromise: Promise<Void>?) {
webClient = WebClient.create(vertx)
var eventBus = vertx.eventBus()
eventBus.consumer<JsonObject>("calc.promise"){ msg ->
var msgBody = msg.body()
var a = msgBody.getInteger("a", 0)
var b = msgBody.getInteger("b", 0)
var c = msgBody.getInteger("c", 0)
var d = msgBody.getInteger("d", 0)
var e = msgBody.getInteger("e", 0)
var f = msgBody.getInteger("f", 0)
var g = msgBody.getInteger("g", 0)
sub(b, c).future().onSuccess {
add(it, d).future().onSuccess {
add(a, it).future().onSuccess {
sub(it, e).future().onSuccess {
sub(it, f).future().onSuccess {
add(it, g).future().onSuccess {
msg.reply(it.toString())
}
}
}
}
}
}
}
}
fun add(a: Int, b: Int) : Promise<Int> {
var promise = Promise.promise<Int>()
webClient.get(7777, "pi", "/add?a=$a&b=$b").send{
if (it.succeeded()) {
var addResult = it.result().bodyAsString().toInt()
promise.complete(addResult)
} else {
promise.fail("calc failed $a add $b")
}
}
return promise
}
fun sub(a: Int, b: Int) : Promise<Int> {
var promise = Promise.promise<Int>()
webClient.get(7777, "pi", "/sub?a=$a&b=$b").send{
if (it.succeeded()) {
var addResult = it.result().bodyAsString().toInt()
promise.complete(addResult)
} else {
promise.fail("calc failed $a sub $b")
}
}
return promise
}
}
大地獄和小地獄的區別而已,因此就在思忖,貌似使用姿勢不對,基于promise的代碼依然是迷之縮進
的,除非我們可以把promise的處理邏輯做成鏈式的
JavaScript的promise
感覺上面的路子還是不對,所以我們來參考下前端同學是如何玩promise的;因為在下對JavaScript不感興趣,所以不求甚解的從大神的博客 摘取一段,如下:
function multiply(input) {
return new Promise(function (resolve, reject) {
log('calculating ' + input + ' x ' + input + '...');
setTimeout(resolve, 500, input * input);
});
}
// 0.5秒后返回input+input的計算結果:
function add(input) {
return new Promise(function (resolve, reject) {
log('calculating ' + input + ' + ' + input + '...');
setTimeout(resolve, 500, input + input);
});
}
var p = new Promise(function (resolve, reject) {
log('start new Promise...');
resolve(123);
});
p.then(multiply)
.then(add)
.then(multiply)
.then(add)
.then(function (result) {
log('Got value: ' + result);
});
發現JavaScript里的promise是鏈式的(就應該這樣嘛),那么上面的例子里的姿勢一定是錯了,問題在哪呢?
JavaScript里的用法,promise是通過then函數進行順序鏈式條用的,那么這個then函數,輸入是一個處理函數,返回的是一個新的promise才行,這樣可以針對一環一環的處理作出管道的效果來。
因為我采用的vertx的promise來做研究,這個類本身處理promise的結果的方法 onSuccess
返回的是future本身,并不是一個新的promise的future,所以不好做鏈式調用
:
@Fluent
default Future<T> onSuccess(Handler<T> handler) {
return onComplete(ar -> {
if (ar.succeeded()) {
handler.handle(ar.result());
}
});
}
改進方案
仔細看了一圈io.vertx.core.Promise
的相關方法, 發現compose
正是我們的解藥。
class PromiseLineVerticle3 : AbstractVerticle(){
lateinit var webClient: WebClient
override fun start(startPromise: Promise<Void>?) {
webClient = WebClient.create(vertx)
var eventBus = vertx.eventBus()
eventBus.consumer<JsonObject>("calc.promise.line3"){ msg ->
var msgBody = msg.body()
var a = msgBody.getInteger("a", 0)
var b = msgBody.getInteger("b", 0)
var c = msgBody.getInteger("c", 0)
var d = msgBody.getInteger("d", 0)
var e = msgBody.getInteger("e", 0)
var f = msgBody.getInteger("f", 0)
var g = msgBody.getInteger("g", 0)
//只要一個onFailure即可
(b asyncSub c)
.compose { it asyncAdd d }
.compose { it asyncAdd a }
.compose { it asyncSub e }
.compose { it asyncSub f }
.compose { it asyncAdd g }
.onSuccess { msg.reply(it.toString()) }
.onFailure{msg.fail(500, it.message)}
}
}
infix fun Int.asyncAdd(input : Int) : Future<Int> {
return calc(this, input, CalcOperator.add)
}
infix fun Int.asyncSub(input : Int) : Future<Int> {
return calc(this, input, CalcOperator.sub)
}
/**
* 所有異常必須被處理
*/
fun calc(a: Int, b: Int, operator: CalcOperator) : Future<Int> {
var promise = Promise.promise<Int>()
webClient.get(7777, "pi", "/${operator.name}?a=$a&b=$b").send{
if (it.succeeded()) {
try{
var addResult = it.result().bodyAsString().toInt()
promise.complete(addResult)
println("$a - $b = $addResult")
} catch (e: Exception) {
promise.fail(e)
}
} else {
it.cause().printStackTrace()
promise.fail("calc failed $a add $b")
}
}
return promise.future()
}
}
enum class CalcOperator {
add, sub
}
通過compose
函數代碼清爽了許多。
特別要提一下的是,上面的寫法it asyncAdd d
是kotlin的中綴表達式
,另外這個地方的it,java程序員一定會問怎么沒有聲明過就使用?對的,kotlin中的lambda如果只有一個參數,就可以默認用it代替,寫法簡單而且避免不知道該其起啥變量名。
基于CompletableFuture實現promise
因為筆者最近vert.x
用的多,各種例子都是基于vert.x的;但其實如果把java和promise一起做搜索的話,得到的結果一般可能是jdk的CompletableFuture實現;而且vert.x
社區也有roadmap準備向jdk的CompletionStage
靠攏(RFC),所以再寫一個CompletableFuture的實現版本:
class PromiseLineVerticle4 : AbstractVerticle(){
lateinit var webClient: WebClient
override fun start(startPromise: Promise<Void>?) {
webClient = WebClient.create(vertx)
var eventBus = vertx.eventBus()
eventBus.consumer<JsonObject>("calc.promise.line4"){ msg ->
var msgBody = msg.body()
var a = msgBody.getInteger("a", 0)
var b = msgBody.getInteger("b", 0)
var c = msgBody.getInteger("c", 0)
var d = msgBody.getInteger("d", 0)
var e = msgBody.getInteger("e", 0)
var f = msgBody.getInteger("f", 0)
var g = msgBody.getInteger("g", 0)
(b asyncSub c)
.thenCompose { it asyncAdd d }
.thenCompose { it asyncAdd a }
.thenCompose { it asyncSub e }
.thenCompose { it asyncSub f }
.thenCompose { it asyncAdd g }
.thenAccept { msg.reply(it.toString()) }
.exceptionally {
msg.fail(500, it.message)
null
}
}
}
infix fun Int.asyncAdd(input : Int) : CompletableFuture<Int> {
return calc(this, input, CalcOperator.add)
}
infix fun Int.asyncSub(input : Int) : CompletableFuture<Int> {
return calc(this, input, CalcOperator.sub)
}
/**
* 所有異常必須被處理
*/
fun calc(a: Int, b: Int, operator: CalcOperator) : CompletableFuture<Int> {
var promise = CompletableFuture<Int>()
webClient.get(7777, "pi", "/${operator.name}?a=$a&b=$b")
.expect(ResponsePredicate.SC_OK).send{
if (it.succeeded()) {
try{
var addResult = it.result().bodyAsString().toInt()
promise.complete(addResult)
println("$a - $b = $addResult")
} catch (e: Exception) {
promise.completeExceptionally(e)
}
} else {
it.cause().printStackTrace()
promise.completeExceptionally(RuntimeException("calc failed $a add $b"))
}
}
return promise
}
}
其實無論是vert.x的promise還是jdk的CompletableFuture, 都是promise模式,寫發生也比較類似,代碼整潔度上也相似,當然筆者很樂于看到java生態圈能夠對此形成標準,就和JS的ES6統一定義了前端的promise一樣,免得很多口水。
實踐上升理論
基于上面的實踐,相信對promise是有一點感覺的了。那么需要實踐上升理論了,理論參見:
https://en.wikipedia.org/wiki/Futures_and_promises
理論說明相關文章很多,筆者理解主要是狀態機的維護,筆者就不再獻丑了,后續如果得空在寫一篇vert.x的promise源碼分析的文章。
promise是異步編程的一個模式,在某些場景下可以解決回調地獄
的問題。
接下來的一篇文章講來聊聊reactor
模式。
系列文章快速導航:
異步編程一:異步編程的魅力
異步編程二:promise模式
異步編程三:reactor模式
異步編程四:協程