最近對接第三方呼叫系統,第三方SDK的所有方法裡都有異常抛出,因為用到了lambda,是以異常處理還是很必要的。
本文主要用到了四種解決方案:
- 直接代碼塊處理
- 自定義函數式接口,warp靜态方法
- 通過Either 類型包裝
- 通過Pair 類型進行再次包裝
方法一:
直接代碼塊處理:
/**
* 上線
* @param schoolId 學校id
* @param cno 座席工号,4-6 位數字
* @param bindType 電話類型,1:電話;2:分機
* @param bindTel 綁定電話
* @return
*/
@Override
public Optional<OnlineResponse> online(Integer schoolId, String cno, Integer bindType, String bindTel) {
Optional<ClientConfiguration> clientConfig = getClientConfig(schoolId);
OnlineResponse response = clientConfig.map(x -> {
try {
return new Client(x).getResponseModel(new OnlineRequest() {{
setCno(cno);
setBindType(bindType);
setBindTel(bindTel);
}});
} catch (ClientException e) {
log.error("調用天潤-上線接口,用戶端異常",e);
} catch (ServerException e) {
log.error("調用天潤-上線接口,服務端異常",e);
}
return null;
}).orElse(null);
return Optional.ofNullable(response);
}
我們大多數人都知道,lambda 代碼塊是笨重的,可讀性較差。而且一點也不優雅,丢失了lambda的簡潔性。
如果我們在 lambda 表達式中需要做多行代碼,那麼我們可以把這些代碼提取到一個單獨的方法中,并簡單地調用新方法。
是以,解決此問題的更好和更易讀的方法是将調用包裝在一個普通的方法中,該方法執行 try-catch 并從 lambda 中調用該方法,如下面的代碼所示:
myList.stream()
.map(this::trySomething)
.forEach(System.out::println);
private T trySomething(T t) {
try {
return doSomething(t);
} catch (MyException e) {
throw new RuntimeException(e);
}
這個解決方案至少有點可讀性,但是如果lambda 表達式中發生了異常,catch裡的異常是抛不出來的,因為java8裡原生的Function是沒有throw異常的,如圖:

方法二:
為了解決方法一的缺陷,我們要自定義一個函數式接口Function,并抛出異常:
/**
* 異常處理函數式接口
* @Author: lhx
* @Date: 2019/9/3 16:08
*/
@FunctionalInterface
public interface CheckedFunction<T,R> {
R apply(T t) throws Exception;
}
現在,可以編寫自己的通用方法了,它将接收這個 CheckedFunction 參數。你可以在這個通用方法中處理 try-catch 并将原始異常包裝到 RuntimeException中,如下列代碼所示:
/**
* lamber 抛出異常
* 發生異常時,流的處理會立即停止
* @param function
* @param <T>
* @param <R>
* @return
*/
public static <T,R> Function<T,R> warp(CheckedFunction<T,R> function){
return t -> {
try {
return function.apply(t);
} catch (Exception e) {
throw new RuntimeException(e);
}
};
}
實際中應用(warp靜态方法放在了Either類裡):
/**
* 上線
* @param schoolId 學校id
* @param cno 座席工号,4-6 位數字
* @param bindType 電話類型,1:電話;2:分機
* @param bindTel 綁定電話
* @return
*/
public Optional<OnlineResponse> online(Integer schoolId, String cno, Integer bindType, String bindTel) {
Optional<ClientConfiguration> clientConfig = getClientConfig(schoolId);
OnlineRequest request = new OnlineRequest() {{
setCno(cno);
setBindType(bindType);
setBindTel(bindTel);
}};
Optional<OnlineResponse> onlineResponse = clientConfig.map(Either.warp(x -> getResponseModel(x, request)));
return onlineResponse;
}
剩下的唯一問題是,當發生異常時,你的 lambda處理會立即停止,如果是stream 處理,我相信大多數人都不希望報異常後流被停止。如果你的業務可以容忍這種情況的話,那沒問題,但是,我可以想象,在許多情況下,直接終止并不是最好的處理方式。
方法三
我們可以把 “異常情況” 下産生的結果,想象成一種特殊性的成功的結果。那我們可以把他們都看成是一種資料,不管成功還是失敗,都繼續處理流,然後決定如何處理它。我們可以這樣做,這就是我們需要引入的一種新類型 - Either類型。
Either 類型是函數式語言中的常見類型,而不是 Java 的一部分。與 Java 中的 Optional 類型類似,一個 Either 是具有兩種可能性的通用包裝器。它既可以是左派也可以是右派,但絕不是兩者兼而有之。左右兩種都可以是任何類型。
如果我們将此原則用于異常處理,我們可以說我們的 Either 類型包含一個 Exception 或一個成功的值。為了友善處理,通常左邊是 Exception,右邊是成功值。
下面,你将看到一個 Either 類型的基本實作 。在這個例子中,我使用了 Optional 類型,代碼如下:
package com.xxx.xxx;
import lombok.ToString;
import org.springframework.data.util.Pair;
import java.util.Optional;
import java.util.function.Function;
/**
* @Author: lhx
* @Date: 2019/9/3 17:01
*/
@ToString
public class Either<L, R> {
private final L left;
private final R right;
private Either(L left, R right) {
this.left = left;
this.right = right;
}
public static <L,R> Either<L,R> Left( L value) {
return new Either(value, null);
}
public static <L,R> Either<L,R> Right( R value) {
return new Either(null, value);
}
public Optional<L> getLeft() {
return Optional.ofNullable(left);
}
public Optional<R> getRight() {
return Optional.ofNullable(right);
}
public boolean isLeft() {
return left != null;
}
public boolean isRight() {
return right != null;
}
public <T> Optional<T> mapLeft(Function<? super L, T> mapper) {
if (isLeft()) {
return Optional.of(mapper.apply(left));
}
return Optional.empty();
}
public <T> Optional<T> mapRight(Function<? super R, T> mapper) {
if (isRight()) {
return Optional.of(mapper.apply(right));
}
return Optional.empty();
}
/**
* lamber 抛出異常
* 發生異常時,流的處理會立即停止
* @param function
* @param <T>
* @param <R>
* @return
*/
public static <T,R> Function<T,R> warp(CheckedFunction<T,R> function){
return t -> {
try {
return function.apply(t);
} catch (Exception e) {
throw new RuntimeException(e);
}
};
}
/**
* lamber 抛出異常
* 發生異常時,流的處理會繼續
* 不儲存原始值
* @param function
* @param <T>
* @param <R>
* @return
*/
public static <T, R> Function<T, Either> lift(CheckedFunction<T,R> function){
return t -> {
try {
return Either.Right(function.apply(t));
} catch (Exception e) {
return Either.Left(e);
}
};
}
/**
* lamber 抛出異常
* 發生異常時,流的處理會繼續
* 異常和原始值都儲存在左側
* @param function
* @param <T>
* @param <R>
* @return
*/
public static <T,R> Function<T, Either> liftWithValue(CheckedFunction<T,R> function) {
return t -> {
try {
return Either.Right(function.apply(t));
} catch (Exception ex) {
return Either.Left(Pair.of(ex,t));
}
};
}
}
你現在可以讓你自己的函數傳回 Either 而不是抛出一個 Exception。但是如果你想在現有的抛出異常的 lambda 代碼中直接使用 Either 的話,你還需要對原有的代碼做一些調整(同warp方法一樣,我都放在了Either 類裡了),如下所示:
/**
* lamber 抛出異常
* 發生異常時,流的處理會繼續
* 不儲存原始值
* @param function
* @param <T>
* @param <R>
* @return
*/
public static <T, R> Function<T, Either> lift(CheckedFunction<T,R> function){
return t -> {
try {
return Either.Right(function.apply(t));
} catch (Exception e) {
return Either.Left(e);
}
};
}
這裡我們把異常資訊儲存到Left裡,其實也可以直接把left的泛型L改為Exception類型,但丢失了靈活性(就是下面提到的一點Try類型)。
通過添加這種靜态提升方法 Either,我們現在可以簡單地“提升”抛出已檢查異常的函數,并讓它傳回一個 Either。這樣做的話,我們現在最終得到一個 Eithers 流而不是一個可能會終止我們的 Stream 的 RuntimeException,具體的代碼如下:
/**
* 上線
* @param schoolId 學校id
* @param cno 座席工号,4-6 位數字
* @param bindType 電話類型,1:電話;2:分機
* @param bindTel 綁定電話
* @return
*/
public Optional<OnlineResponse> online(Integer schoolId, String cno, Integer bindType, String bindTel) {
Optional<ClientConfiguration> clientConfig = getClientConfig(schoolId);
OnlineRequest request = new OnlineRequest() {{
setCno(cno);
setBindType(bindType);
setBindTel(bindTel);
}};
Optional<Either> either = clientConfig.map(Either.lift(x -> getResponseModel(x, request)));
return null;
}
因為傳回的是Optional<Either>類型,是以我們還要做一下解析:
/**
* 處理包裝的傳回結果
* @param either
* @param <T>
* @return
*/
public T disposeResponse(Optional<Either> either) throws Exception {
if (either.isPresent()){
Either entity = either.get();
if (entity.isLeft()){
Optional<Exception> optional = entity.mapLeft(x -> x);
log.error("調用天潤接口異常:"+optional.get().getMessage(),optional.get());
throw new Exception(optional.get().getMessage());
}else {
Optional<T> optional = entity.mapRight(x -> x);
log.info("調用天潤接口傳回資訊:"+ JSON.toJSONString(optional.get()));
return optional.get();
}
}
return null;
}
實際應用代碼:
/**
* 上線
* @param schoolId 學校id
* @param cno 座席工号,4-6 位數字
* @param bindType 電話類型,1:電話;2:分機
* @param bindTel 綁定電話
* @return
*/
public Optional<OnlineResponse> online(Integer schoolId, String cno, Integer bindType, String bindTel) throws Exception {
Optional<ClientConfiguration> clientConfig = getClientConfig(schoolId);
OnlineRequest request = new OnlineRequest() {{
setCno(cno);
setBindType(bindType);
setBindTel(bindTel);
}};
return Optional.ofNullable(disposeResponse(clientConfig.map(Either.lift(x -> getResponseModel(x, request)))));
}
這樣的話,就解決了lambda中有異常停止的問題,上面的disposeResponse裡我抛出了異常,因為我需要知道第三方的異常資訊,如果你的業務不需要,可以不往外抛,直接把異常消化掉也可以。
方法四
其實也就是方法三的擴充,比如說我還想知道請求參數是什麼,請求參數我也想用到,方法三是擷取不了請求參數的。
我們現在可能遇到的問題是,如果 Either 隻儲存了包裝的異常,并且我們無法重試,因為我們丢失了原始值。
因為 Either 類型是一個通用的包裝器,是以它可以用于任何類型,而不僅僅用于異常處理。這使我們有機會做更多的事情而不僅僅是将一個 Exception 包裝到一個 Either 的左側執行個體中。
通過使用 Either 儲存任何東西的能力,我們可以将異常和原始值都儲存在左側。為此,我們隻需制作第二個靜态提升功能,spring的org.springframework.data.util.Pair類。
/**
* lamber 抛出異常
* 發生異常時,流的處理會繼續
* 異常和原始值都儲存在左側
* @param function
* @param <T>
* @param <R>
* @return
*/
public static <T,R> Function<T, Either> liftWithValue(CheckedFunction<T,R> function) {
return t -> {
try {
return Either.Right(function.apply(t));
} catch (Exception ex) {
return Either.Left(Pair.of(ex,t));
}
};
}
你可以看到,在這個 liftWithValue 函數中,這個 Pair 類型用于将異常和原始值配對到 Either 的左側,如果出現問題我們可能需要所有資訊,而不是隻有 Exception。
解析方法
/**
* 處理包裝的傳回結果
* @param either
* @param <T>
* @return
*/
public <T extends ResponseModel> T disposeResponsePair(Optional<Either> either) throws Exception {
if (either.isPresent()){
Either entity = either.get();
if (entity.isLeft()){
Optional<Pair> optional = entity.mapLeft(x -> x);
Object second = optional.get().getSecond();
log.info("請求參數:{}",JSON.toJSONString(second));
Exception first = (Exception)optional.get().getFirst();
log.error("調用天潤接口異常:"+first.getMessage(),first);
throw new Exception(first.getMessage());
}else {
Optional<Pair> optional = entity.mapRight(x -> x);
log.info("調用天潤接口傳回資訊:"+ JSON.toJSONString(optional.get().getSecond()));
return (T) optional.get().getSecond();
}
}
return null;
}
實際應用:
/**
* 上線
* @param schoolId 學校id
* @param cno 座席工号,4-6 位數字
* @param bindType 電話類型,1:電話;2:分機
* @param bindTel 綁定電話
* @return
*/
public Optional<OnlineResponse> online(Integer schoolId, String cno, Integer bindType, String bindTel) throws Exception {
Optional<ClientConfiguration> clientConfig = getClientConfig(schoolId);
OnlineRequest request = new OnlineRequest() {{
setCno(cno);
setBindType(bindType);
setBindTel(bindTel);
}};
return Optional.ofNullable(disposeResponsePair(clientConfig.map(Either.liftWithValue(x -> getResponseModel(x, request)))));
}
如果 Either 是一個 Right 類型,我們知道我們的方法已正确執行,我們可以正常的提取結果。另一方面,如果 Either 是一個 Left 類型,那意味着有地方出了問題,我們可以提取 Exception 和原始值,然後我們可以按照具體的業務來繼續處理。
擴充
包裝成 Try 類型
使用過 Scala 的人可能會使用 Try 而不是 Either 來處理異常。Try 類型與 Either 類型是非常相似的。
它也有兩種情況:“成功”或“失敗”。失敗時隻能儲存 Exception 類型,而成功時可以儲存任何你想要的類型。
是以 Try 可以說是 Either 的一種固定的實作,因為他的 Left 類型被确定為 Exception了,如下列的代碼所示:
public class Try<Exception, R> {
private final Exception failure;
private final R succes;
public Try(Exception failure, R succes) {
this.failure = failure;
this.succes = succes;
}
}
有人可能會覺得 Try 類型更加容易使用,但是因為 Try 隻能将 Exception 儲存在 Left 中,是以無法将原始資料儲存起來,這就和最開始 Either 不使用 Pair 時遇到的問題一樣了。是以我個人更喜歡 Either 這種更加靈活的。
無論如何,不管你使用 Try 還是 Either,這兩種情況,你都解決了異常處理的初始問題,并且不要讓你的流因為 RuntimeException而終止。
使用已有的工具庫
無論是 Either 和 Try 是很容易實作自己。另一方面,您還可以檢視可用的功能庫。例如:VAVR(以前稱為Javaslang)确實具有可用的類型和輔助函數的實作。我建議你去看看它,因為它比這兩種類型還要多得多。
但是,你可以問自己這樣一個問題:當你隻需幾行代碼就可以自己實作它時,是否希望将這個大型庫作為依賴項進行異常處理。
結論
當你想在 lambda 表達式中調用一個會抛出異常的方法時,你需要做一些額外的處理才行。
- 将其包裝成一個 RuntimeException 并且建立一個簡單的包裝工具來複用它,這樣你就不需要每次都寫try/catch 了
- 如果你想要有更多的控制權,那你可以使用 Either 或者 Try 類型來包裝方法執行的結果,這樣你就可以将結果當成一段資料來處理了,并且當抛出 RuntimeException 時,你的流也不會終止。
- 如果你不想自己封裝 Either 或者 Try 類型,那麼你可以選擇已有的工具庫來使用