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

Posted 落叶随风

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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方法里就已经设置了。请读者们自行查看。

 1  public Builder(Configuration configuration, String id, SqlSource sqlSource, SqlCommandType sqlCommandType) {
 2       mappedStatement.configuration = configuration;
 3       mappedStatement.id = id;
 4       mappedStatement.sqlSource = sqlSource;
 5       mappedStatement.statementType = StatementType.PREPARED;
 6       mappedStatement.parameterMap = new ParameterMap.Builder(configuration, "defaultParameterMap", null, new ArrayList<ParameterMapping>()).build();
 7       mappedStatement.resultMaps = new ArrayList<ResultMap>();
 8       mappedStatement.sqlCommandType = sqlCommandType;
 9       mappedStatement.keyGenerator = configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType) ? new Jdbc3KeyGenerator() : new NoKeyGenerator();
10       String logId = id;
11       if (configuration.getLogPrefix() != null) {
12         logId = configuration.getLogPrefix() + id;
13       }
14       mappedStatement.statementLog = LogFactory.getLog(logId);
15       mappedStatement.lang = configuration.getDefaultScriptingLanuageInstance();
16     }

如果实在不想用默认的方式进行处理的话,可以在相关每一个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方法。

 1 public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
 2     Statement stmt = null;
 3     try {
 4       Configuration configuration = ms.getConfiguration();
 5       StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
 6       stmt = prepareStatement(handler, ms.getStatementLog());
 7       return handler.<E>query(stmt, resultHandler) ; 
 8     } finally {
 9       closeStatement(stmt);
10     }
11   }

源码的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类)里面。

 1  public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
 2     ErrorContext.instance().sql(boundSql.getSql());
 3     Statement statement = null;
 4     try {
 5       statement = instantiateStatement(connection);
 6       setStatementTimeout(statement, transactionTimeout);
 7       setFetchSize(statement);
 8       return statement;
 9     } catch (SQLException e) {
10       closeStatement(statement);
11       throw e;
12     } catch (Exception e) {
13       closeStatement(statement);
14       throw new ExecutorException("Error preparing statement.  Cause: " + e, e);
15     }
16   }

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

 protected abstract Statement instantiateStatement(Connection connection) throws SQLException;

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

 1  protected Statement instantiateStatement(Connection connection) throws SQLException {
 2     String sql = boundSql.getSql();
 3     if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) {
 4       String[] keyColumnNames = mappedStatement.getKeyColumns();
 5       if (keyColumnNames == null) {
 6         return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);
 7       } else {
 8         return connection.prepareStatement(sql, keyColumnNames);
 9       }
10     } else if (mappedStatement.getResultSetType() != null) {
11       return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY);
12     } else {
13       return connection.prepareStatement(sql);
14     }
15   }

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

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

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

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

 1  public void setParameters(PreparedStatement ps) {
 2     ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
 3     List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
 4     if (parameterMappings != null) {
 5       for (int i = 0; i < parameterMappings.size(); i++) {
 6         ParameterMapping parameterMapping = parameterMappings.get(i);
 7         if (parameterMapping.getMode() != ParameterMode.OUT) {
 8           Object value;
 9           String propertyName = parameterMapping.getProperty();
10           if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
11             value = boundSql.getAdditionalParameter(propertyName);
12           } else if (parameterObject == null) {
13             value = null;
14           } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
15             value = parameterObject;
16           } else {
17             MetaObject metaObject = configuration.newMetaObject(parameterObject);
18             value = metaObject.getValue(propertyName);
19           }
20           TypeHandler typeHandler = parameterMapping.getTypeHandler();
21           JdbcType jdbcType = parameterMapping.getJdbcType();
22           if (value == null && jdbcType == null) {
23             jdbcType = configuration.getJdbcTypeForNull();
24           }
25           try {
26             typeHandler.setParameter(ps, i + 1, value, jdbcType);
27           } catch (TypeException e) {
28             throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
29           } catch (SQLException e) {
30             throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
31           }
32         }
33       }
34     }

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接口的源码也就是成了下一个目标了。

 

以上是关于MyBatis 源码分析——生成Statement接口实例的主要内容,如果未能解决你的问题,请参考以下文章

mybatis-加载statement配置2

Mybatis源码分析之结果封装ResultSetHandler和DefaultResultSetHandler

Mybatis源码阅读之--本地(一级)缓存实现原理分析

Mybatis常用工具类-SQL类

Mybatis中selectKey源码分析

mybatis 源码分析二