在平时的工作中,很少会听到回调这个词语。但是在自己不断地学习当中,发现很多地方用到了回调,比如 I/O 模型中的 Reactor 模型、Spring 框架中的 JdbcTemplate、Spring WebFlux 和观察者模式,其实也用上了回调。若是以后学习反应式编程,回调知识是必备的基础。
含义
回调基于组合关系来实现,把一个对象引用传递给另一个对象,一种对象之间的关系。
回调也可以说是把一段可执行代码当做参数传递到另一段代码中,然后在将来某个时间点执行这段可执行代码。我知道上面那句话很抽象,下面会用例子讲解给大家。
实际案例
传递对象
在 Java 8 之前,java 语法不支持传递函数指针,只能通过传递对象给另外一个对象。
public class Test {public static void main(String[] args) throws Exception {new Test().doWork(new Callback() { // 匿名类@Overridepublic void call() {System.out.println("回调了");}});}public void doWork(Callback callback) {System.out.println("正在工作");callback.call();}public interface Callback {void call();}}
执行结果:
正在工作回调了
当调用 doWork 的方法时,将 CallBack 对象引用传递给 doWork 的方法参数,传递进入的引用稍后执行回调。整个流程还是比较清晰。这样做的好处是,我们将 doWork 方法的流程和 call 方法的流程剥离开来,两个函数不会产生太多的耦合。doWork 方法可以执行一些其他操作(这里只打印了几个字),到 call 方法时发生回调,call 方法之前的代码不会产生阻塞。
传递函数
Java 8 以后有了 lambda 表达式,我们可以将一个函数当做一个参数传递到方法参数里。所以我们继续用 lambda 表达式改造上面的代码。
public class Test {public static void main(String[] args) throws Exception {// 匿名类new Test().doWork(() -> System.out.println("回调了"));}public void doWork(Callback callback) {System.out.println("正在工作");callback.call();}public interface Callback {void call();}}
这里用 lambda 表达式把一段代码塞进了 doWork 中,可以执行回调,成功打印字符串。接下来我们继续改造,假设这段 lambda 表达式有返回值,而且 doWork 方法里要根据回调方法的返回值产生新的流程分支。思考一下,这段代码我们应该如何修改。
public class Test {public static void main(String[] args) {// 匿名类new Test().doWork(() -> {Random random = new Random();return random.nextInt(100);});}public void doWork(Supplier supplier) {System.out.println("正在工作");Integer result = (Integer) supplier.get();if (result % 2 == 0) {System.out.println(result + "被2整除");} else {System.out.println(result + "不能被2整除");}}public interface Supplier<T> {T get();}}
在上面一段代码中,主函数 doWork 依赖回调函数的返回值,所以调用 get 方法时会发生阻塞,而 get 方法之前的代码不会发生阻塞(还没有回调呢)。
JdbcTemplate
Java 提供了 JDBC 类库来封装不同类型的数据库操作。不过,直接使用 JDBC 来编写操作数据库的代码,还是有点复杂的。比如,下面这段是使用 JDBC 来查询用户信息的代码。
public class JdbcDemo {public User queryUser(long id) {Connection conn = null;Statement stmt = null;try {//1.加载驱动Class.forName("com.mysql.jdbc.Driver");conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/demo", "root", "123456");//2.创建statement类对象,用来执行SQL语句stmt = conn.createStatement();//3.ResultSet类,用来存放获取的结果集String sql = "select * from user where id=" + id;ResultSet resultSet = stmt.executeQuery(sql);String eid = null, ename = null, price = null;while (resultSet.next()) {User user = new User();user.setId(resultSet.getLong("id"));user.setName(resultSet.getString("name"));user.setTelephone(resultSet.getString("telephone"));return user;}} catch (ClassNotFoundException e) {// TODO: log...} catch (SQLException e) {// TODO: log...} finally {if (conn != null)try {conn.close();} catch (SQLException e) {// TODO: log...}if (stmt != null)try {stmt.close();} catch (SQLException e) {// TODO: log...}}return null;}}
queryUser() 函数包含很多流程性质的代码,跟业务无关,比如,加载驱动、创建数据库连接、创建 statement、关闭连接、关闭 statement、处理异常。针对不同的 SQL 执行请求,这些流程性质的代码是相同的、可以复用的,我们不需要每次都重新敲一遍。
若使用 JdbcTemplate,代码瞬间少了很多。
public class JdbcTemplateDemo {private JdbcTemplate jdbcTemplate;public User queryUser(long id) {String sql = "select * from user where id="+id;return jdbcTemplate.query(sql, BeanPropertyRowMapper.newInstance(User.class)).get(0);}}
那 JdbcTemplate 底层具体是如何实现的呢?我们来看一下它的源码。因为 JdbcTemplate 代码比较多,我只摘抄了部分相关代码,贴到了下面。
@Nullablepublic <T> T query(PreparedStatementCreator psc, @Nullable final PreparedStatementSetter pss, final ResultSetExtractor<T> rse)throws DataAccessException {Assert.notNull(rse, "ResultSetExtractor must not be null");logger.debug("Executing prepared SQL query");return execute(psc, new PreparedStatementCallback<T>() {@Override@Nullablepublic T doInPreparedStatement(PreparedStatement ps) throws SQLException {ResultSet rs = null;try {if (pss != null) {pss.setValues(ps);}rs = ps.executeQuery();return rse.extractData(rs);}finally {JdbcUtils.closeResultSet(rs);if (pss instanceof ParameterDisposer) {((ParameterDisposer) pss).cleanupParameters();}}}});}
execute 方法的第二个参数是一个回调函数,这个函数主要目的是执行查询、处理结果集,并关闭结果集资源。让我们再看看 execute 方法的内部。
@Override@Nullablepublic <T> T execute(PreparedStatementCreator psc, PreparedStatementCallback<T> action)throws DataAccessException {Assert.notNull(psc, "PreparedStatementCreator must not be null");Assert.notNull(action, "Callback object must not be null");if (logger.isDebugEnabled()) {String sql = getSql(psc);logger.debug("Executing prepared SQL statement" + (sql != null ? " [" + sql + "]" : ""));}Connection con = DataSourceUtils.getConnection(obtainDataSource());PreparedStatement ps = null;try {ps = psc.createPreparedStatement(con);applyStatementSettings(ps);T result = action.doInPreparedStatement(ps);handleWarnings(ps);return result;}catch (SQLException ex) {// Release Connection early, to avoid potential connection pool deadlock// in the case when the exception translator hasn't been initialized yet.if (psc instanceof ParameterDisposer) {((ParameterDisposer) psc).cleanupParameters();}String sql = getSql(psc);psc = null;JdbcUtils.closeStatement(ps);ps = null;DataSourceUtils.releaseConnection(con, getDataSource());con = null;throw translateException("PreparedStatementCallback", sql, ex);}finally {if (psc instanceof ParameterDisposer) {((ParameterDisposer) psc).cleanupParameters();}JdbcUtils.closeStatement(ps);DataSourceUtils.releaseConnection(con, getDataSource());}}
根据之前的结论,T result = action.doInPreparedStatement(ps);这行代码之前是不会发生阻塞的。整个 excecute 方法就像是一个模板方法,我们不关心加载驱动、创建数据库连接、创建 statement、关闭连接、关闭 statement,我们想要的是执行查询,并给我们返回结果集。
