在平时的工作中,很少会听到回调这个词语。但是在自己不断地学习当中,发现很多地方用到了回调,比如 I/O 模型中的 Reactor 模型、Spring 框架中的 JdbcTemplate、Spring WebFlux 和观察者模式,其实也用上了回调。若是以后学习反应式编程,回调知识是必备的基础。

含义

回调基于组合关系来实现,把一个对象引用传递给另一个对象,一种对象之间的关系。
回调也可以说是把一段可执行代码当做参数传递到另一段代码中,然后在将来某个时间点执行这段可执行代码。我知道上面那句话很抽象,下面会用例子讲解给大家。

实际案例

传递对象

在 Java 8 之前,java 语法不支持传递函数指针,只能通过传递对象给另外一个对象。

  1. public class Test {
  2. public static void main(String[] args) throws Exception {
  3. new Test().doWork(new Callback() { // 匿名类
  4. @Override
  5. public void call() {
  6. System.out.println("回调了");
  7. }
  8. });
  9. }
  10. public void doWork(Callback callback) {
  11. System.out.println("正在工作");
  12. callback.call();
  13. }
  14. public interface Callback {
  15. void call();
  16. }
  17. }

执行结果:

  1. 正在工作
  2. 回调了

当调用 doWork 的方法时,将 CallBack 对象引用传递给 doWork 的方法参数,传递进入的引用稍后执行回调。整个流程还是比较清晰。这样做的好处是,我们将 doWork 方法的流程和 call 方法的流程剥离开来,两个函数不会产生太多的耦合。doWork 方法可以执行一些其他操作(这里只打印了几个字),到 call 方法时发生回调,call 方法之前的代码不会产生阻塞。

传递函数

Java 8 以后有了 lambda 表达式,我们可以将一个函数当做一个参数传递到方法参数里。所以我们继续用 lambda 表达式改造上面的代码。

  1. public class Test {
  2. public static void main(String[] args) throws Exception {
  3. // 匿名类
  4. new Test().doWork(() -> System.out.println("回调了"));
  5. }
  6. public void doWork(Callback callback) {
  7. System.out.println("正在工作");
  8. callback.call();
  9. }
  10. public interface Callback {
  11. void call();
  12. }
  13. }

这里用 lambda 表达式把一段代码塞进了 doWork 中,可以执行回调,成功打印字符串。接下来我们继续改造,假设这段 lambda 表达式有返回值,而且 doWork 方法里要根据回调方法的返回值产生新的流程分支。思考一下,这段代码我们应该如何修改。

  1. public class Test {
  2. public static void main(String[] args) {
  3. // 匿名类
  4. new Test().doWork(() -> {
  5. Random random = new Random();
  6. return random.nextInt(100);
  7. });
  8. }
  9. public void doWork(Supplier supplier) {
  10. System.out.println("正在工作");
  11. Integer result = (Integer) supplier.get();
  12. if (result % 2 == 0) {
  13. System.out.println(result + "被2整除");
  14. } else {
  15. System.out.println(result + "不能被2整除");
  16. }
  17. }
  18. public interface Supplier<T> {
  19. T get();
  20. }
  21. }

在上面一段代码中,主函数 doWork 依赖回调函数的返回值,所以调用 get 方法时会发生阻塞,而 get 方法之前的代码不会发生阻塞(还没有回调呢)。

JdbcTemplate

Java 提供了 JDBC 类库来封装不同类型的数据库操作。不过,直接使用 JDBC 来编写操作数据库的代码,还是有点复杂的。比如,下面这段是使用 JDBC 来查询用户信息的代码。

  1. public class JdbcDemo {
  2. public User queryUser(long id) {
  3. Connection conn = null;
  4. Statement stmt = null;
  5. try {
  6. //1.加载驱动
  7. Class.forName("com.mysql.jdbc.Driver");
  8. conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/demo", "root", "123456");
  9. //2.创建statement类对象,用来执行SQL语句
  10. stmt = conn.createStatement();
  11. //3.ResultSet类,用来存放获取的结果集
  12. String sql = "select * from user where id=" + id;
  13. ResultSet resultSet = stmt.executeQuery(sql);
  14. String eid = null, ename = null, price = null;
  15. while (resultSet.next()) {
  16. User user = new User();
  17. user.setId(resultSet.getLong("id"));
  18. user.setName(resultSet.getString("name"));
  19. user.setTelephone(resultSet.getString("telephone"));
  20. return user;
  21. }
  22. } catch (ClassNotFoundException e) {
  23. // TODO: log...
  24. } catch (SQLException e) {
  25. // TODO: log...
  26. } finally {
  27. if (conn != null)
  28. try {
  29. conn.close();
  30. } catch (SQLException e) {
  31. // TODO: log...
  32. }
  33. if (stmt != null)
  34. try {
  35. stmt.close();
  36. } catch (SQLException e) {
  37. // TODO: log...
  38. }
  39. }
  40. return null;
  41. }
  42. }

