Java回调机制--从模块调用说起

Java 模块之间的调用

本文简单介绍了Java模块调用的几种方式,主要通过demo阐释了回调机制的实现

在一个应用系统中,必然存在模块之间的调用,调用的方式有几种:

1. 同步调用

方法A()调用方法B(),并且等待方法B()的返回,然后A()再执行下一步操作

Java回调机制--从模块调用说起

此方法适用于B()方法执行的时间不长,如若不然,那么A()方法会长时间等待B()方法执行完成而处于阻塞状态,如此,可能会导致整个流程的阻塞。

2. 异步调用

Java回调机制--从模块调用说起

为了解决A()方法调用B()方法造成流程阻塞而出现的,最典型的方式就是开启新线程。这样的话,A()方法不必等待B()方法的返回而继续执行,但是这种方法存在一个问题:如果A()需要知道B()的执行结果(根据业务需求,有些操作如异步线程刷新缓存、推送通知等等不需要,而有些如更改状态的操作则需要),则需要通过某种方式对B()的执行结果进行监听,在Java中可以通过Future+Callable来实现。

3. 回调

Java回调机制--从模块调用说起

在回调模式中,

A()方法调用B()方法
B()方法执行完毕之后,调用A类的AA()方法(回调方法),将执行结果返回给A

要实现这个需求,有几点问题需要思考:

  • A类中如何调用B类的方法(这个简单,字段注入即可)
  • B类的方法执行完成之后,如何调用A类的callback方法(被调用的B类的方法,必须有A类对象作为形参)
  • 如何提升代码的可复用性及可扩展性?(面向接口)
3.1 一个简单的例子:
场景描述:男孩需要向女孩表白,但羞于表达,只好借助神父传达心意,神父知道男孩的请求之后,将要对女孩说的话告诉男孩。
/**
 * @author wangy
 * @date 2018/8/30 / 20:37
 * @description 这是那个男孩
 * java 回调机制的引入  A--调用-->B(c) ---调用-->A(d) d 方法称之为回调方法
 * 使用场景: A想完成某事, 但A不能独立完成,需要借助B的力量, B完成之后, 要将结果通知给A(通过调用A的方法实现)
 */
public class Kidd {
    private String name;
    private GodFather godFather;

    public Kidd(String name,GodFather godFather) {
        this.name = name;
        this.godFather = godFather;
    }

    public void askGodFather(){
        godFather.witness(this);
    }
    public void confess(String voice){
        System.out.println(this.name+":"+ voice);
    }
}

男孩类有两个方法:

  1. askGodFather() 方法用于调用神父类witness()方法
  2. confess()方法用于让神父类回调
/**
 * @author:wangy
 * @date: 2018/8/30 / 20:45
 * @description: 这是神父类
 */
public class GodFather {

    public void witness(Kidd kidd) {
       kidd.confess("the moon light is gorgeous tonight");
    }
}

神父类比较简单,只有一个方法witness(),它有一个Kidd对象作为形参,这个对象用于回调男孩类的回调方法

// 测试类
public class Test {
    public static void main(String[] args) {
        new Kidd("Alex",new GodFather()).askGodFather();
    }
}

返回:

 Alex:the moon light is gorgeous tonight

以上是一个最简单的Java 回调机制的模型,该模型还存在一些不限于以下列出的问题:

  • B类的方法形参单一,上例中形参为Kidd对象,那么,换成Cat对象便又要写一个“神父类”;
  • 同理,A类调用的B类方法单一,上例中需求的是“表白神父”*(功能A),如果换成“免灾神父”(功能B),又要写一个神父;
  • 总结起来,就是通过‘类’的方式实现回调,代码的扩展性和可复用性较差
3.2 一个相对健壮的回调机制应该是这样的
Java回调机制--从模块调用说起

以上的UML图解释了Java回调的实质:

A类和B类分别为接口的实现类,这样代码的扩展性一下就提升了

3.3 另一个简单的例子:
场景描述:老师课堂点名学生回答问题,学生解答完毕之后回答老师

回调接口(A类需实现):

/**
 * @author wangy
 * @desc 先定义一个回调接口,所有的A类要实现结果回调,必需实现该类并覆写回调方法,供B类调用
 */
public interface Callback {
    /**
     * 回调接口是供B类来调用的,所以它的形参中必须包含A类调用B方法时候的返回值(对象)
     * @param a B类方法的返回值
     * @param student 根据功能需求传递其他参数
     */
    void tellAnswer(Student student , int  a);
}

