MyBatis 源码分析——生成Statement接口实例

时间:2023-03-10 04:49:30
MyBatis 源码分析——生成Statement接口实例

JDBC的知识对于JAVA开发人员来讲在简单不过的知识了。PreparedStatement的作用更是胸有成竹。我们最常见用到有俩个方法:executeQuery方法和executeUpdate方法。这俩个方法之外还有一个execute方法。只是这个方法我们很少用。但是mybatis框架就是却用这个方法来实现的。不管mybatis用是哪一个方法来实现。有一点可以肯定——那就是必须得到Statement接口实例。你可以这样子理解mybatis把如何获得Statement接口实例做了一个完美的封装。而这一个封装就是上一章出现的StatementHandler接口。

mybatis里面实现StatementHandler接口有四个类。

RoutingStatementHandler类:笔者把它理解为下面三个类的代理类。

CallableStatementHandler类:对应处理JDBC里面的CallableStatement类。

PreparedStatementHandler类:对应处理JDBC里面的PreparedStatement类。

SimpleStatementHandler类:对应处理JDBC里面的一般Statement接口实例(笔者也不知道JDBC是需叫他什么)。

正如上面所讲的笔者把RoutingStatementHandler类理解为三个类的代理类。mybatis并没有直接去引用后面三个类。而是通过RoutingStatementHandler类来判断当前到底要调用哪个类。再去执行相关的Statement接口实例。

 public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler;
}

这一段源码就是前一章尾部源码的后继执行。源码的意图就是新建一个RoutingStatementHandler类实例。关键的点是在RoutingStatementHandle类的构造函数里面。

 public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {

    switch (ms.getStatementType()) {
case STATEMENT:
delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
case PREPARED:
delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
case CALLABLE:
delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
default:
throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
} }

从这里就可以看出笔者为什么说RoutingStatementHandler类可以理解为三个类的代理类。事实上所有的工作都是内部成员delegate来做的。而delegate又是在构造函数里面进行判断生成的。看样子在这里JDBC的三种操作方式完美的体现出来。通过MappedStatement的getStatementType方法得到相应返回值,判断当前SQL语句是要用哪一种操作方式来进行。默认情况下是用Prepared方式。当前笔者不是瞎说的。在MappedStatement的Builder方法里就已经设置了。请读者们自行查看。

  public Builder(Configuration configuration, String id, SqlSource sqlSource, SqlCommandType sqlCommandType) {
mappedStatement.configuration = configuration;
mappedStatement.id = id;
mappedStatement.sqlSource = sqlSource;
mappedStatement.statementType = StatementType.PREPARED;
mappedStatement.parameterMap = new ParameterMap.Builder(configuration, "defaultParameterMap", null, new ArrayList<ParameterMapping>()).build();
mappedStatement.resultMaps = new ArrayList<ResultMap>();
mappedStatement.sqlCommandType = sqlCommandType;
mappedStatement.keyGenerator = configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType) ? new Jdbc3KeyGenerator() : new NoKeyGenerator();
String logId = id;
if (configuration.getLogPrefix() != null) {
logId = configuration.getLogPrefix() + id;
}
mappedStatement.statementLog = LogFactory.getLog(logId);
mappedStatement.lang = configuration.getDefaultScriptingLanuageInstance();
}

如果实在不想用默认的方式进行处理的话,可以在相关每一个XML节点的statementType属性进行设置。如下

<select id="SelectProducts" resultMap="result" statementType="STATEMENT" >
select * from Products where #{0} > ProductID and ProductName like #{1}
</select>

生成Statement接口实例要用到StatementHandler接口的俩个方法:prepare方法和parameterize方法。prepare方法用于完成构建Statement接口实例。parameterize方法用于处理Statement接口实例对应的参数。理解这一过程需要调头查看SimpleExecutor类的doQuery方法。

 public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.<E>query(stmt, resultHandler) ;
} finally {
closeStatement(stmt);
}
}

源码的prepareStatement方法里面可以体现prepare方法和parameterize方法的作用。通过prepareStatement方法就可以得到一个完整Statement接口实例。最后在通过StatementHandler接口实例的query方法来获得对应的结果。笔者暂且跳过这一个过程(query方法处理结果)。让我们来看看关于prepare方法和parameterize方法。

 private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
Connection connection = getConnection(statementLog);
stmt = handler.prepare(connection, transaction.getTimeout());
handler.parameterize(stmt);
return stmt;
}

