严格来说,回调函数并不属于23种设计模式中的任何一种。但是,回调函数在微服务中的使用越来越多,所以今天再来总结复习一下。
回调的意义在于,通过在其他对象中调用自身类中定义的函数,达到一定的目的(常见于事件注册,监听以及线程Runnable中的run)。
一、模块间调用
在一个应用系统中,无论使用何种语言开发,必然存在模块之间的调用,调用的方式分为几种:
(1)同步调用
同步调用是最基本并且最简单的一种调用方式,类A的方法a()调用类B的方法b(),一直等待b()方法执行完毕,a()方法继续往下走。这种调用方式适用于方法b()执行时间不长的情况,因为b()方法执行时间一长或者直接阻塞的话,a()方法的余下代码是无法执行下去的,这样会造成整个流程的阻塞。
(2)异步调用
异步调用是为了解决同步调用可能出现阻塞,导致整个流程卡住而产生的一种调用方式。类A的方法方法a()通过新起线程的方式调用类B的方法b(),代码接着直接往下执行,这样无论方法b()执行时间多久,都不会阻塞住方法a()的执行。但是这种方式,由于方法a()不等待方法b()的执行完成,在方法a()需要方法b()执行结果的情况下(视具体业务而定,有些业务比如启异步线程发个微信通知、刷新一个缓存这种就没必要),必须通过一定的方式对方法b()的执行结果进行监听。在Java中,可以使用Future+Callable的方式做到这一点。
(3)回调
最后是回调,回调的思想是:
- 类A的a()方法调用类B的b()方法
- 类B的b()方法执行完毕主动调用类A的callback()方法
这样一种调用方式组成了上图,也就是一种双向的调用方式。所谓回调:就是A类中调用B类中的某个方法b(),然后B类中反过来调用A类中的方法callback(),callback()这个方法就叫回调方法。
二、回调结构:
- callback接口:定义回调方法
- class A:实现接口callback,包含一个class B的引用b
- class B:有一个参数为callback的方法func(Callback callback)
三、代码示例(一)
3.1、callback接口:定义回调方法
package cn.lxk.callback02;
/**
* 回调接口
*/
public interface Callback {
// 回调方法
public void tellAnswer(String answer);
}
3.2、class A:实现接口callback,包含一个class B的引用b
package cn.lxk.callback02;
/**
* 老师对象
*/
public class Teacher implements Callback {
private Student student;
public Teacher(Student student) {
this.student = student;
}
public void askQuestion() {
System.out.println("老师:1+2=?");
new Thread(()->student.resolveQuestion(this)).start();
}
@Override
public void tellAnswer(String answer) {
System.out.println(answer);
}
public void doSomething() {
System.out.println("老师去做其他事情。。。");;
}
}
3.3、定义class B的接口(面向接口编程,可省)
package cn.lxk.callback02;
/**
* 学生接口
*/
public interface Student {
public void resolveQuestion(Callback callback);
}
3.4、class B:有一个参数为callback的方法func(Callback callback)
package cn.lxk.callback02;
/**
* 一个名叫Ricky的同学解决老师提出的问题
*/
public class Ricky implements Student {
@Override
// 注册回调
public void resolveQuestion(Callback callback) {
// 模拟解决问题
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
}
callback.tellAnswer("学生:我知道了,你的答案是3(想了3秒钟)");
}
}
测试类:
package cn.lxk.callback02;
public class MainClass {
public static void main(String[] args) {
Student student = new Ricky();
Teacher teacher = new Teacher(student);
teacher.askQuestion();
teacher.doSomething();
}
}
测试结果:
老师:1+2=?
老师去做其他事情。。。
学生:我知道了,你的答案是3(想了3秒钟)
简单总结、分析一下这个例子就是:
(1)老师调用学生接口的方法resolveQuestion,向学生提问
(2)学生解决完毕问题之后调用老师的回调方法tellAnswer
这样一套流程,构成了一种双向调用的关系。
四、代码示例(二)--常见用法,回调函数调用时定义(注册)
4.1、定义数据
package cn.lxk.callback;
public class Data {
private int n;
private int m;
public Data(int n, int m) {
System.out.println("调用data的构造函数");
this.n = n;
this.m = m;
}
@Override
public String toString() {
int r = n / m;
return n + "/" + m + " = " + r;
}
public int getN() { return n; }
public void setN(int n) { this.n = n; }
public int getM() { return m; }
public void setM(int m) { this.m = m; }
}
4.2、定义回调函数接口
package cn.lxk.callback;
public interface FetcherCallback {
void onData(Data data) throws Exception;
void onError(Throwable cause);
}
4.3、 定义class B的接口(面向接口编程,可省)
package cn.lxk.callback;
public interface Fetcher {
void fetchData(FetcherCallback callback);
}
4.4、 class B:有一个参数为callback的方法func(Callback callback)
package cn.lxk.callback;
public class MyFetcher implements Fetcher {
final Data data;
public MyFetcher(Data data) {
System.out.println("调用MyFetcher的构造函数");
this.data = data;
}
// 此方法接受一个对象
@Override
public void fetchData(FetcherCallback callback) {
try {
//正常情况
System.out.println("调用fetchData方法正常");
// 注册回调函数,但还没有实现
callback.onData(data);
} catch (Exception e) {
//报错情况
System.out.println("调用fetchData方法异常");
// 注册回调函数,但还没有实现
callback.onError(e);
}
}
}
4.5、class A虽未实现接口callback,当通过类B(MyFetcher类),访问其中的fetchData()方法
package cn.lxk.callback;
public class Worker {
/**
* Fetcher.fetchData()方法需传递一个FetcherCallback类型的参数,当获得数据或发生错误时被回调。
对于每种情况都提供了同意的方法:
• FetcherCallback.onData(),将接收数据时被调用
• FetcherCallback.onError(),发生错误时被调用
因为可以将这些方法的执行从"caller"线程移动到其他的线程执行;但也不会保证FetcherCallback
的每个方法都会被执行。回调过程有个问题就是当你使用链式调用很多不同的方法会导致线性代码;有些人认为
这种链式调用方法会导致代码难以阅读,但是我认为这是一种风格和习惯问题。例如,基于Javascript的
Node.js越来越受欢迎,它使用了大量的回调,许多人都认为它的这种方式利于阅读和编写。
*/
public void doWork() {
System.out.println("调用doWork方法");
Fetcher fetcher = new MyFetcher(new Data(1, 0));
fetcher.fetchData(new FetcherCallback() {
@Override
public void onError(Throwable cause) {
System.out.println("错误回调: " + cause.getMessage());
}
@Override
public void onData(Data data) {
System.out.println("正常回调: " + data.toString());
}
});
}
public static void main(String[] args) {
Worker w = new Worker();
w.doWork();
}
}
测试结果:
正常常景:
调用doWork方法
调用data的构造函数
调用MyFetcher的构造函数
调用fetchData方法正常
正常回调: 1/2 = 0异常场景:
调用doWork方法
调用data的构造函数
调用MyFetcher的构造函数
调用fetchData方法正常
调用fetchData方法异常
错误回调: / by zero
五、应用(多线程)
package cn.lxk.futures;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
/**
* 有时候使用Future感觉很丑陋,因为你需要间隔检查Future是否已完成,而使用回调会直接收到返回通知。
*/
public class FutureExample {
public static void main(String[] args) throws Exception {
// 创建线程池
ExecutorService executor = Executors.newCachedThreadPool();
// 创建线程
Runnable task1 = new Runnable() {
@Override
public void run() {
// do something
System.out.println("第一个runnable线程.....");
}
};
// 创建线程
Callable<Integer> task2 = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
// do something
System.out.println("第二个Callable线程……");
return new Integer(100);
}
};
// 提交第一个线程
Future<?> f1 = executor.submit(task1);
// 提交第二个线程
Future<Integer> f2 = executor.submit(task2);
// 打印第1个线程是否结束
System.out.println("第1个线程是否结束? " + f1.isDone());
// 打印第2个线程是否结束
System.out.println("第2个线程是否结束? " + f2.isDone());
// 等待第一个线程结束
while (f1.isDone()) {
System.out.println("第一个线程结束");
break;
}
// 等待第二个线程结束
while (f2.isDone()) {
System.out.println("第二个线程结束,返回结果: " + f2.get());
break;
}
}
}
测试结果: