来聊聊函数回调
# 用一次重构了解函数回调
假设有一个这样一个服务商,只需用户将视频文件上传到它的接口,即生成一个饺子的3D建模。
我们不妨用一段代码模拟一下该服务商的接口,只需用户调用该接口即可得到一个饺子的3D建模。
/**
* 饺子提供商
*/
public class DumplingProvider {
/**
* 返回一个饺子的3D建模文件
*
* @return
*/
public String makeDumpling() {
return "3D-Dumpling Model File";
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
于是我们的APP1接入了这个接口,并成功得到饺子的3D建模文件。
public class App1 {
public static void main(String[] args) {
DumplingProvider provider = new DumplingProvider();
//调用服务商的接口获取饺子建模文件
System.out.println(provider.makeDumpling());
}
}
2
3
4
5
6
7
输出结果:
3D-Dumpling Model File
随着服务商业务的拓展,越来越多的APP准备接入该接口,并且不同的APP可能还要求饺子模型有所不同,例如:
- APP1希望得到默认的饺子建模文件。
- APP2希望得到一个小号的饺子建模文件。
- APP3希望得到一个中号的饺子建模文件。
- APP4希望得到一个圆形的饺子建模文件。
对此我们的服务商的代码可能会变成这样:
/**
* 饺子提供商
*/
public class DumplingProvider {
/**
* 返回一个饺子的3D建模文件
*
* @return
*/
public String makeDumpling(String appName) {
if ("APP1".equals(appName)) {
return "3D-Dumpling Model File";
} else if ("APP2".equals(appName)) {
return "3D-small-Dumpling Model File";
} else if ("APP3".equals(appName)) {
return "3D-middle-Dumpling Model File";
} else if ("APP4".equals(appName)) {
return "3D-circle-Dumpling Model File";
}
return "";
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
同时APP1调用的接口也需要做出调整:
public class App1 {
public static void main(String[] args) {
DumplingProvider provider = new DumplingProvider();
//调用服务商的接口获取饺子建模文件
System.out.println(provider.makeDumpling("APP1"));
}
}
2
3
4
5
6
7
随着服务商业务再一次拓展,对于建模的要求定制化越来越细致,而且调用方也变得越来越多,如果通过硬编码的形式会导致维护成本逐渐变大,所以回调函数就派上了用场。通过让调用方提供回调函数,让服务方通过回调调用方的函数得到定制化需求,然后返回对应的建模文件给用户。

于是我们再次对代码进行改造,让每一个调用方都继承下面这个接口,并实现modeFormat方法,要求modeFormat返回自己对建模的定制化需求。
public interface ModeFormat {
String modeFormat();
}
2
3
于是服务商的代码就会变成这样,通过回调调用方的函数获取定制化需求,并根据需求返回对应格式的建模。
/**
* 饺子提供商
*/
public class DumplingProvider {
/**
* 返回一个饺子的3D建模文件
*
* @return
*/
public String makeDumpling(ModeFormat format) {
return format.modeFormat()+"-3D-Dumpling Model File";
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
我们上文中提到APP2要求定制小型的饺子建模,所以对应的调用代码如下,可以看到app2传入一个回调函数告知服务商自己的需求,服务商在收到该调用后就会根据该回调得到需求并返回定制化结果。
public class App2 {
public static void main(String[] args) {
DumplingProvider provider = new DumplingProvider();
//调用服务商的接口获取饺子建模文件
System.out.println(provider.makeDumpling(()->"small"));
}
}
2
3
4
5
6
7
8
输出结果:
small-3D-Dumpling Model File
2
到此,我们来一个小结,可以看到通过回调函数,可以让复杂的逻辑细节交由调用方实现,而服务提供方只需提供一个通用的接口即可应对成千上万的需求。
# 异步回调
# 异步回调示例
我们上文中对于建模的调用都是同步实现的,随着调用方的增多,所以调用方调用建模接口时的耗时可能会增加,假设APP1的代码如下所示,在获取建模文件后,还要执行一些剩余的业务需求:
public static void main(String[] args) {
DumplingProvider provider = new DumplingProvider();
//调用服务商的接口获取饺子建模文件
System.out.println(provider.makeDumpling(() -> ""));
//完成剩余业务需求
doSomething();
}
private static void doSomething() {
System.out.println("完成剩余业务需求");
}
2
3
4
5
6
7
8
9
10
11
12
13
因为服务方的业务量增大,导致makeDumpling执行需要10s,而doSomething有要求能够立刻完成,对此我们就必须要让服务商的方法变成异步的,确保服务方的接口调用时能够立刻返回,并且服务方在我们执行业务逻辑期间,能够异步回调我们的接口获取定制化需求完成建模,确保调用方可以立刻执行重要的逻辑,再回过头拉取服务方的结果。

于是服务方代码变成这样:
/**
* 饺子提供商
*/
public class DumplingProvider {
/**
* 返回一个饺子的3D建模文件
*
* @return
*/
public FutureTask<String> makeDumpling(ModeFormat format) {
//将建模任务封装成异步的,并开启线程执行,再将任务返回给调用方,确保调用立刻返回,返回期间进行建模工作
FutureTask<String> task = new FutureTask<>(()->format.modeFormat() + "-3D-Dumpling Model File");
new Thread(task).start();
return task;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
对应的调用方代码也要做出响应调整,可以看到调用服务方接口后即可得到一个异步任务,代码并不会阻塞,而是继续处理剩余业务逻辑后,再去拉取调用结果。
public class App1 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
DumplingProvider provider = new DumplingProvider();
//调用服务商的接口获取饺子建模文件的异步任务
Future<String> future = provider.makeDumpling(() -> "default");
//完成剩余业务需求
doSomething();
//拉取异步任务结果
System.out.println(future.get());
}
private static void doSomething() {
System.out.println("完成剩余业务需求");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
输出结果如下,可以看到代码优先执行完成了剩余的重要业务需求,然后再去拉取异步结果,通过让服务方调用变成异步,期间的调用也变成异步调用的方式避免了调用阻塞,确保调用方业务逻辑的顺畅。
完成剩余业务需求
default-3D-Dumpling Model File
2
# 异步回调的思想
可以看出异步的回调的工作流程如下:
- 调用方发起调用。
- 服务方收到调用,以异步的方式执行业务逻辑。
- 因为服务方异步执行,调用方的调用可以立即返回,并执行后续逻辑。
- 服务方在调用方工作期间,发起回调,获取回调函数内部逻辑和结果完成逻辑执行。
- 调用方完成所有逻辑,拉取服务方异步任务结果。
异步回调利用了多核CPU使得回调函数不再是阻塞式执行,而是服务方异步执行时回调,也就是说对于调用方来说,它只关心自己该做什么,对于回调何时被执行是未知的。但可以明确的是,这样的做法尽可能的利用了多核资源,提升的程序执行效率。

# 小结
# 回调的函数的定义
回调函数本质上就是普通函数中的一种,它和普通函数一样,唯一的区别就是它将函数以参数的形式传给其他可执行代码,而不是我们常规调用的整数、小数、对象。 通过回调函数,我们可以将复杂的逻辑细节封装起来,服务方在提供服务时,只需通过回调即可获得定制化需求,所以对于网络通信这种客户端、服务端交互场景,回调函数就非常适用于作为服务端的响应处理器,在收到客户端的消息时,只需将结果以入参形式传到客户端的回调函数即可完成双向通信。

# 两种回调类型
由上文介绍可知,函数的回调分为阻塞式回调和异步回调:
- 同步回调:即阻塞式,它的回调会在调用方发起调用期间执行,服务方结果必须在调用期间完成并返回。

- 异步回调:异步回调即服务方是异步执行的,调用方只需提供回调函数,无需关心服务方何时发起回调,只需在执行其他工作完成后,及时去拉取调用结果即可。相比前者异步回调充分的历用多核资源,所以对于高并发场景下的IO操作,异步回调相比同步回调更有优势。

# 回调地狱问题
虽然回调带来的好处众多,但是我们还是需要不能过度使用回调方法,假如我们的功能需要顺序调用4个服务,为了提高程序执行效率,我们采用嵌套异步回调,那么代码就会变成这样:
//getServiceA传入回调a
getServiceA(function(a){
//a调用getServiceB传入回调b
getServiceB(a, function(b){
//b调用getServicec传入回调c
getServicec(b,function(c){
//c调用getServiceD传入回调d
getServiceD(c,function(d) {
});
});
});
});
2
3
4
5
6
7
8
9
10
11
12
13
很明显,因为执行异步,这就使得这种嵌套结构下的代码调试变得非常困难,而且可读性也不高,所以对于使用回调时,我们需要慎重考虑一下功能维护成本和难度再使用。
# 参考文献
计算机底层的秘密:https://book.douban.com/subject/36370606/ (opens new window)