promise-java非同步程式設計解決方案

忘語本尊發表於2018-07-31

java promise(GitHub)是Promise A+規範的java實現版本。Promise A+是commonJs規範提出的一種非同步程式設計解決方案,比傳統的解決方案—回撥函式和事件—更合理和更強大。promise實現了Promise A+規範,包裝了java中對多執行緒的操作,提供統一的介面,使得控制非同步操作更加容易。實現過程中參考文件如下:

基本使用:修改pom.xml

<repositories>
    <repository>
      <id>wjj-maven-repo</id>
      <url>https://raw.github.com/zhanyingf15/maven-repo/master</url>
    </repository>
</repositories>
複製程式碼
<dependency>
  <groupId>com.wjj</groupId>
  <artifactId>promise</artifactId>
  <version>1.0.0</version>
</dependency>
複製程式碼

如果maven settings.xml使用了mirror配置,修改mirrorOf

<mirror>
  <id>nexus</id>
  <mirrorOf>*,!wjj-maven-repo</mirrorOf> 
  <url>http://maven.aliyun.com/nexus/content/groups/public/</url>
</mirror>
複製程式碼
IPromise promise = new Promise.Builder().promiseHanler(new PromiseHandler() {
    @Override
    public Object run(PromiseExecutor executor) throws Exception {
        return 2*3;
    }
}).build();
複製程式碼

上面的例子中建立了一個promise物件,指定PromiseHandler實現,在run方法中寫具體的業務邏輯,類似於Runable的run方法。promise物件一經建立,將立即非同步執行。推薦使用lambda表示式,更加簡潔。

IPromise promise = new Promise.Builder().promiseHanler(executor -> {
    return 2*3;
}).build();
複製程式碼

獲取promise的執行結果通常使用兩個方法thenlisten,前者是阻塞的後者是非阻塞的。then方法返回一個新的promise物件,因此支援鏈式呼叫。

new Promise.Builder().promiseHanler(executor -> {//promise0
    return 2*3;
}).build().then(resolvedData -> {//返回一個新的promise1
    System.out.println(resolvedData);
    return (Integer)resolvedData+1;
}).then(res2->{
    System.out.println(res2);
    //建立一個新的promise2並返回
    return new Promise.Builder().externalInput(res2).promiseHanler(executor -> {
        return (Integer)executor.getExternalInput()+2;
    }).build();
}).then(res3->{
    System.out.println(res3);
    return res3;
});
複製程式碼

從上面可以看到promise0、promise1和Promise2是鏈式呼叫的,每一次then方法都返回一個新的promise。在then方法的回撥中,如果返回的是一個非promise物件,那麼promise被認為是一個fulfilled狀態的promise,如果返回的是一個promsie例項,那麼該例項將會非同步執行。
假如需要非同步順序執行a->b-c->d四個執行緒,呼叫順序如下

new PromiseA()
.then(dataA->new PromiseB())//A的回撥
.then(dataB->new PromiseC())//B的回撥
.then(dataC->new PromiseD())//C的回撥
.then(dataD->xxx)//D的回撥
.pCatch(error->xxxx)//捕獲中間可能產生的異常
複製程式碼

DOCS

docs文件參考promise wiki

promise規範

promise規範可以參考 Promise A+規範。其中ES6 Promise物件 在Promise A+規範上做了一些補充。java promise在使用上基本與ES6 Promise物件保持一致,部分地方有些許不同。 Promise的三個狀態

  • pending:等待態,對應執行緒未執行或執行中
  • fulfilled:完成態,對應執行緒正常執行完畢,其執行結果稱為終值
  • rejected:拒絕態,對應執行緒異常結束,其異常原因稱為拒因
    狀態轉移只能由pending->fulfilled或pending->rejected,狀態一旦發生轉移無法再次改變。

使用

IPromise promise = new Promise.Builder().promiseHandler(handler->2*3).build();//mark1
promise.then(resolvedData -> {
    System.out.println(resolvedData);
    return null;
});
複製程式碼

建立一個執行緒非常簡單,mark1標註的行建立一個IPromise例項promise,並指定非同步邏輯,這裡簡單地做了個乘法操作。promise例項一經建立,非同步邏輯將立即執行,執行結果或執行中丟擲的異常將儲存在promise例項中。可以在建立promise例項時指定一個執行緒池。

ExecutorService pool = Promise.pool(5,10);
IPromise promise = new Promise.Builder().pool(pool).promiseHandler(handler->2*3).build();
複製程式碼

上面建立了一個最小為5最大為10的執行緒池,promise例項對應的執行緒將被提交的執行緒池中執行。promise可以通過then或listen方法獲取執行結果,then方法是阻塞的而listen是非阻塞的。
通常情況下非同步邏輯需要訪問外部引數,而外部引數往往並不是final的,promise提供了輸入外部引數到內部邏輯的方法externalInput

Map<String,String> map = ImmutableMap.of("name","張三");
IPromise promise = new Promise.Builder().externalInput(map).promiseHandler(handler->{
    Map<String,String> m = (Map<String,String>)handler.getExternalInput();
    return "你好:"+m.get("name");
}).build();
複製程式碼

