前言
模式模式在Spring中的应用较多,这里结合JdbcTemplate的源码来和大家一起学习下,更加深刻滴认识下模版模式,以便在日常开发中,能灵活运用模版模式,来减少重复代码,增强代码的可拓展性。
何为模版模式
经典模板方法定义:
父类定义了骨架(调用哪些方法及顺序),某些特定方法由子类实现。最大的好处:代码复用,减少重复代码。除了子类要实现的特定方法,其他方法及方法调用顺序都在父类中预先写好了。所以父类模板方法中有两类方法:
共同的方法: 所有子类都会用到的代码
不同的方法: 子类要覆盖的方法,分为两种:
- 抽象方法:父类中的是抽象方法,子类必须覆盖
- 钩子方法:父类中是一个空方法,子类继承了默认也是空的
注:为什么叫钩子,子类可以通过这个钩子(方法),控制父类,因为这个钩子实际是父类的方法(空方法)!
public abstract class AbstractTemplate {
public void templateMethod() {
// 执行一些通用的操作
step1();
step2();
step3();
// 执行一些通用的操作
}
protected abstract void step1();
protected abstract void step2();
protected abstract void step3();
}
public class ConcreteTemplate extends AbstractTemplate {
protected void step1() {
// 实现具体的业务逻辑
}
protected void step2() {
// 实现具体的业务逻辑
}
protected void step3() {
// 实现具体的业务逻辑
}
}
public class Client {
public static void main(String[] args) {
AbstractTemplate template = new ConcreteTemplate();
template.templateMethod();
}
}
在上面的示例中,AbstractTemplate是一个模板类,定义了一个templateMethod()方法作为模板方法。该方法中包含了通用的操作,以及调用了三个抽象方法(step1()、step2()、step3())来完成具体的业务逻辑。ConcreteTemplate是一个具体的模板类,实现了三个抽象方法。在Client类中,我们实例化了ConcreteTemplate对象,并调用了templateMethod()方法,这样就完成了整个模板方法模式的使用。通过使用模板方法模式,在Spring中我们可以在不改变模板类的情况下,通过实现不同的回调方法来定制特定的业务逻辑,从而达到代码的复用和扩展的目的。
Spring是如何使用模版模式?
Spring模板方法模式实质:是模板方法模式和回调模式的结合,是Template Method不需要继承的另一种实现方式。Spring几乎所有的外接扩展都采用这种模式。我们先来看看JdbcTemplate的类图
在JdbcOperations接口中定义execute抽象方法
在JdbcTemplate中实现了execute方法
@Override
public <T> T execute(StatementCallback<T> action) throws DataAccessException {
Assert.notNull(action, "Callback object must not be null");
Connection con = DataSourceUtils.getConnection(getDataSource());
Statement stmt = null;
try {
Connection conToUse = con;
if (this.nativeJdbcExtractor != null &&
this.nativeJdbcExtractor.isNativeConnectionNecessaryForNativeStatements()) {
conToUse = this.nativeJdbcExtractor.getNativeConnection(con);
}
stmt = conToUse.createStatement();
applyStatementSettings(stmt);
Statement stmtToUse = stmt;
if (this.nativeJdbcExtractor != null) {
stmtToUse = this.nativeJdbcExtractor.getNativeStatement(stmt);
}
T result = action.doInStatement(stmtToUse);
handleWarnings(stmt);
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.
JdbcUtils.closeStatement(stmt);
stmt = null;
DataSourceUtils.releaseConnection(con, getDataSource());
con = null;
throw getExceptionTranslator().translate("StatementCallback", getSql(action), ex);
}
finally {
JdbcUtils.closeStatement(stmt);
DataSourceUtils.releaseConnection(con, getDataSource());
}
}
在上面的方法中,一些通用的建立链接,创建Statement对象,释放Statement对象,关闭链接代码都是一些固定的逻辑。实际有变化的逻辑都在T result = action.doInStatement(stmtToUse);中注意看这里的action,是一个回调对象,我们可以看下这个对象的定义
public interface StatementCallback<T> {
/**
* Gets called by {@code JdbcTemplate.execute} with an active JDBC
* Statement. Does not need to care about closing the Statement or the
* Connection, or about handling transactions: this will all be handled
* by Spring's JdbcTemplate.
* <p><b>NOTE:</b> Any ResultSets opened should be closed in finally blocks
* within the callback implementation. Spring will close the Statement
* object after the callback returned, but this does not necessarily imply
* that the ResultSet resources will be closed: the Statement objects might
* get pooled by the connection pool, with {@code close} calls only
* returning the object to the pool but not physically closing the resources.
* <p>If called without a thread-bound JDBC transaction (initiated by
* DataSourceTransactionManager), the code will simply get executed on the
* JDBC connection with its transactional semantics. If JdbcTemplate is
* configured to use a JTA-aware DataSource, the JDBC connection and thus
* the callback code will be transactional if a JTA transaction is active.
* <p>Allows for returning a result object created within the callback, i.e.
* a domain object or a collection of domain objects. Note that there's
* special support for single step actions: see JdbcTemplate.queryForObject etc.
* A thrown RuntimeException is treated as application exception, it gets
* propagated to the caller of the template.
* @param stmt active JDBC Statement
* @return a result object, or {@code null} if none
* @throws SQLException if thrown by a JDBC method, to be auto-converted
* to a DataAccessException by a SQLExceptionTranslator
* @throws DataAccessException in case of custom exceptions
* @see JdbcTemplate#queryForObject(String, Class)
* @see JdbcTemplate#queryForRowSet(String)
*/
T doInStatement(Statement stmt) throws SQLException, DataAccessException;
}
可以看下这里execute(StatementCallback<T> action)调用的地方,分别对应查询,更新和批量更新操作
我们到UpdateStatementCallback中去看下
@Override
public int update(final String sql) throws DataAccessException {
Assert.notNull(sql, "SQL must not be null");
if (logger.isDebugEnabled()) {
logger.debug("Executing SQL update [" + sql + "]");
}
class UpdateStatementCallback implements StatementCallback<Integer>, SqlProvider {
@Override
public Integer doInStatement(Statement stmt) throws SQLException {
int rows = stmt.executeUpdate(sql);
if (logger.isDebugEnabled()) {
logger.debug("SQL update affected " + rows + " rows");
}
return rows;
}
@Override
public String getSql() {
return sql;
}
}
return execute(new UpdateStatementCallback());
}
可以看到UpdateStatementCallback 实现了StatementCallback接口,将具体update操作逻辑放在了execute外层,这样就是将变化作为回调对象传入到execute方法中。可以到QueryStatementCallback 看下,亦是同理
@Override
public <T> T query(final String sql, final ResultSetExtractor<T> rse) throws DataAccessException {
Assert.notNull(sql, "SQL must not be null");
Assert.notNull(rse, "ResultSetExtractor must not be null");
if (logger.isDebugEnabled()) {
logger.debug("Executing SQL query [" + sql + "]");
}
class QueryStatementCallback implements StatementCallback<T>, SqlProvider {
@Override
public T doInStatement(Statement stmt) throws SQLException {
ResultSet rs = null;
try {
rs = stmt.executeQuery(sql);
ResultSet rsToUse = rs;
if (nativeJdbcExtractor != null) {
rsToUse = nativeJdbcExtractor.getNativeResultSet(rs);
}
return rse.extractData(rsToUse);
}
finally {
JdbcUtils.closeResultSet(rs);
}
}
@Override
public String getSql() {
return sql;
}
}
return execute(new QueryStatementCallback());
}
总结
为什么JdbcTemplate没有使用继承?因为这个类的方法太多,但是我们还是想用到JdbcTemplate已有的稳定的、公用的数据库连接,那么我们怎么办呢?
我们可以把变化的东西抽出来作为一个参数传入JdbcTemplate的方法中。但是变化的东西是一段代码,而且这段代码会用到JdbcTemplate中的变量。怎么办?那我们就用回调对象吧。在这个回调对象中定义一个操纵JdbcTemplate中变量的方法,我们去实现这个方法,就把变化的东西集中到这里了。然后我们再传入这个回调对象到JdbcTemplate,从而完成了调用。