最近对接第三方呼叫系统,第三方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,并抛出异常:
/**
* 异常处理函数式接口
*/
@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 类型,代码如下:
import lombok.ToString;
import org.springframework.data.util.Pair;
import java.util.Optional;
import java.util.function.Function;
@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类型,所以我们还要做一下解析:
/**
* 处理包装的返回结果
* @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 类型,那么你可以选择已有的工具库来使用