queryUser() 函数包含很多流程性质的代码,跟业务无关,比如,加载驱动、创建数据库连接、创建 statement、关闭连接、关闭 statement、处理异常。针对不同的 SQL 执行请求,这些流程性质的代码是相同的、可以复用的,我们不需要每次都重新敲一遍。
若使用 JdbcTemplate,代码瞬间少了很多。

  1. public class JdbcTemplateDemo {
  2. private JdbcTemplate jdbcTemplate;
  3. public User queryUser(long id) {
  4. String sql = "select * from user where id="+id;
  5. return jdbcTemplate.query(sql, BeanPropertyRowMapper.newInstance(User.class)).get(0);
  6. }
  7. }

那 JdbcTemplate 底层具体是如何实现的呢?我们来看一下它的源码。因为 JdbcTemplate 代码比较多,我只摘抄了部分相关代码,贴到了下面。

  1. @Nullable
  2. public <T> T query(
  3. PreparedStatementCreator psc, @Nullable final PreparedStatementSetter pss, final ResultSetExtractor<T> rse)
  4. throws DataAccessException {
  5. Assert.notNull(rse, "ResultSetExtractor must not be null");
  6. logger.debug("Executing prepared SQL query");
  7. return execute(psc, new PreparedStatementCallback<T>() {
  8. @Override
  9. @Nullable
  10. public T doInPreparedStatement(PreparedStatement ps) throws SQLException {
  11. ResultSet rs = null;
  12. try {
  13. if (pss != null) {
  14. pss.setValues(ps);
  15. }
  16. rs = ps.executeQuery();
  17. return rse.extractData(rs);
  18. }
  19. finally {
  20. JdbcUtils.closeResultSet(rs);
  21. if (pss instanceof ParameterDisposer) {
  22. ((ParameterDisposer) pss).cleanupParameters();
  23. }
  24. }
  25. }
  26. });
  27. }

execute 方法的第二个参数是一个回调函数,这个函数主要目的是执行查询、处理结果集,并关闭结果集资源。让我们再看看 execute 方法的内部。

  1. @Override
  2. @Nullable
  3. public <T> T execute(PreparedStatementCreator psc, PreparedStatementCallback<T> action)
  4. throws DataAccessException {
  5. Assert.notNull(psc, "PreparedStatementCreator must not be null");
  6. Assert.notNull(action, "Callback object must not be null");
  7. if (logger.isDebugEnabled()) {
  8. String sql = getSql(psc);
  9. logger.debug("Executing prepared SQL statement" + (sql != null ? " [" + sql + "]" : ""));
  10. }
  11. Connection con = DataSourceUtils.getConnection(obtainDataSource());
  12. PreparedStatement ps = null;
  13. try {
  14. ps = psc.createPreparedStatement(con);
  15. applyStatementSettings(ps);
  16. T result = action.doInPreparedStatement(ps);
  17. handleWarnings(ps);
  18. return result;
  19. }
  20. catch (SQLException ex) {
  21. // Release Connection early, to avoid potential connection pool deadlock
  22. // in the case when the exception translator hasn't been initialized yet.
  23. if (psc instanceof ParameterDisposer) {
  24. ((ParameterDisposer) psc).cleanupParameters();
  25. }
  26. String sql = getSql(psc);
  27. psc = null;
  28. JdbcUtils.closeStatement(ps);
  29. ps = null;
  30. DataSourceUtils.releaseConnection(con, getDataSource());
  31. con = null;
  32. throw translateException("PreparedStatementCallback", sql, ex);
  33. }
  34. finally {
  35. if (psc instanceof ParameterDisposer) {
  36. ((ParameterDisposer) psc).cleanupParameters();
  37. }
  38. JdbcUtils.closeStatement(ps);
  39. DataSourceUtils.releaseConnection(con, getDataSource());
  40. }
  41. }

根据之前的结论,T result = action.doInPreparedStatement(ps);这行代码之前是不会发生阻塞的。整个 excecute 方法就像是一个模板方法,我们不关心加载驱动、创建数据库连接、创建 statement、关闭连接、关闭 statement,我们想要的是执行查询,并给我们返回结果集。