Mybatis 代码流程及实现原理解析(三)

来源:互联网 发布:冠新软件洪继群 编辑:程序博客网 时间:2024/06/09 22:14

接上篇, 这篇继续分析XMLMapperBuilder.parse()里的configurationElement() 这个方法。

private void configurationElement(XNode context) {    try {      //mapper可以加namespace来避免重复情况      String namespace = context.getStringAttribute("namespace");      builderAssistant.setCurrentNamespace(namespace);      //该节点表示从其他名目空间引用缓存配置       cacheRefElement(context.evalNode("cache-ref"));      cacheElement(context.evalNode("cache"));//解析cache子节点.      //解析parameterMap 子节点,因为有多个并列的parameterMap节点,所以要加上路径,解析返回的列表      parameterMapElement(context.evalNodes("/mapper/parameterMap"));      //解析resultMap子节点,因为有多个并列的resultMap节点,所以要加上路径,解析返回的列表      resultMapElements(context.evalNodes("/mapper/resultMap"));      //解析sql子节点,因为有多个并列的sql节点,所以要加上路径,解析返回的列表      sqlElement(context.evalNodes("/mapper/sql"));      //解析select,insert,update,delete子节点      buildStatementFromContext(context.evalNodes("select|insert|update|delete"));    } catch (Exception e) {      throw new RuntimeException("Error parsing Mapper XML. Cause: " + e, e);    }  }

此方法逐步解析<mapper>的子节点。

子节点<cache-ref>

xml配置片段:

<cache-ref namespace="com.Book"/>

java代码解析:

private void cacheRefElement(XNode context) {    if (context != null) {     //首先用该节点的namespace替换cacherefmap中mapper文件的。      configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace"));      CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace"));      try {       //创建resolver,判断该namespace是否可用。      cacheRefResolver.resolveCacheRef();      } catch (IncompleteElementException e) {        //如果不可用,加入到为完成列表,在所有节点解析完成后,再统一做处理      configuration.addIncompleteCacheRef(cacheRefResolver);      }    }  }
CacheRefResolver.java的resolve方法

public Cache resolveCacheRef() {  return assistant.useCacheRef(cacheRefNamespace);}
MapperBuilderAssistant.java的useCacheRef方法:

public Cache useCacheRef(String namespace) {    if (namespace == null) {      throw new BuilderException("cache-ref element requires a namespace attribute.");    }    try {      unresolvedCacheRef = true;      Cache cache = configuration.getCache(namespace);      //如果当前configuration对象没有持有该namespace的Cache, 则抛出异常。      if (cache == null) {        throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.");      }      currentCache = cache;      unresolvedCacheRef = false;      return cache;    } catch (IllegalArgumentException e) {      throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.", e);    }  }

子节点<cache>

xml配置如下:

<mapper namespace="com.skoyou.domain.UserDAO"><cache type="com.domain.something.MyCustomCache"  eviction="FIFO"  flushInterval="60000"  size="512"  readOnly="tr"><property name="cacheFile" value="/tmp/my-custom-cache.tmp"/></cache>
type:配置自定义缓存或为其他第三方缓存方案 创建适配器来完全覆盖缓存行为。如果未指定的话, 使用“PERPETFUL”类型(PerpetualCache.java)。
eviction:缓存回收策略,默认值是“LRU”, 有如下几种策略可以指定:
LRU:最近最少使用的:移除最长时间不被使用的对象。
FIFO:先进先出:按对象进入缓存的顺序来移除它们。
SOFT:软引用:移除基于垃圾回收器状态和软引用规则的对象。
WEAK:弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象.
flushInterval:刷新间隔)可以被设置为任意的正整数,而且它们代表一个合理的毫秒 形式的时间段。默认情况是不设置,也就是没有刷新间隔,缓存仅仅调用语句时刷新。
size:(引用数目)可以被设置为任意正整数,要记住你缓存的对象数目和你运行环境的 可用内存资源数目。默认值是 1024。
readyOnly:(只读)属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓 存对象的相同实例。因此这些对象不能被修改。这提供了很重要的性能优势。可读写的缓存 会返回缓存对象的拷贝(通过序列化) 。这会慢一些,但是安全,因此默认是 false。
cache可以有个子节点<property> 来加载.

java方法解析:

private void cacheElement(XNode context) throws Exception {    if (context != null) {      //获取cache type的值,没有的话,使用默认PERPETUAL      String type = context.getStringAttribute("type", "PERPETUAL");      // 得到type类, 默认是PERPETUAL 对应的PerpetualCache      Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);      //获取eviction的值,没有的话,使用默认LRU得到eviction类, 即LruCache.class      String eviction = context.getStringAttribute("eviction", "LRU");      Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);      Long flushInterval = context.getLongAttribute("flushInterval");      Integer size = context.getIntAttribute("size");       boolean readWrite = !context.getBooleanAttribute("readOnly", false);      Properties props = context.getChildrenAsProperties();     //根据提供的参数, 建一个cache对象, 并将这个对象放入到Configuration 对象中.      builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, props);     } }



子节点<parameterMap>

在Mybatis中,这个节点已经被废弃了!老式风格的参数映射。内联参数是首选,这个元素可能在将来被移除。其实解析还是比较简单的。取这个节点的所有属性,build出一个ParameterMapping对象,方法parametermappings列表里, 然后将这个列表更新到configuration对象中的ParameterMap中去。

    for (XNode parameterMapNode : list) {      String id = parameterMapNode.getStringAttribute("id");      String type = parameterMapNode.getStringAttribute("type");      Class<?> parameterClass = resolveClass(type);      List<XNode> parameterNodes = parameterMapNode.evalNodes("parameter");      List<ParameterMapping> parameterMappings = new ArrayList<ParameterMapping>();      for (XNode parameterNode : parameterNodes) {        String property = parameterNode.getStringAttribute("property");        String javaType = parameterNode.getStringAttribute("javaType");        String jdbcType = parameterNode.getStringAttribute("jdbcType");        String resultMap = parameterNode.getStringAttribute("resultMap");        String mode = parameterNode.getStringAttribute("mode");        String typeHandler = parameterNode.getStringAttribute("typeHandler");        Integer numericScale = parameterNode.getIntAttribute("numericScale");        ParameterMode modeEnum = resolveParameterMode(mode);        Class<?> javaTypeClass = resolveClass(javaType);        JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);        @SuppressWarnings("unchecked")        Class<? extends TypeHandler<?>> typeHandlerClass = (Class<? extends TypeHandler<?>>) resolveClass(typeHandler);        ParameterMapping parameterMapping = builderAssistant.buildParameterMapping(parameterClass, property, javaTypeClass, jdbcTypeEnum, resultMap, modeEnum, typeHandlerClass, numericScale);        parameterMappings.add(parameterMapping);      }      builderAssistant.addParameterMap(id, parameterClass, parameterMappings);    }  }

子节点<resultMap>

这个节点的解析较复杂,会在下一遍中单独讲解。

子节点<sql>

<sql id="userColumns"> id,username,password </sql>
这个元素可以被用来定义可重用的 SQL 代码段,可以包含在其他语句中。
Java 代码

private void sqlElement(List<XNode> list) throws Exception {    if (configuration.getDatabaseId() != null) {  //传人configuration现有的database id。      sqlElement(list, configuration.getDatabaseId());    }    sqlElement(list, null);  }
sqlElement方法:

  private void sqlElement(List<XNode> list, String requiredDatabaseId) throws Exception {    for (XNode context : list) { //由于有多个并列的sql节点,需求逐个解析。     //sql 也可以有自己的database id,但实际上这个id要与全局配置的一致      String databaseId = context.getStringAttribute("databaseId");        String id = context.getStringAttribute("id");     //判断id是否包含有当前的namespace,包含的话直接返回当前id。 没有的话,则以当前mapper的namespace.id值返回      id = builderAssistant.applyCurrentNamespace(id, false);      if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId))        sqlFragments.put(id, context);    }  }

databaseIdMatchesCurrent 方法:

sql节点自己指定的database id要与configuration中的一致。

private boolean databaseIdMatchesCurrent(String id, String databaseId, String requiredDatabaseId) {    if (requiredDatabaseId != null) {      if (!requiredDatabaseId.equals(databaseId)) {        return false;      }    } else {      if (databaseId != null) {        return false;      }      // skip this fragment if there is a previous one with a not null databaseId      if (this.sqlFragments.containsKey(id)) {        XNode context = this.sqlFragments.get(id);        if (context.getStringAttribute("databaseId") != null) {          return false;        }      }    }    return true;  }
实际上这个sqlFragments map是在XMLMappBuilder初始化的时候,有configuration传入的, 所以configuration能得到最新的对这个map的修改。这个map会在build select等节点的时候被用到。

子节点<select>,<insert>,<update>,<delete>
节点配置,以select为例:

<select  id="selectPerson"  databaseId="test"  fetchSize="256"  timeout="10000"  parameterMap="deprecated"  parameterType="int"  resultMap="personResultMap"  resultType="hashmap"  lang="XML"  resultSetType="FORWARD_ONLY"  statementType="PREPARED"  flushCache="false"  useCache="true"  resultOrdered="false"  >

节点属性描述:

属性描述id在命名空间中唯一的标识符,可以被用来引用这条语句。databaseId如果配置了databaseProvider,databaseId要与配置的一致。如果不匹配的话,后续代码不会继续解析           fetchSize这是暗示驱动程序每次批量返回的结果行数。默认不设置(驱动 自行处理)。timeout这个设置驱动程序等待数据库返回请求结果,并抛出异常时间的 最大等待值。默认不设置(驱动自行处理)parameterMap这是引用外部 parameterMap 的已经被废弃的方法。使用内联参数 映射和 parameterType 属性。parameterType将会传入这条语句的参数类的完全限定名或别名。resultMap 命名引用外部的 resultMap。 返回 map 是 MyBatis 最具力量的特性, 对其有一个很好的理解的话, 许多复杂映射的情形就 能被解决了。 使用 resultMap 或 resultType,但不能同时使用。resultType从这条语句中返回的期望类型的类的完全限定名或别名。注意集 合情形,那应该是集合可以包含的类型,而不能是集合本身。使 用 resultType 或 resultMap,但不能同时使用lang提供”XML“或”RAW“,不同的lang,可能会影响最终的sql语句的生成,默认是”XML“resultSetTypeFORWARD_ONLY|SCROLL_SENSITIVE|SCROLL_INSENSITIVE 中的一种。默认是不设置(驱动自行处理)。statementTypeSTA TEMENT,PREPARED 或 CALLABLE 的一种。 这会让 MyBatis 使用选择使用 Statement,PreparedStatement 或 CallableStatement。 默认值:PREPAREDfhushCache将其设置为 true,不论语句什么时候被调用,都会导致缓存被 清空。默认值:falseuserCache将其设置为 true, 将会导致本条语句的结果被缓存。 默认值: trueresultOrdered这个属性值适用于有嵌套结果的select 语句。如果是true, 那么就认为包含了嵌套的结果, 或者嵌套的结果被分在一组。这样的话, 当一个新的主结果返回时, 就不会出现上一个结果行。 这能够更加内存友好地填充嵌套结果。默认是false。

另外还有3个是只对insert有效的属性:

useGeneratedKeys 这会告诉 MyBatis使用JDBC的getGeneratedKeys 方法来取出由数据(比如:像 MySQL 和 SQL Server 这样的数据库管理系统的自动递增字段)内部生成的主键。 默认值:falsekeyProperty 标记一个属性, MyBatis 会通过 getGeneratedKeys 或者通过 insert 语句的 selectKey 子元素设置它的值。 默认: 不设置keyColumn标记一个属性, MyBatis 会通过 getGeneratedKeys 或者通过 insert 语句的 selectKey 子元素设置它的值。 默认: 不设置

sql语句的解析实际上是在 XMLStatementBuider的parseStatementNode方法中解析的。

  private void buildStatementFromContext(List<XNode> list) {    if (configuration.getDatabaseId() != null) {      buildStatementFromContext(list, configuration.getDatabaseId());    }    buildStatementFromContext(list, null);  }  private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {    for (XNode context : list) {      //构造statementBuilder。这几个sql语句相关的节点在这个类里解析      final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);      try {        statementParser.parseStatementNode();      } catch (IncompleteElementException e) {        configuration.addIncompleteStatement(statementParser);      }    }  }

XMLStatementBuilder.java的 parseStatementNode方法,如下。这个方法比较长,但主要是读取属性值,然后包装生成一个MappedStatement,并放入configuration对象中。

public void parseStatementNode() {    //读取id 属性    String id = context.getStringAttribute("id");    String databaseId = context.getStringAttribute("databaseId");    //读取databaseId 属性,但是一定要与现有存在的匹配, 否则不继续解析剩余节点     if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) return;    Integer fetchSize = context.getIntAttribute("fetchSize");    Integer timeout = context.getIntAttribute("timeout");    String parameterMap = context.getStringAttribute("parameterMap");    String parameterType = context.getStringAttribute("parameterType");    Class<?> parameterTypeClass = resolveClass(parameterType);    String resultMap = context.getStringAttribute("resultMap");    String resultType = context.getStringAttribute("resultType");    String lang = context.getStringAttribute("lang");    LanguageDriver langDriver = getLanguageDriver(lang);    Class<?> resultTypeClass = resolveClass(resultType);    String resultSetType = context.getStringAttribute("resultSetType");    //statementType 默认是PreparedStatement    StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));    ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);    String nodeName = context.getNode().getNodeName();    SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;    //如果是select语句,则默认值是false。否则默认值是true。    boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);    //如果是select语句,则默认值是true。否则默认值是false。    boolean useCache = context.getBooleanAttribute("useCache", isSelect);    boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);    // 解析<include>子节点,其实是用引用替换<sql>节点的实际内容。    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);    includeParser.applyIncludes(context.getNode());    // 解析<selectKey>子节点,仅对<insert>有用。后面会讲解parseSelectKeyNodes方法    List<XNode> selectKeyNodes = context.evalNodes("selectKey");    if (configuration.getDatabaseId() != null) {      parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, configuration.getDatabaseId());    }    parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, null);    //默认是XMLLanguageDriver,parameterTypeClass用不上    //生成持有当前sql语句的sqlSource对象,后面会具体看这个sqlSource是怎么生成的    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);    //keyProperty和keyColumn使用于<insert>语句    String keyProperty = context.getStringAttribute("keyProperty");    String keyColumn = context.getStringAttribute("keyColumn");    KeyGenerator keyGenerator;    String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;    keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);    if (configuration.hasKeyGenerator(keyStatementId)) {      keyGenerator = configuration.getKeyGenerator(keyStatementId);    } else {      keyGenerator = context.getBooleanAttribute("useGeneratedKeys",          configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))          ? new Jdbc3KeyGenerator() : new NoKeyGenerator();    }    //通过解析出来的属性值,构造MappedStatement对象。并放入configuration对象中    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,        resultSetTypeEnum, flushCache, useCache, resultOrdered,         keyGenerator, keyProperty, keyColumn, databaseId, langDriver);  }

<insert>节点的子节点<selectKey>的解析

<selectKey>配置例子:

<selectKey  databaseId="mysql“  keyProperty="id"  resultType="int"  order="BEFORE"  statementType="PREPARED">select CAST(RANDOM()*1000000 as INTEGER) a from SYSIBM.SYSDUMMY1  </selectKey>

keyProperty:selectKey 语句结果应该被设置的目标属性。就是给哪个字段生成主键值。

resultType:结果的类型。MyBatis 通常可以算出来,但是写上也没有问题。 MyBatis 允许任何简单类型用作主键的类型,包括字符串

statementType:MyBatis 支持 STA TEMENT ,PREPARED 和 CALLABLE 语句的映射类型,分别代表 PreparedStatement 和 CallableStatement 类型。

order:这可以被设置为 BEFORE 或 AFTER。如果设置为 BEFORE,那 么它会首先选择主键, 设置 keyProperty 然后执行插入语句。 如果 设置为 AFTER,那么先执行插入语句,然后是 selectKey 元素- 这和如 Oracle 数据库相似,可以在插入语句中嵌入序列调用.

java代码:

public void parseSelectKeyNodes(String parentId, List<XNode> list, Class<?> parameterTypeClass, LanguageDriver langDriver, String skRequiredDatabaseId) {   //可以有多个<selectKey>节点。   for (XNode nodeToHandle : list) {      String id = parentId + SelectKeyGenerator.SELECT_KEY_SUFFIX;      String databaseId = nodeToHandle.getStringAttribute("databaseId");      if (databaseIdMatchesCurrent(id, databaseId, skRequiredDatabaseId)) {        parseSelectKeyNode(id, nodeToHandle, parameterTypeClass, langDriver, databaseId);      }    }  }  //  public void parseSelectKeyNode(String id, XNode nodeToHandle, Class<?> parameterTypeClass, LanguageDriver langDriver, String databaseId) {    String resultType = nodeToHandle.getStringAttribute("resultType");    Class<?> resultTypeClass = resolveClass(resultType);    StatementType statementType = StatementType.valueOf(nodeToHandle.getStringAttribute("statementType", StatementType.PREPARED.toString()));    String keyProperty = nodeToHandle.getStringAttribute("keyProperty");    boolean executeBefore = "BEFORE".equals(nodeToHandle.getStringAttribute("order", "AFTER"));    //defaults    boolean useCache = false;    boolean resultOrdered = false;    KeyGenerator keyGenerator = new NoKeyGenerator();    Integer fetchSize = null;    Integer timeout = null;    boolean flushCache = false;    String parameterMap = null;    String resultMap = null;    ResultSetType resultSetTypeEnum = null;    //解析生成主键的sql语句    SqlSource sqlSource = langDriver.createSqlSource(configuration, nodeToHandle, parameterTypeClass);    SqlCommandType sqlCommandType = SqlCommandType.SELECT;    //生成MappedStatement 对象,放入configuration对象的mappedStatements map中    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,        resultSetTypeEnum, flushCache, useCache, resultOrdered,        keyGenerator, keyProperty, null, databaseId, langDriver);    id = builderAssistant.applyCurrentNamespace(id, false);    MappedStatement keyStatement = configuration.getMappedStatement(id, false);    //同时将这个MappedStatement 对象放入到configuration的 keyGenerator map中    configuration.addKeyGenerator(id, new SelectKeyGenerator(keyStatement, executeBefore));    nodeToHandle.getParent().getNode().removeChild(nodeToHandle.getNode());  }

下篇会主要看看<resultMap>的解析和sqlSource的生成。