原创 Shepherd Shepherd进阶笔记 2024年09月14日 08:40 浙江
详解回调机制callback及在函数式编程的实际应用
Shepherd进阶笔记
专注分享Java技术进阶知识点,日常工作点点滴滴,滴水穿石
67篇原创内容
公众号
1.概述
回调(Callback)是一种编程模式,其中一个函数(或方法)在执行完成后通过调用另一个函数(或方法)来传递执行结果,或在特定事件发生时调用。这种模式常用于异步操作、事件驱动编程中,可以提升代码的可扩展性、灵活性和模块化。
1.1 什么是回调
回调机制指的是将一个方法或函数作为参数传递给另一个方法,待特定事件或操作完成时调用这个方法,处理结果或执行后续操作。可以理解为一种“通知”机制,当一个任务完成时,调用回调函数来传递结果或执行后续逻辑
回调可以分为同步回调和异步回调:
异步回调:常见于异步操作,如网络请求、文件 I/O 操作等,在任务执行完成后,异步回调函数会被调用,处理返回的结果或错误。
同步回调:指回调函数在主函数执行期间立即被调用。即回调是顺序执行的,不涉及多线程或异步操作
整体回调机制流程如下图所示:
可以看出回调是一种双向调用关系,类A
事先把回调函数callback()
注册到类B
上,接下来类A
的methodA()
在调用类B
的methodB()
的时候,类B
的methodB()
又反过来调用事先注册好的类A
回调函数callback()
,最终形成A调用B,B又调用A的双向调用场景,这就是回调机制
1.2 回调的结构
一个回调函数一般会包括以下三个部分:
-
回调注册:将回调函数作为参数传递给另一个函数。例如:传递给一个异步任务。
-
任务执行:主函数执行任务,可能是异步或同步操作。
-
回调调用:任务执行完成后,根据成功或失败等条件调用回调函数。
1.3 回调的作用
回调函数允许我们把控制权转移给任务的调用方。在不同的场景下,回调函数有很多作用:
-
事件驱动编程:当事件发生时(如按钮点击、页面加载完成等),触发回调函数。
-
异步处理:任务完成后,回调函数可以处理返回结果或错误,避免阻塞程序。
-
模块化:回调使得代码更加解耦、模块化,不同的任务可以通过不同的回调函数来处理结果。
2.回调的实现方式
Java的回调机制常通过接口、匿名内部类、或Lambda表达式来实现。接口提供了一种将方法作为参数传递的手段,而Lambda表达式是Java 8引入的用于简化回调写法的一种方式。
2.1 通过接口实现回调
Java中,接口是实现回调的常见方式。可以通过定义一个回调接口,包含需要回调的方法,然后在业务逻辑中通过传递接口的实现类,触发回调。代码示例如下:
// 1. 定义回调接口
interface Callback {
void onComplete(String result);
}
// 2. 定义业务逻辑类,接受回调
class Task {
public void execute(Callback callback) {
// 模拟一些业务逻辑处理
String result = "Task Completed!";
// 回调通知调用方
callback.onComplete(result);
}
}
// 3. 使用回调
public class CallBackTest {
public static void main(String[] args) {
Task task = new Task();
// 通过匿名类实现回调
task.execute(new Callback() {
@Override
public void onComplete(String result) {
System.out.println("Callback received: " + result);
}
});
}
}
Callback接口:定义了一个onComplete
方法,用于在任务完成后回调结果。
Task类:模拟了一个任务,完成后调用回调的onComplete
方法。
匿名类:在调用Task
的execute
方法时,通过匿名类实现了回调接口,并处理回调结果。
2.2 通过Lambda表达式实现回调(Java 8+)
从Java 8开始,Lambda表达式让回调的实现变得更加简洁。Lambda可以简化接口实现,尤其适用于回调只有一个方法的情况(即函数式接口)。
public class CallBackTest {
public static void main(String[] args) {
Task task = new Task();
// 使用Lambda表达式实现回调
task.execute(result -> System.out.println("Callback received: " + result));
}
}
2.3 异步回调
在同步回调中,回调函数会立即执行。而在异步回调中,任务执行完成后回调函数是在另一个线程中被调用的,常用于网络请求、数据库查询等耗时操作。
在Java中,异步回调通常使用 CompletableFuture
来实现。
import java.util.concurrent.CompletableFuture;
interface Callback {
void onComplete(String result);
}
class AsyncTask {
public void executeAsync(Callback callback) {
CompletableFuture.supplyAsync(() -> {
// 模拟耗时任务
try { Thread.sleep(2000); } catch (InterruptedException e) { }
return "Task Completed!";
}).thenAccept(callback::onComplete); // 任务完成后回调
}
}
public class CallBackTest {
public static void main(String[] args) {
AsyncTask task = new AsyncTask();
// 异步回调,任务完成后调用
task.executeAsync(result -> System.out.println("Async callback received: " + result));
System.out.println("Main thread continues executing...");
}
}
代码解析:
-
CompletableFuture.supplyAsync
:执行异步任务,模拟耗时操作。 -
thenAccept
:异步任务完成后调用回调函数。 -
主线程不会等待任务完成,而是继续执行。
2.4 支付异步回调
实际上,回调不仅可以应用在代码设计实现上,在更高层次的架构设计上也比较常用。比如,通过三方支付系统来实现支付功能,用户在发起支付请求之后,一般不会一直阻塞到支付结果返回,而是注册回调接口(类似回调函数,一般是一个回调用的 URL)给三方支付系统,等三方支付系统执行完成之后,将结果通过回调接口返回给用户,代码如下所示:
@Controller
@RequestMapping("/api/mall/pay")
@Api("支付相关接口")
@Slf4j
public class PayController {
@Resource
private PayService payService;
@Resource
private WxPayConfig wxPayConfig;
@GetMapping(value="/create", produces = "text/html")
@ApiOperation("发起支付")
public ModelAndView create(@RequestParam("orderNo") String orderNo,
@RequestParam("payType") Integer payType) {
// 发起支付,会传递回调通知接口地址
PayResponse response = payService.create(orderNo, payType);
// 支付方式不同,渲染就不同, WXPAY_NATIVE使用codeUrl, ALIPAY_PC使用body
Map<String, String> map = new HashMap<>();
if (Objects.equals(payType, PayConstant.TYPE_WXPAY)) {
map.put("codeUrl", response.getCodeUrl());
map.put("orderNo", orderNo);
map.put("returnUrl", wxPayConfig.getReturnUrl());
return new ModelAndView("wxView", map);
}else if (Objects.equals(payType, PayConstant.TYPE_ALIPAY)) {
map.put("body", response.getBody());
return new ModelAndView("alipayView", map);
}
throw new RuntimeException("暂不支持的支付类型");
}
@PostMapping("/notify")
@ApiOperation("异步通知")
@ResponseBody
public String asyncNotify(@RequestBody String notifyData) {
return payService.asyncNotify(notifyData);
}
}
配置文件:
wx:
appId: wxd898f****
mchId: 14834****
mchKey: 098F6BCD4621****
notifyUrl: http://shepherd.natapp1.cc/api/mall/pay/notify
returnUrl: http://127.0.0.1
alipay:
appId: 201610220****
privateKey: MIIEvgIBADAN****
publicKey: MIIBIjANBgkqhkiG****
notifyUrl: http://39q70t4507.qicp.vip/api/mall/pay/notify
returnUrl: http://127.0.0.1
可以看到在发起支付的时候把回调的通知接口地址给到支付平台,支付平台通过回调通知接口告知调用方支付成功与否,这种方式也是回调思想的体现。
项目推荐:基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba企业级系统架构底层框架封装,解决业务开发时常见的非功能性需求,防止重复造轮子,方便业务快速开发和企业技术栈框架统一管理。引入组件化的思想实现高内聚低耦合并且高度可配置化,做到可插拔。严格控制包依赖和统一版本管理,做到最少化依赖。注重代码规范和注释,非常适合个人学习和企业使用
Github地址:https://github.com/plasticene/plasticene-boot-starter-parent
Gitee地址:https://gitee.com/plasticene3/plasticene-boot-starter-parent
微信公众号:Shepherd进阶笔记
交流探讨qun:Shepherd_126
3.回调机制在主流框架中的应用
回调机制思想在很多框架都有应用,因为通过回调可以使代码更加灵活、优雅,这里我就用顶流Spring
框架来进行示例吧,因为我在第一次阅读Spring
源码时,看到这个回调机制写法第一反应还没太看明白,深入了解跟进之后才深感赞叹与佩服~,回到正题,我们都知道BeanFactory
是Spring
框架的顶层接口,定义一系列获取bean的方法:
Object getBean(String name) throws BeansException;
<T> T getBean(String name, Class<T> requiredType) throws BeansException;
Object getBean(String name, Object... args) throws BeansException;
<T> T getBean(Class<T> requiredType) throws BeansException;
<T> T getBean(Class<T> requiredType, Object... args) throws BeansException;
跟随代码进入到实现类AbstractBeanFactory
,实现逻辑代码如下:
@Override
public Object getBean(String name) throws BeansException {
return doGetBean(name, null, null, false);
}
@Override
public <T> T getBean(String name, Class<T> requiredType) throws BeansException {
return doGetBean(name, requiredType, null, false);
}
#doGetBean()
就是获取bean的核心逻辑所在(ps: Spring框架里面do开头的都是真正实现逻辑的地方),下面是单例bean的创建核心逻辑:
// 上面的缓存中没找到,需要根据不同的模式创建
// bean实例化
// Create bean instance.
if (mbd.isSingleton()) { // 单例模式
sharedInstance = getSingleton(beanName, () -> {
try {
return createBean(beanName, mbd, args);
}
catch (BeansException ex) {
// 显式从单例缓存中删除 Bean 实例
// 因为单例模式下为了解决循环依赖,可能他已经存在了,所以销毁它
destroySingleton(beanName);
throw ex;
}
});
bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
}
注意,注意,注意,这里的#getSingleton()
方法调用使用了函数式参数传递,典型的函数式编程,这就意味着后续会来执行回调方法#createBean()
。接下来看看#getSingleton()
:
public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
Assert.notNull(beanName, "Bean name must not be null");
// 对单例缓存对象map加锁
synchronized (this.singletonObjects) {
// 从缓存中获取
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
.....
// 单例bean创建前置处理,标记当前bean在创建中
beforeSingletonCreation(beanName);
boolean newSingleton = false;
boolean recordSuppressedExceptions = (this.suppressedExceptions == null);
if (recordSuppressedExceptions) {
this.suppressedExceptions = new LinkedHashSet<>();
}
try {
// 初始化 bean
// 这个过程其实是调用 createBean() 方法
// singletonFactory由回调方法产生
singletonObject = singletonFactory.getObject();
newSingleton = true;
}
.....
finally {
if (recordSuppressedExceptions) {
this.suppressedExceptions = null;
}
// 单例bean创建后置处理,标记当前bean不在创建中
afterSingletonCreation(beanName);
}
// 加入缓存
if (newSingleton) {
addSingleton(beanName, singletonObject);
}
}
return singletonObject;
}
}
你会发现该方法的第二个参数ObjectFactory
是一个函数式接口,执行到代码singletonObject = singletonFactory.getObject()
的时候会回调执行#createBean()
来创建bean对象。这就是Spring框架bean工厂容器核心逻辑生成bean的写法,可见回调和函数式编程的实用性。
4.回调机制在业务系统开发中实战
既然回调机制这么灵活优雅,那我们就来实战一下吧,做过金融行业的大概都知道,这么多银行和金融平台的信贷数据格式都是不一样的,这时候加入有一个系统需要对接各个银行或者金融平台的数据就很麻烦了,总不能一家银行一家银行地去硬编码实现吧!!!重复劳动不够优雅。现在的问题就是如何高效地清洗转换成业务数据导入我们自己的业务系统。
数据对接的流程大体是固定的:通过调银行接口或者访问银行数据文件获取源数据,然后进行清洗转换成业务系统需要的格式数据,导入业务系统,记录日志。这里变换的就是数据转换,我们将它抽取出来通过函数式编程进行封装,然后使用 Lambda 来简化代码使用,从而完美应对不同银行的数据转换,做到灵活不失优雅,下面是代码封装和简单示例:
// 定义转换数据的函数式接口
@FunctionalInterface
public interface DataTransformer<T, R> {
R transform(T input);
}
// 逻辑处理类
public class DataProcessor<T, R> {
public R processData(T data, DataTransformer<T, R> transformer) {
return transformer.transform(data);
}
}
// 使用场景
public class DataProcessor<T, R> {
public static <T, R> R processData(T data, DataTransformer<T, R> transformer) {
return transformer.transform(data);
}
public static void main(String[] args) {
// 简单示例1
Integer length = DataProcessor.processData("Hello", String::length);
System.out.println(length); // 输出 5
// 示例2:从person转换为student
Person person = new Person();
person.setName("zhang san");
person.setAge(18);
Student student = DataProcessor.processData(person, DataProcessor::convertToStudent);
System.out.println(student);
}
private static Student convertToStudent(Person person) {
Student student = new Student();
student.setName(person.getName());
student.setAge(person.getAge());
return student;
}
}
5.总结
Java中的回调机制(Callback)是实现异步编程、解耦代码、灵活处理任务的一种重要方式。通过回调,方法A可以在方法B完成时被通知并处理结果。Java的回调机制并不像JavaScript那样原生支持函数作为参数,但通过接口、匿名类或Lambda表达式,可以轻松实现类似的效果。
Shepherd进阶笔记
专注分享Java技术进阶知识点,日常工作点点滴滴,滴水穿石
67篇原创内容
公众号