spring + ibatis 源码解读

最近对ibatis2.1.0.256这个版本(大部分应用都是用该版本)的源代码以及spring的SqlMapClientTemplate相关的代码看了下. 这个版本的ibatis有一个问题, 就是如果执行sql出错的话, 出错信息非常不友好, 连最基本的sql和paramter都没有, 给查找问题带来不小的麻烦, 本想看看代码能进行一下定制, 由于版本太老, 扩展性太差了, 基本没法扩展, 最后被迫放弃.

题外话, 现在ibatis都已经出到3.0了(为什么我们还不升级?)

发现spring和ibatis的代码风格相差还是很大的. 两相比较就可以看出差距来, 同时也对di有了进一步的理解.

在spring中几乎将所有的类都当一个个bean来看待, 因此所有的依赖要么是通过构造函数注入, 要么通过setter方式注入. 而ibatis则明显没有这样的概念, 很多地方对其他类的依赖都是直接在构造函数中new出来的, 这样给定制带来了麻烦, 可以说, 基本上是没法对ibatis进行扩展. 因此我们可以认为ibatis是一个非常封闭的dao框架(当然后续版本的ibatis有所改善).

基于ibatis这样的架构, 导致spring对ibatis的扩展也非常有限.其中最重要的类在我看来无非两个:SqlMapClientFactoryBean, SqlMapClientTemplate, 前者是一个典型的FactoryBean, 用来将ibatis纳入spring的ioc容器中管理. 这里我们可以借鉴一下spring自己的FactoryBean的做法. 一般的FactoryBean都要实现FactoryBean接口, 在getObject()方法中, 我们可以去具体实现到底要怎么创建所需要的bean, 当然简单的可以直接new, 一般情况下还需要借助其他的接口来获取所需要的bean, 比如这里还实现了InitializingBean接口, 即在bean的定义以及相关的依赖设置完毕之后, 调用了afterPropertiesSet()方法,此时便开始根据配置文件来构造SqlMapClient了, 在根据配置构造SqlMapClient的部分spring调用了ibatis的SqlMapClientBuilder这个构造器来处理的, 从这里开始, ibatis便关闭了我们灵活处理构造SqlMapClient的大门. 对内部的构造过程我们几乎完全无法控制, 如果希望有所改善, 必须复写大量的类.

SqlMapClientTemplate用来对ibatis的SqlMapClient进行包装, 以便在调用ibatis访问数据库的时候, 做一些手脚, 不过这里这里主要是为了将iBatis抛出的SqlException转换成spring统一的DataAccessException异常, 处理异常的CRUD统一入口代码如下:


public Object execute(SqlMapClientCallback action) throws DataAccessException {
SqlMapSession session = this.sqlMapClient.openSession();
Connection ibatisCon = null;
try {
Connection springCon = null;
try {
....
return action.doInSqlMapClient(session);
}
catch (SQLException ex) {
throw getExceptionTranslator().translate("SqlMapClient operation", null, ex);
}
finally {
DataSourceUtils.releaseConnection(springCon, getDataSource());
}
}
finally {
if (ibatisCon == null) {
session.close();
}
}
}

因此如果你不喜欢spring的exception处理方式, 也可以不用它. 直接用SqlMapClient即可. spring之所以能将所有的操作归结到一个入口, 这个与SqlMapClientCallback接口的使用有很大的关系. 这里我们可以见到spring里面典型的接口->匿名类的用法.spring对匿名类用的那可是相当的多, 这个也是我们值得借鉴的一个地方, 不过匿名类不可滥用, 一般来说匿名类都是很简单的调用(10行左右), 如果有大段的代码调用, 那么匿名类就不合适了, 这里基本都很简单, 都是一个简单的转发, 即将对SqlMapClientTemplate的调用转发给SqlMapClient, 比如下面的代码:

public Object queryForObject(final String statementName, final Object parameterObject)

throws DataAccessException {

return execute(new SqlMapClientCallback() {

public Object doInSqlMapClient(SqlMapExecutor executor) throws SQLException {

return executor.queryForObject(statementName, parameterObject);

}

});

}


