对于回调地狱(Callback hell),想必大家都不陌生,尤其对于前端的朋友,当然前端的朋友通过各种办法去避免回调地狱,比如Promise。但是对于后端的朋友,尤其在RxJava、Reactor等反应式编程框架兴起之后,对于回调地狱只是听得多,但是见得的少。
为了更好了解回调地狱Callback hell问题在哪,我们首先需要学会怎么写出一个回调地狱。在之前,我们得知道什么是回调函数。
本文将包含:
我们今天从最开始讲起,先讲讲什么是回调函数。
在百度百科上,是这么说的:
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。 回调是任何一个被以方法为其第一个参数的其它方法的调用的方法。很多时候,回调是一个当某些事件发生时被调用的方法。
什么?不好理解?确实很难理解,并且这段解释还有指针云云,对于java用户实在是不友好。
给大家举个例子,供大家参考,也欢迎批评指正:
回调:调用方在调用被调方后,被调方还将结果反馈给调用方。(A调用B,B完成后,将结果反馈给A)
举个例子:老板安排员工一项工作,员工去完成。员工完成工作后,给老板反馈工作结果。这个过程就叫回调。
这下容易理解很多了吧!Talk is cheap, Show me the code! 好,我们就用这个写一个简单的例子。
首先,我们先写一个如下的 Callback
接口,接口只包含一个方法,用于callback操作。
/** * @author yangzijing */ public interface Callback<T> { /** * 具体实现 * @param t */ public void callback(T t); } 复制代码
老板是被反馈的对象,于是需要实现 Callback
这个接口,重载 callback
方法;对于老板具体要干什么,当然是做大生意,于是有了 makeBigDeals
方法;老板当然不能是光杆司令,他需要一个员工,我们再构造方法里给他添加一个员工 Worker
,稍后我们来实现Worker类。
public class Boss implements Callback<String> { 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); } } 复制代码
员工类,很简单,出入一个工作,完成就好了,返回结果即可。但是如何完成回调?
public class Worker { public String work(String someWork) { return 'result'; } } 复制代码
我们很容易想到就是这个思路,非常符合思维的逻辑,但是在回调中,我们需要做一些改变。
对于员工来说,需要知道两点,谁是老板,需要干啥。于是,输入两个参数,分别是老板和工作内容。具体内容分两步,首先完成任务,之后则是汇报给老板。
public class Worker { public void work(Callback<String> 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<String> { …… @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<String>() { 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<String>() { 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<String>() { 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<String>() { 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来异步调用的示例:
logger.info("分配工作..."); CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> worker.work(someDetail)); logger.info("分配完工作。"); logger.info("老板下班回家了。。。"); logger.info("boss got the feedback from worker: {}", future.get()); 复制代码
在上面的代码,我们可以看到,虽然 Worker
工作是异步的,但是老板获取工作的结果( future.get()
)的时候却需要等待,而这个等待的过程是阻塞的。这是回调和Future一个显著的区别。
回调和Future的对比: callback和future对比
如何解决回调地狱的问题,最常用的就是反应式编程 RxJava 和 Reactor ,还有Kotlin的Coroutine协程,OpenJDK搞的Project Loom。其中各有优势,按下不表。