在平时的工作中,很少会听到回调这个词语。但是在自己不断地学习当中,发现很多地方用到了回调,比如 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() { // 匿名类
@Override
public 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 代码比较多,我只摘抄了部分相关代码,贴到了下面。
@Nullable
public <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
@Nullable
public 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
@Nullable
public <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,我们想要的是执行查询,并给我们返回结果集。