上面说到prepare方法就是用于构建Statement接口实例。默认情况是PreparedStatementHandler类。那么笔者就拿PreparedStatementHandler类来切入吧。当笔者点开PreparedStatementHandler类的源码,试着去查看一下prepare方法。发现找不到。原来他在PreparedStatementHandler类的父类(BaseStatementHandler类)里面。

  public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
ErrorContext.instance().sql(boundSql.getSql());
Statement statement = null;
try {
statement = instantiateStatement(connection);
setStatementTimeout(statement, transactionTimeout);
setFetchSize(statement);
return statement;
} catch (SQLException e) {
closeStatement(statement);
throw e;
} catch (Exception e) {
closeStatement(statement);
throw new ExecutorException("Error preparing statement. Cause: " + e, e);
}
}

每一个框架都有一个共同的特点——方法调来调去的。prepare方法里面通过instantiateStatement方法来返回相关的Statement实例。而这个方法却是一个抽象方法。

 protected abstract Statement instantiateStatement(Connection connection) throws SQLException;

他的实例就是在各自的子类里面。完美的利用了继承的好处。

  protected Statement instantiateStatement(Connection connection) throws SQLException {
String sql = boundSql.getSql();
if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) {
String[] keyColumnNames = mappedStatement.getKeyColumns();
if (keyColumnNames == null) {
return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);
} else {
return connection.prepareStatement(sql, keyColumnNames);
}
} else if (mappedStatement.getResultSetType() != null) {
return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY);
} else {
return connection.prepareStatement(sql);
}
}

上面的源码是PreparedStatementHandler类的。所以不用笔者多讲——就是生成PreparedStatement实例。

有了PreparedStatement实例,当然就要对他进行设置相应的参数。这也是parameterize方法的作用。但是如何是简单的设置那显然没有什么可说的。主要还是因为mybatis对于设置参数方面做精心的设计。好话不多说。还是看一下源码最实在。

public void parameterize(Statement statement) throws SQLException {
parameterHandler.setParameters((PreparedStatement) statement);
}

ParameterHandler接口的作用显然不用笔者多讲。DefaultParameterHandler类便是他的实例类。DefaultParameterHandler类的代码不多,可是他包含的内容却很多。进去看一下就知道了。

  public void setParameters(PreparedStatement ps) {
ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings != null) {
for (int i = 0; i < parameterMappings.size(); i++) {
ParameterMapping parameterMapping = parameterMappings.get(i);
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
TypeHandler typeHandler = parameterMapping.getTypeHandler();
JdbcType jdbcType = parameterMapping.getJdbcType();
if (value == null && jdbcType == null) {
jdbcType = configuration.getJdbcTypeForNull();
}
try {
typeHandler.setParameter(ps, i + 1, value, jdbcType);
} catch (TypeException e) {
throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
} catch (SQLException e) {
throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
}
}
}
}

BoundSql类又一次出现在我们的面前,前面笔者也没有提过关于BoundSql类的作用。因为如果没有一个上下文的作用是很难推断出BoundSql类。笔者也只是从源码来看的,也不一定是对的。前面部分的源码里面有出现过使用MappedStatement类。他可以说是一个包含节点(select节点,update节点等)信息的类。但是对的具体的SQL语句用到的信息却很少。那么BoundSql类就是存放于的组装SQL句语信息。从源码里面我们可以看到BoundSql类处理返回结果的信息却没有。有的只是SQL语句的参数之类的信息。如下他的内部成员。

 private String sql;
private List<ParameterMapping> parameterMappings;
private Object parameterObject;
private Map<String, Object> additionalParameters;
private MetaObject metaParameters;

有了对BoundSql类的概念认识,我们接着谈谈上面源码(setParameters方法部分)里面发生的事情吧。如果想要一下就明白他是做什么的怎么样子做。那笔者只能说自己功力不行。笔者只能大概的看出他在做什么。通过BoundSql类获得相应的ParameterMapping类。找到对应的属性名(如:#{id})。接着通过传入的参数信息获得对应的MetaObject类。在通过MetaObject类和属性名获得相应属性名的值。最后一步就是通过TypeHandler接口实例设置值了。

到了这里面StatementHandler接口的工作算是结束了。对于MetaObject类是如何获得的,他又是什么。笔者这里就不多加言论。笔者留意的点还是TypeHandler接口。这部分的知识点官网也讲到过——typeHanlders。了解TypeHandler接口的源码也就是成了下一个目标了。