resolve和reject

resolve和reject是PromiseExecutor類的方法,resolve方法將promise狀態由pending->fulfilled,reject方法將promise狀態由pending->rejected。如果promise已經是非pending狀態,resolve和reject呼叫將無效。

new Promise.Builder().promiseHandler(handler->{
    int a = 2*3;
    handler.resolve(a);
    return null;
}).build().listen(((resolvedData, e) -> {
    System.out.println(resolvedData);
}));
複製程式碼

上面的例子中,計算出a的值,手動將promise狀態轉移為fulfilled,並將a的值作為promise的終值。同樣也可以手動呼叫handler.reject(e)將promise狀態轉為rejected,e(e為Throwable例項)作為promise的據因。

在前面和後面的例子中,並沒有顯示呼叫handler.resolve(x)方法,而是return具體的結果。因為在 resolve方法之後return之前程式丟擲異常,該異常不會更改promise的狀態,異常會被內部吞掉,resolve方法已經將promise的狀態修改為fulfilled了。

new Promise.Builder().promiseHandler(handler->{
    int a = 2*3;
    handler.resolve(a);
    throw new RuntimeException("err");
}).build()
        .listen(((resolvedData, e) -> {
    System.out.println(resolvedData);
    System.out.println(e==null);
}));
複製程式碼

列印結果

6
true
複製程式碼

上面的例子中,手動呼叫resolve方法後,後續邏輯即便是丟擲了異常,e仍然是null,因為promise狀態已經轉變為fulfilled,後續的所有邏輯(包括return的值)已經跟promise的最終狀態無關,後續異常和返回結果將被忽略。因此非特殊情況下不建議直接呼叫resolve方法,而是直接return返回執行結果。這種方式是handler.resolve(x)的隱式做法,return的結果將作為promise的終值

異常捕獲

promise的rejected狀態對應執行緒異常結束(執行時異常或手動呼叫executor.reject(e)),其據因儲存了異常例項,這些異常被promise內部吞掉,並不會丟擲到當前執行環境中,所有try...catch是無法捕獲到promise內部邏輯丟擲的異常的。promise提供了多種方式可以偵測到內部異常:

then方法的第二個引數是可選引數,如果發生異常,第二個引數回撥將被執行。

listen和pFinally行為是一致的,onCompleteListener的listen(Object resolvedData,Throwable e)方法第二引數為異常物件,如果發生異常,e為異常例項,否則為null。

pCatch是推薦使用的異常捕獲方式,then和listen對於異常只能“觀察”不能修正,在promise鏈式呼叫時,一旦發生異常,then方法只能觀察到異常發生,但是異常仍會向呼叫鏈後方傳遞,並拒絕後面promise的執行。pCatch不同,它捕獲到異常後,可以自行根據業務邏輯對異常處理,繼續執行後面的promise鏈。

new Promise.Builder().promiseHandler(executor -> 3).build().then(resolvedData->{//p1
    System.out.println("a:"+resolvedData);
    return new Promise.Builder().promiseHandler(executor -> {//p2
        executor.reject(new RuntimeException("err"));
        return resolvedData;
    }).build();
}).then(resolvedData1 -> {//p3
        System.out.println("b:"+resolvedData1);
        return "b:"+resolvedData1;
    },rejectReason -> {
        System.err.println("c:"+rejectReason);
    }
).then(resolvedData2 -> {//p4
        System.out.println("d:"+resolvedData2);
        return "d:"+resolvedData2;
    },rejectReason -> {
        System.err.println("e:"+rejectReason);
    }
);
複製程式碼

執行結果

a:3
c:java.lang.RuntimeException: err
e:java.lang.RuntimeException: err
複製程式碼

在上面的例子中,p1,p2,p3鏈式呼叫,p1執行後在p2處手動丟擲異常,p2的then偵測到異常,p3,p4的正常邏輯被取消了

new Promise.Builder().promiseHandler(executor -> 3).build().then(resolvedData->{
    System.out.println("a:"+resolvedData);
    return new Promise.Builder().promiseHandler(executor -> {
        executor.reject(new RuntimeException("err"));
        return resolvedData;
    }).build();
}).pCatch(e->{
    System.out.println("捕獲到異常");
    return 3;
}).then(resolvedData1 -> {
        System.out.println("b:"+resolvedData1);
        return "b:"+resolvedData1;
    },rejectReason -> {
        System.err.println("c:"+rejectReason);
    }
).then(resolvedData2 -> {
        System.out.println("d:"+resolvedData2);
        return "d:"+resolvedData2;
    },rejectReason -> {
        System.err.println("e:"+rejectReason);
    }
);
複製程式碼

列印結果

a:3
捕獲到異常
b:3
d:b:3
複製程式碼

pCatch捕獲到異常後,返回一個修正值3,這個值會傳遞個下一個promise處理,繼續完成鏈式呼叫。pCatch也可以直接返回一個promise,promise的狀態決定是否繼續後續鏈的執行(如果pCatch返回的promise是rejected狀態仍然會拒絕後續promise的執行直到遇到下一個pCatch)