另外这里需要说一下的是, SqlMapClient是ibatis用来访问数据库的一个入口, 即一个SqlMapClient表示一个数据库. 所有的CRUD操作都要经过这个接口.

下面我们就来历数ibatis的种种让人崩溃的做法.

ibatis主要做了两件事, 一个就是解析sqlmap配置文件, 就是我们配置的一个sql语句和javabean与表字段之间的映射关系, 这里我们不做多展开.另外一个就是调用jdbc驱动, 执行数据库操作.

这里我们先从解析开始.ibatis的解析入口是SqlMapClientBuilder类. 这个builder类将解析的过程封装到了SqlMapConfigParser中.代码如下:
  public static SqlMapClient buildSqlMapClient(Reader reader) {

// return new XmlSqlMapClientBuilder().buildSqlMap(reader);

return new SqlMapConfigParser().parse(reader);

}


万恶的new出现了, 这里我们已经没法对parser进行控制了.

接着我们来看看SqlMapConfigParser构造器:
  public SqlMapConfigParser() {

this(null, null);

}
public SqlMapConfigParser(XmlConverter sqlMapConfigConv, XmlConverter sqlMapConv) {

super(new Variables());

parser.setValidation(true);

parser.setEntityResolver(new SqlMapClasspathEntityResolver());
vars.sqlMapConfigConv = sqlMapConfigConv;

vars.sqlMapConv = sqlMapConv;
vars.delegate = new SqlMapExecutorDelegate();

vars.typeHandlerFactory = vars.delegate.getTypeHandlerFactory();

vars.client = new SqlMapClientImpl(vars.delegate);
...

}


又是大量的new操作, 再一次无语了.

而且只有get方法, 没有set方法, 再一次关闭了注入依赖的可能.

在SqlMapConfigParser中有两个类需要注意, 即SqlMapExecutorDelegate和SqlMapClientImpl, SqlMapExecutorDelegate这个封装了从配置文件解析的一些东东, SqlMapClientImpl所需要的一些配置就是从SqlMapExecutorDelegate获取的, 因此SqlMapClientImpl注入了SqlMapExecutorDelegate, 并在需要的时候转调SqlMapExecutorDelegate, SqlMapClientImpl因为是操作数据库的入口, 还需要负责管理事务, session等, 里面有一个很重要的东东:SqlExecutor, 这个也是在SqlMapExecutorDelegate中实现的.如同名字一样, 它是用来执行sql语句的.这个似乎是我们用来控制底层数据库操作的一个入口, 如果能进行配置将是一件幸福的事儿.它是在SqlMapExecutorDelegate中创建的, 很遗憾, 也是在构造函数中new出来的:
  public SqlMapExecutorDelegate() {

mappedStatements = new HashMap();

cacheModels = new HashMap();

resultMaps = new HashMap();

parameterMaps = new HashMap();
requestPool = new ThrottledPool(RequestScope.class, DEFAULT_MAX_REQUESTS);

sessionPool = new ThrottledPool(SessionScope.class, DEFAULT_MAX_SESSIONS);
sqlExecutor = new SqlExecutor();

typeHandlerFactory = new TypeHandlerFactory();

dataExchangeFactory = new DataExchangeFactory(typeHandlerFactory);

}


SqlExecutor是一个具体类, 而且没有setter方法, 再一次崩溃.

看到这里, 我们几乎完全失去了对ibatis的底层操作进行控制的可能, 我们继续看SqlExecutor是如何被使用的.

所有的sql语句的执行都是封装在ibatis的一个个MappedStatement中, 大部分的逻辑都是放在GeneralStatement中, 这里我们拿出查询的处理代码:
  protected void executeQueryWithCallback(RequestScope request, Connection conn, Object parameterObject, Object resultObject, RowHandler rowHandler, int skipResults, int maxResults)

