`

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开关打开即可.

4
1
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics