对于回调地狱(Callback hell),想必大家都不陌生,尤其对于前端的朋友,当然前端的朋友通过各种办法去避免回调地狱,比如Promise。但是对于后端的朋友,尤其在RxJava、Reactor等反应式编程框架兴起之后,对于回调地狱只是听得多,但是见得的少。
为了更好了解回调地狱Callback hell问题在哪,我们首先需要学会怎么写出一个回调地狱。在之前,我们得知道什么是回调函数。
本文将包含:
什么是回调
回调的优势
回调地狱是什么
为什么会出现回调地狱
回调和Future有什么区别
如何解决回调地狱
我们今天从最开始讲起,先讲讲什么是回调函数。
什么是回调函数?
在百度百科上,是这么说的:
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。 回调是任何一个被以方法为其第一个参数的其它方法的调用的方法。很多时候,回调是一个当某些事件发生时被调用的方法。
什么?不好理解?确实很难理解,并且这段解释还有指针云云,对于java用户实在是不友好。
给大家举个例子,供大家参考,也欢迎批评指正:
回调:调用方在调用被调方后,被调方还将结果反馈给调用方。(A调用B,B完成后,将结果反馈给A) 举个例子:老板安排员工一项工作,员工去完成。员工完成工作后,给老板反馈工作结果。这个过程就叫回调。
这下容易理解很多了吧!Talk is cheap, Show me the code! 好,我们就用这个写一个简单的例子。
回调的例子
Callback接口
首先,我们先写一个如下的Callback接口,接口只包含一个方法,用于callback操作。
/**
* @author yangzijing
*/
public interface Callback {
/**
* 具体实现
* @param t
*/
public void callback(T t);
}
Boss类
老板是被反馈的对象,于是需要实现Callback这个接口,重载callback方法;对于老板具体要干什么,当然是做大生意,于是有了makeBigDeals方法;老板当然不能是光杆司令,他需要一个员工,我们再构造方法里给他添加一个员工Worker,稍后我们来实现Worker类。
public class Boss implements Callback {
private Worker worker;
public Boss(Worker worker) {
this.worker = worker;
}
@Override
public void callback(String s) {
}
public void makeBigDeals(final String someDetail) {
worker.work(someDetail);
}
}
Worker类
员工类,很简单,出入一个工作,完成就好了,返回结果即可。但是如何完成回调?
public class Worker {
public String work(String someWork) {
return 'result';
}
}
我们很容易想到就是这个思路,非常符合思维的逻辑,但是在回调中,我们需要做一些改变。
让代码回调起来
对于员工来说,需要知道两点,谁是老板,需要干啥。于是,输入两个参数,分别是老板和工作内容。具体内容分两步,首先完成任务,之后则是汇报给老板。
public class Worker {
public void work(Callback boss, String someWork) {
String result = someWork + 'is done!'; // 做一些具体的处理
boss.callback(result); // 反馈结果给老板
}
}
接下来,我们完成Boss类。在callback方法中,接收到传来的结果,并对结果进行处理,我们这里仅打印出来;在makeBigDeals方法中,老板分配工作,员工去完成,如果完成过程是异步,则是异步调用,如果是同步的,则是同步回调,我们这里采用异步方式。
在新建线程中,我们执行worker.work(Boss.this, someDetail),其中Boss.this即为当前对象,在这里,我们正式完成了回调。
public class Boss implements Callback {
……
@Override
public void callback(String result) { // 参数为worker输出的结果
logger.info("Boss got: {}", result) // 接到完成的结果,并做处理,在这里我们仅打印出来
}
public void makeBigDeals(final String someDetail) {
logger.info("分配工作");
new Thread(() -> worker.work(Boss.this, someDetail)); // 异步完成任务
logger.info("分配完成");
logger.info("老板下班。。");
}
}
回调结果
Show me the result! 好,跑一下代码试一下。
Worker worker = new Worker();
Boss boss = new Boss(worker); // 给老板指派员工
boss.makeBigDeals("coding"); // 老板有一个代码要写
结果如下。在结果中可以看到,老板在分配完工作后就下班了,在下班后,另一个线程通知老板收到反馈"coding is done"。至此,我们完成了异步回调整个过程。
INFO 2019 九月 20 11:30:54,780 [main] - 分配工作
INFO 2019 九月 20 11:30:54,784 [main] - 分配完成
INFO 2019 九月 20 11:30:54,784 [main] - 老板下班。。
INFO 2019 九月 20 11:30:54,787 [Thread-0] - Boss got: coding is done!
我将代码示例传至Github,供大家参考。 callback代码示例
回调的优势
解耦,回调将子过程从主过程中解耦。 对于相同的输入,可能对其有不同的处理方式。在回调函数,我们完成主流程(例如上面的Boss类),对于过程中的子流程(例如上面的Worker类)从主流程中分离出来。对于主流程,我们只关心子过程的输入和输出,输入在上面的例子中即为Worker.work中的参数,而子过程的输出则是主过程的callback方法的参数。
异步回调不会阻塞主线程。上面的例子清晰可以看到,员工没有完成工作之前老板就已经下班,当工作完成后,会通过另一个线程通知老板。老板在这个过程无需等待子过程。
回调地狱
总体设计
我们将上述功能扩展,老板先将工作交给产品经理进行设计;设计完成后,交给程序员完成编码。流程示意如图。
将任务交给产品经理
首先,写一个Callback,内部new一个产品经理的的Worker,在makeBigDeal方法实现主任务,将任务交给产品经理;在重载的callback方法中,获取产品经理的输出。
new Callback() {
private Worker productManager = new Worker();
@Override
public void callback(String s) {
System.out.println("产品经理 output: " + s); // 获取产品经理的输出
}
public void makeBigDeals(String bigDeal) {
System.out.println("Boss将任务交给产品");
new Thread(() -> {
this.productManager.work(this, bigDeal); // 异步调用产品经理处理过程
}).start();
}
}.makeBigDeals("design");
再将产品经理输出交给开发
在拿到产品经理的输出之后,再将输出交给开发。于是我们在再次实现一个Callback接口。同样的,在Callback中,new一个开发的Worker,在coding方法中,调用Worker进行开发;在重载的callback方法中,获取开发处理后的结果。
@Override
public void callback(String s) {
System.out.println("产品经理 output: " + s); // 产品经理的输出
String midResult = s + " coding";
System.out.println("产品经理设计完成,再将任务交给开发");
new Callback() {
private Worker coder = new Worker();
@Override
public void callback(String s) {
System.out.println("result: " + s); // 获取开发后的结果
}
public void coding(String coding) {
new Thread(() -> coder.work(this, coding)).start(); // 调用开发的Worker进行开发
}
}.coding(midResult); // 将产品经理的输出交给开发
}
完整的实现
new Callback() {
private Worker productManager = new Worker();
@Override
public void apply(String s) {
System.out.println("产品经理 output: " + s);
String midResult = s + " coding";
System.out.println("产品经理设计完成,再将任务交给开发");
new Callback() {
private Worker coder = new Worker();
@Override
public void apply(String s) {
System.out.println("result: " + s);
}
public void coding(String coding) {
new Thread(() -> coder.work(this, coding)).start();
}
}.coding(midResult);
}
public void makeBigDeals(String bigDeal) {
System.out.println("Boss将任务交给产品");
new Thread(() -> this.productManager.work(this, bigDeal)).start();
}
}.makeBigDeals("design");
好了,一个简单的回调地狱完成了。Show me the result!
Boss将任务交给产品
产品经理 output: design is done!
产品经理设计完成,再将任务交给开发
result: design is done! coding is done!
回调地狱带来了什么?
到底什么是回调地狱?简单的说,回调地狱就是Callback里面又套了一个Callback,但是如果嵌套层数过多,仿佛掉入地狱,于是有了回调地狱的说法。
优势: 回调地狱给我们带来什么?事实上,回调的代码如同管道一样,接收输入,并将处理后的内容输出至下一步。而回调地狱,则是多个管道连接,形成的一个流程,而各个子流程(管道)相互独立。前端的朋友可能会更熟悉一些,例如Promise.then().then().then(),则是多个处理管道形成的流程。
劣势: 回调的方法虽然将子过程解耦,但是回调代码的可读性降低、复杂性大大增加。
Callback Hell示例:Callback Hell
和Future对比
在上面,我们提到异步回调不会阻塞主线程,那么使用Future也不会阻塞,和异步回调的差别在哪?
我们写一个使用Future来异步调用的示例:
logger.info("分配工作...");
CompletableFuture future = CompletableFuture.supplyAsync(() -> worker.work(someDetail));
logger.info("分配完工作。");
logger.info("老板下班回家了。。。");
logger.info("boss got the feedback from worker: {}", future.get());
在上面的代码,我们可以看到,虽然Worker工作是异步的,但是老板获取工作的结果(future.get())的时候却需要等待,而这个等待的过程是阻塞的。这是回调和Future一个显著的区别。
如何解决
如何解决回调地狱的问题,最常用的就是反应式编程RxJava和Reactor,还有Kotlin的Coroutine协程,OpenJDK搞的Project Loom。其中各有优势,按下不表。
总结
总结一下:
什么是回调。回调是调用方在调用被调方后,被调方还将结果反馈给调用方。(A调用B,B完成后,将结果反馈给A)
回调的优势。1)子过程和主过程解耦。2)异步调用并且不会阻塞主线程。
回调地狱是什么。回调地狱是回调函数多层嵌套,多到看不清=。=
为什么会出现回调地狱。每一个回调像一个管道,接受输出,处理后将结果输出到下一管道。各个管道处理过程独立,多个管道组成整个处理过程。
回调和Future有什么区别。1)两者机制不同;2)Future在等待结果时会阻塞,而回调不会阻塞。
如何解决回调地狱。最常见的则是反应式编程RxJava和Reactor。