throws SQLException {

ErrorContext errorContext = request.getErrorContext();

errorContext.setActivity("preparing the mapped statement for execution");

errorContext.setObjectId(this.getId());

errorContext.setResource(this.getResource());
try {

parameterObject = validateParameter(parameterObject);
Sql sql = getSql();
errorContext.setMoreInfo("Check the parameter map.");

ParameterMap parameterMap = sql.getParameterMap(request, parameterObject);
errorContext.setMoreInfo("Check the result map.");

ResultMap resultMap = sql.getResultMap(request, parameterObject);
request.setResultMap(resultMap);

request.setParameterMap(parameterMap);
errorContext.setMoreInfo("Check the parameter map.");

Object[] parameters = parameterMap.getParameterObjectValues(request, parameterObject);
errorContext.setMoreInfo("Check the SQL statement.");

String sqlString = sql.getSql(request, parameterObject);
errorContext.setActivity("executing mapped statement");

errorContext.setMoreInfo("Check the SQL statement or the result map.");

RowHandlerCallback callback = new RowHandlerCallback(resultMap, resultObject, rowHandler);

sqlExecuteQuery(request, conn, sqlString, parameters, skipResults, maxResults, callback);
errorContext.setMoreInfo("Check the output parameters.");

if (parameterObject != null) {

postProcessParameterObject(request, parameterObject, parameters);

}
errorContext.reset();

sql.cleanup(request);

notifyListeners();

} catch (SQLException e) {

errorContext.setCause(e);

throw new NestedSQLException(errorContext.toString(), e.getSQLState(), e.getErrorCode(), e);

} catch (Exception e) {

errorContext.setCause(e);

throw new NestedSQLException(errorContext.toString(), e);

}

}
protected void sqlExecuteQuery(RequestScope request, Connection conn, String sqlString, Object[] parameters, int skipResults, int maxResults, RowHandlerCallback callback) throws SQLException {

getSqlExecutor().executeQuery(request, conn, sqlString, parameters, skipResults, maxResults, callback);

}



在这段代码中, 有sql语句, 参数的校验, 另外就是调用sqlExecutor执行了, 基本上很简单, 这里不得不暴露一个ibatis对异常处理非常糟糕的做法, 就是隐藏信息中,没有包含出错的sql语句和对应的参数, 我们仅仅只能知道哪个sqlmap文件配置的哪个sql有问题, 但是不知道最终的sql是个什么样子. 而我们实际开发过程中很大一部分都是因为bad sql导致的. 不能呈现bad sql 自然给解决问题带来不小的难度, 我曾经想了不少问题都没法解决.

另外, 对于在日志中输入正常的sql, 参数, 执行结果的做法, 这里ibatis用到了动态代理.比如这个PreparedStatementLogProxy类:
  public Object invoke(Object proxy, Method method, Object[] params) throws Throwable {
try {
if (EXECUTE_METHODS.contains(method.getName())) {
if (log.isDebugEnabled()) {
log.debug("{pstm-" + id + "} PreparedStatement: " + removeBreakingWhitespace(sql));
log.debug("{pstm-" + id + "} Parameters: " + getValueString());
log.debug("{pstm-" + id + "} Types: " + getTypeString());
}
clearColumnInfo();
if ("executeQuery".equals(method.getName())) {
ResultSet rs = (ResultSet) method.invoke(statement, params);
return ResultSetLogProxy.newInstance(rs);
} else {
return method.invoke(statement, params);
}
} else if (SET_METHODS.contains(method.getName())) {
if ("setNull".equals(method.getName())) {
setColumn(params[0], null);
} else {
setColumn(params[0], params[1]);
}
return method.invoke(statement, params);
} else if ("getResultSet".equals(method.getName())) {
ResultSet rs = (ResultSet) method.invoke(statement, params);
return ResultSetLogProxy.newInstance(rs);
} else {
return method.invoke(statement, params);
}
} catch (Throwable t) {
throw ClassInfo.unwrapThrowable(t);
}
}

它的log很特殊是跟java.sql的一些类相关的:
  private static final Log log = LogFactory.getLog(PreparedStatement.class);

因此很多人在问如何打印日志, 实际上就是要将java.sql这个package下相关类的DEBUG开关打开即可.
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值