MyBatis 源碼分析——類型處理器


官網上面講到:無論是 MyBatis 在預處理語句(PreparedStatement)中設置一個參數時,還是從結果集中取出一個值時, 都會用類型處理器將獲取的值以合適的方式轉換成 Java 類型。那么為什么會有類型處理器呢?這一點並不難理解,SQL語句事實上可以理解為一門面向數據庫的編程語言。所以相對而言都有自己的數據類型。這也就意味着存在數據類型不一至的問題。同時不同的數據庫之間數據類型還有一定的差義。類型處理器則就是用於處理數據類型不一至的問題。

筆者看過幾個不同的ORM框架都存在着類型處理器的概念。可見類型處理器在ORM框架上實現有多么重要。在官網上面已經列出了20多種的內部類型處理器。筆者建議最好選擇性的看。好比如說筆者當前的列子里面用到的一個叫UnknownTypeHandler的類型處理器。

List<Product> products = dao.SelectProducts(30, "a");
<select id="SelectProducts" resultMap="result" >
        select * from Products where #{0} > ProductID and ProductName like #{1}
</select>

例子里面並沒有指出是什么樣子的JAVA類型。所以當然是UnknownTypeHandler類型了。即然這樣子我們不煩設置一下他的類型在來看看。我們只要把上面的配置修改一下就可以了。如下紅色標記。

<select id="SelectProducts" resultMap="result" >
        select * from Products where #{0,javaType=int,jdbcType=NUMERIC} > ProductID and ProductName like #{1,javaType=String,jdbcType=VARCHAR}
</select>

關於#{}的語法問題,相信筆者不用多講大家都清楚。從上面簡單的設置里,我們可以看到源碼里面會找到IntegerTypeHandler和StringTypeHandler類型處理器來設置參數。類型處理器都是實現於TypeHandler接口,源碼都存放在org.apache.ibatis.type的命名空間下。TypeHandler接口的源碼也很簡單。我們可以看到setParameter方法。顧名思義他就是用於設置參數。其他的方法都是用於處理回返的結果。如下。

public interface TypeHandler<T> {

  void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;

  T getResult(ResultSet rs, String columnName) throws SQLException;

  T getResult(ResultSet rs, int columnIndex) throws SQLException;

  T getResult(CallableStatement cs, int columnIndex) throws SQLException;

}

想要了解源碼,就要找一個合適的入切類——筆者選擇IntegerTypeHandler類。IntegerTypeHandler類並沒有直接的實現了TypeHandler接口。而是繼承了BaseTypeHandler類。正如上面所講的,BaseTypeHandler類的setParameter方法我們必須了解一下。如下

 1 public void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
 2     if (parameter == null) {
 3       if (jdbcType == null) {
 4         throw new TypeException("JDBC requires that the JdbcType must be specified for all nullable parameters.");
 5       }
 6       try {
 7         ps.setNull(i, jdbcType.TYPE_CODE);
 8       } catch (SQLException e) {
 9         throw new TypeException("Error setting null for parameter #" + i + " with JdbcType " + jdbcType + " . " +
10                 "Try setting a different JdbcType for this parameter or a different jdbcTypeForNull configuration property. " +
11                 "Cause: " + e, e);
12       }
13     } else {
14       try {
15         setNonNullParameter(ps, i, parameter, jdbcType);
16       } catch (Exception e) {
17         throw new TypeException("Error setting non null for parameter #" + i + " with JdbcType " + jdbcType + " . " +
18                 "Try setting a different JdbcType for this parameter or a different configuration property. " +
19                 "Cause: " + e, e);
20       }
21     }
22   }

源碼的意思很簡單——把參數分為非空值和空值來分別設置。空值的設置方法是JDBC內部自己的。而非空值的方法是mybatis自己封裝的。所以我們可以看到setNonNullParameter方法是一個抽象方法。看樣子我們可以猜測的出來——真正的實現是放在子類的setNonNullParameter方法里面。

 public abstract void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;

來看一下IntegerTypeHandler類的setNonNullParameter方法。

  public void setNonNullParameter(PreparedStatement ps, int i, Integer parameter, JdbcType jdbcType)
      throws SQLException {
    ps.setInt(i, parameter);
  }

事實上筆者認為IntegerTypeHandler類沒有什么可看的。但是UnknownTypeHandler類的處理卻得我們去研究。我們可以先想一下——不知道類型的情況下,如何知道需要的類型處理器。

 public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType)
      throws SQLException {
    TypeHandler handler = resolveTypeHandler(parameter, jdbcType);
    handler.setParameter(ps, i, parameter, jdbcType);
  }

不難看出他又根據參數值和JDBC數據類型來判斷獲得相應的類型處理器。然后在進行參數設值。所以我們的目標移向resolveTypeHandler方法。

 1   private TypeHandler<? extends Object> resolveTypeHandler(Object parameter, JdbcType jdbcType) {
 2     TypeHandler<? extends Object> handler;
 3     if (parameter == null) {
 4       handler = OBJECT_TYPE_HANDLER;
 5     } else {
 6       handler = typeHandlerRegistry.getTypeHandler(parameter.getClass(), jdbcType);
 7       // check if handler is null (issue #270)
 8       if (handler == null || handler instanceof UnknownTypeHandler) {
 9         handler = OBJECT_TYPE_HANDLER;
10       }
11     }
12     return handler;
13   }