pCatch可以在promise鏈的任何位置出現,出現的次數不受限制,如果沒有異常出現,將忽略pCatch邏輯。listen和pFinally只能在鏈的末尾出現,無論異常是否發生,它都將被呼叫(類似於try...catch...finally)。

promise 組合

由於開發中無法預計執行緒什麼時候執行結束,有時需要拿到執行緒執行結果在進行下一步操作就比較麻煩。如果是單個promise,可以簡單地使用then方法阻塞當前執行緒,等待promise執行緒執行完畢。如果是多個promise並行執行,需要等待所有的promise都執行完畢才能執行下一步,可以使用all或者waitAll方法。

IPromise p1 = new Promise.Builder().promiseHandler(executor -> {
    Thread.sleep(1000);
    return 1;
}).build();
IPromise p2 = new Promise.Builder().promiseHandler(executor -> {
    Thread.sleep(4000);
    return 2;
}).build();
IPromise p3 = new Promise.Builder().promiseHandler(executor -> {
    Thread.sleep(2000);
    return 3;
}).build();
Promise.all(p1,p2,p3).then(resolvedData -> {
    Object[] datas = (Object[])resolvedData;
    for(Object d:datas){
        System.out.println(d);
    }
    return null;
},e->e.printStackTrace());
複製程式碼

上面建立了三個promise,Promise.all將三個promise組裝成一個新promise p,新的promise p的狀態將由p1-p3的狀態決定,如果p1-p3全部正常結束,p的狀態是fulfilled,其終值是一個陣列,按傳入順序儲存p1-p3的執行結果。依據promise規範,如果p1-p3任意一個異常結束或手動呼叫executor.reject()方法將pn狀態轉為rejected,p的狀態會轉為rejected,並嘗試取消其餘promise。具體可以參考wiki all

1.0.1版本all還有一個過載方法all(ExecutorService threadPool,final IPromise ...promises),可以指定p的執行環境,不指定執行緒池預設新開一個執行緒。

在有些情況下,當p1-p3的其中一個發生異常時,並不希望p的狀態立即轉變為rejected並嘗試取消其餘promise的執行,而是希望其餘promise繼續執行,可以使用waitAll()方法。Promise.waitAll將多個promise組裝成一個新promise p,不同於all,p1-p3的狀態不會影響p的狀態,如果p自身未發生異常(waitAll內部使用了CountDownLatch處理多個執行緒,可能會有異常),p的狀態一直是fulfilled,其終值是一個陣列,陣列值是pn的終值或據因。具體可參考wiki-waitAll,使用方式如下:

IPromise p1 = new Promise.Builder().promiseHandler(handler->2*3).build();
IPromise p2 = new Promise.Builder().promiseHandler(handler->{
    throw new RuntimeException("手動丟擲異常");
}).build();
IPromise p = Promise.waitAll(p1,p2).then(resolvedData -> {
    Object[] datas = (Object[]) resolvedData;
    for(Object d:datas){
        if(d instanceof Throwable){
            ((Throwable)d).printStackTrace();
        }else{
            System.out.println(d);
        }
    }
    return datas;
});
複製程式碼

輸出結果

6
java.lang.RuntimeException: 手動丟擲異常
複製程式碼

p1為正常執行完畢,其終值為6,p2手動丟擲異常,使用waitAll後,p的終值為一個陣列,遍歷陣列需要判斷值的型別。

類似於all,Promise.race方法將多個 Promise p1,...pn例項,包裝成一個新的 Promise 例項 p,只要p1-pn有一個狀態發生改變(無論是轉變為正常狀態還是異常狀態),p的狀態立即改變,並嘗試取消其餘promise的執行。第一個改變的promise的狀態和終值作為p的狀態和終值

Promise.resolve和Promise.pTry

這兩個方法都是Promise的靜態方法。Promise.resolve方法有多個過載,最重要的一個是resolve(Object object,String methodName,List<Object> args),該方法是將object的指定方法以非同步方式執行,該方法的執行結果作為Promise的終值,具體可參考wiki Promise.resolve

pTry方法將object的指定方法以同步方式執行,該方法的執行結果作為Promise的終值,如果object為IPromise例項,將忽略methodName和args引數,非同步執行該例項。
該方法是以Promise統一處理同步和非同步方法,不管object是同步操作還是非同步操作,都可以使用then指定下一步流程,用pCatch方法捕獲異常,避免開發中出現以下情況

try{
  object.doSomething(args1,args2);//可能會丟擲異常
  promise.then(resolvedData->{
      //一些邏輯
  }).then(resolvedData->{
      //一些邏輯
  }).pCatch(e->{
      //異常處理邏輯
  })
}catch(Exception e){
  //異常處理邏輯
}
複製程式碼

使用pTry,可以簡化異常處理

List args = new ArrayList(){args1,args2};
Promise.pTry(object,"doSomething",args)
.then(resolvedData->{
      //一些邏輯
}).then(resolvedData->{
  //一些邏輯
}).pCatch(e->{
  //異常處理邏輯
})
複製程式碼

相關文章