B类的接口:

/**
 * @author wangy
 * @desc B 类, 抽象为一个接口,方便A类对不同实现类(不同功能需求)的回调
 */
public interface Student {
    /**
     * 方法接收一个callback参数,用于指示B类执行方法之后,向谁"汇报"
     * @param callback
     */
    void resolveAnswer(Callback callback);

    String getName();
}

A类实体:

/**
 * @author:wangy
 * @date: 2018/9/18 / 10:17
 * @description: A类, 实现回调接口并覆写方法
 */
public class Teacher implements Callback {
    /**
     * 接口作为属性字段,便于扩展
     */
    private Student student;

    public Teacher(Student student) {
        this.student = student;
    }

    /**
     * askQuestion() 方法用于调用B类的方法
     */
    public void askQuestion() {
        student.resolveAnswer(this);
    }

    /**
     * 覆写的回调方法,用于供B类调用,以便通知执行结果
     *
     * @param a
     */
    @Override
    public void tellAnswer(Student student , int a) {
        System.out.println("嗯,"+student.getName()+" 完成任务花了 " + a + " 秒");
    }
}

A类实体有2个方法

  1. askQuestion() 用于调用B类的方法
  2. 覆写的tellAnswer()方法,用于接收回调结果

B类实体:

/**
 * @author:wangy
 * @version:1.0
 * @date: 2018/9/18 / 10:20
 * @description: B类的(某种特殊功能)实现类
 */
public class Rookie implements Student {
    private String name;

    public Rookie(String name) {
        this.name = name;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public void resolveAnswer(Callback callback) {
        // 模拟方法执行过程
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 回调,给指定callback的实现类
        callback.tellAnswer(this, 3);
    }
}

B类实体有一个方法resolveAnswer(Callback callback),接收一个callback参数,该参数用于调用A类的回调方法

测试类:

public class Test {

    public static void main(String[] args) {
        Teacher teacher = new Teacher(new Rookie("alan"));
        teacher.askQuestion();
    }
}

返回:

嗯,alan 完成任务花了 3 秒
3.4 分析

上面的例子中,对A类和B类分别进行了抽象,这样做的好处就是:

  1. 抽象了A类之后,对于B类来说,不必要关心是“哪位老师”叫B类完成任务,只需要完成任务就好了,也就是说我B类的方法,可以复用;
  2. 抽象了B类之后,对于A类来说,相对更加灵活,其调用B类的方法不仅仅只限于“one-by-one”这种模式,而是一次可以对“多个学生”进行提问,只需要将A类中的字段修改为List即可。
  3. 回调的核心就是回调方将本身即this传递给调用方,调用方接着调用回调方的方法告诉它想要知道的信息回调是一种思想、是一种机制,至于具体如何实现,如何通过代码将回调实现得优雅、实现得可扩展性比较高,一看开发者的个人水平,二看开发者对业务的理解程度。
3.5 同步回调与异步回调

上述的例子是一个典型的同步回调的示例。同步回调顾名思义就是A()调用B()之后,等待B()执行完成并且调用A()的回调函数,程序再继续执行。

异步回调就是在A()调用B()的过程中,开启一个新线程,不等待B()方法执行完成并回调之后再执行后续操作。

修改上例子中的A类中的方法,将其修改为异步回调

public class Teacher implements Callback {
    /**
     * 接口作为属性字段,便于扩展
     */
    private Student student;

    public Teacher(Student student) {
        this.student = student;
    }

    /**
     * askQuestion() 方法用于调用B类的方法
     */
    public void askQuestion() {
        // 异步回调
        new Thread(new Runnable() {
            @Override
            public void run() {
                student.resolveAnswer(Teacher.this);
            }
        }).start();
    }
    
    @Override
    public void tellAnswer(Student student , int a) {
        System.out.println("知道了,"+student.getName()+" 完成任务花了 " + a + " 秒");
    }
}

同步回调和异步回调的选择要结合具体的业务场景,比如充值服务,要将充值的结果返回给用户,调用充值服务之后必须要等待充值接口的返回;但是如果是批量的处理(如退订业务),这时候可以用异步回调,主程序完成之后(或者页面app先返回数据给用户),后台业务逻辑继续执行,最后才业务执行的结果,可以作异步处理;

References:

  • to be finished…