源碼的意思是判斷傳入的參數值是不是為空的,空則調用ObjectTypeHandler類型處器。如果不為空,根據參數傳入的類型和當前JDBC數據類型來獲得相應的類型處理器。如果還是找不到或是還是UnknownTypeHandler類,則調用ObjectTypeHandler類型處器。UnknownTypeHandler類實際沒有設置參數值的功能。而是由其他的類型處理器來實現。

好了,到了這里面對於類型處理器,相信大家都有了一定的理解。但是這里還有一個問題值得我們去研究一下——類型處理器是什么時候創建的。想要了解他的來源就必須回頭去看一下DefaultParameterHandler類的setParameters方法。也就是上一章的結尾處相同的代碼。

 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類上一章筆者也有講到。他是用於存放sql語句信息。為什么這里要提到他呢?相信從源碼里面我們可以看到類型處理器是通過ParameterMapping類來獲得的。而ParameterMapping類又是通過 BoundSql類獲得。所以就不得不去了解一下BoundSql類是如何獲得ParameterMapping類的。

關於如何BoundSql類是如何獲得ParameterMapping類的。事實這一塊跟XMLMapperBuilder類有關系。XMLMapperBuilder類就是用於加載Mapper文件的。而他的configurationElement方法。正是我們現在需要了解的。如下

 private void configurationElement(XNode context) {
    try {
      String namespace = context.getStringAttribute("namespace");
      if (namespace == null || namespace.equals("")) {
        throw new BuilderException("Mapper's namespace cannot be empty");
      }
      builderAssistant.setCurrentNamespace(namespace);
      cacheRefElement(context.evalNode("cache-ref"));
      cacheElement(context.evalNode("cache"));
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      sqlElement(context.evalNodes("/mapper/sql"));
      buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
    }
  }

最后面的buildStatementFromContext方法是了解如何獲得ParameterMapping類的開頭。這句代碼中已經很明顯的看出他是用於加載select,insert等節點信息。當我們繼續跟進去的話,最終會看到一個了parseStatementNode方法。這一個過程讀者們自行查看。

1  processSelectKeyNodes(id, parameterTypeClass, langDriver);
2     
3     // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
4     SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
5     String resultSets = context.getStringAttribute("resultSets");
6     String keyProperty = context.getStringAttribute("keyProperty");
7     String keyColumn = context.getStringAttribute("keyColumn");

筆者只是貼出一部分的代碼。紅色標記的內容是我們所要關注的重點。(其外parseStatementNode方法還是了解如何創建MappedStatement類的入口類。)筆者為什么要提到SqlSource接口呢?我們都知道在mybatis框架里面的sql語句決不可能只有sql語句。還會出現if之類的語法。那么這種sql語句,mybatis框架叫他為動態 SQL。而BoundSql類就是通過SqlSource接口所繼承的類獲得。

mybatis對sql語句定義為三種半。為什么是三種半呢?因為還有一個可能正在研發中。每一種在源碼中都有相關的類。

1.DynamicSqlSource類用於動態SQL。

2.StaticSqlSource類用於靜態SQL。

3.ProviderSqlSource類用於腳本語言。

4.VelocitySqlSource類用於XXX。還是讀者們自己去看吧。

SqlSource接口的源碼才是筆者為什么要講SqlSource接口的最好證明。如下

public interface SqlSource {

  BoundSql getBoundSql(Object parameterObject);

}

我們搞清楚了BoundSql類是哪里來了之后。我們在去了解如何獲得ParameterMapping類就變得簡單多了。這里筆者就不把全部的過程拿出來講了。就直接把獲得ParameterMapping類的始點提出來——SqlSourceBuilder類的parse方法里面。

public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
    ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
    GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
    String sql = parser.parse(originalSql);
    return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
  }

ParameterMappingTokenHandler類就是用於解析ParameterMapping類的。具體如何去解析不用筆者在動手吧。在建新ParameterMapping類的時候,他的內部resolveTypeHandler方法會被調用。而這個時候我們的類型處理器正式出現了。

 private void resolveTypeHandler() {
      if (parameterMapping.typeHandler == null && parameterMapping.javaType != null) {
        Configuration configuration = parameterMapping.configuration;
        TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
        parameterMapping.typeHandler = typeHandlerRegistry.getTypeHandler(parameterMapping.javaType, parameterMapping.jdbcType);
      }
    }

類型處理器相關的內容。到了這里可以說是結束了。我們知道了他的來源,也知道了他的去處。事實如果我們認真點去了解正個過程。你們會發現這個過程用於很buidler設計模式。同時mybatis框架把mapper文件的信息分為mapper根處理、四節點處理、腳本處理、SQL語句處理。

1.Mapper根處理對應的XMLMapperBuilder類。

2.四節點處理對應的XMLStatementBuilder類。即是select節點,update節點,insert節點,delect節點。

3.腳本處理對應的XMLScriptBuilder類。

4.SQL語句處理對應的SqlSourceBuilder類。

當然這也是筆者自己的看法而以。不一定對。你們可以自己去了解。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM