(三)Mybatis持久化框架原理之启动源码分析

目录

一、对于mybatis配置文件的加载

1.对config.xml文件的加载

1.1 通过build方式创建SqlSessionFactory

1.2 XMLConfigBuilder调用parse方法

1.3 properties标签的解析方法propertiesElement

1.4 typeAliases标签的解析方法typeAliasesElement

1.5 plugins标签的解析方法pluginElement

1.6 objectFactory等标签的解析方法objectFactoryElement

1.7 settings标签的解析方法settingsElement

1.8 environments标签的解析方法environmentsElement

1.9 databaseIdProvider标签的解析方法databaseIdProviderElement

1.10 typeHandlers标签的解析方法typeHandlerElement

2.mapper.xml文件的解析

2.1 解析mapper.xml构造器入口

2.2 XMLMapperBuilder构造器调用parse方法

2.3 解析mapper.xml文件方法configurationElement

2.4 解析cache-ref标签方法cacheRefElement

2.5 解析cache标签方法cacheElement

2.6 解析parameterMap标签方法parameterMapElement

2.7 解析resultMap标签方法resultMapElements

2.8 解析sql标签方法sqlElement

2.9 解析select|insert|update|delete标签方法buildStatementFromContext

2.10 XMLStatementBuilder构造器解析select|insert|update|delete标签

2.11 XMLScriptBuilder构造器创建SqlSource对象


注:观看本篇前请先阅读(二)Mybatis持久化框架原理之架构组成以完成基本框架的认识,且本篇较长,需要一步一步慢慢看并结合实际使用经验来理解。

一、对于mybatis配置文件的加载

1.对config.xml文件的加载

1.1 通过build方式创建SqlSessionFactory

mybatis对配置文件加载的入口便是

SqlSessionFactoryBuilder源码如下SqlSessionFactoryBuilder构造器,在这里面完成解析config.xml文件,并且将获得的configuration对象返回,并创建SqlSessionFactory对象。

SqlSessionFactoryBuilder关键源码如下:

public class SqlSessionFactoryBuilder {
  public SqlSessionFactory build(InputStream inputStream) {
    // 一般使用方法都是把文件流传入进来,直接进行解析
    return build(inputStream, null, null);
  }
  public SqlSessionFactory build(InputStream inputStream, 
          String environment, Properties properties) {
    try {
      // 创建config.xml文件解析构造器
      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, 
              environment, properties);
      // parser.parse方法返回的是configuration对象
      return build(parser.parse());
    } catch (Exception e) {
      ...
    } finally {
      ...
    }
  }
  public SqlSessionFactory build(Configuration config) {
    // 直接返回默认的SqlSessionFactory对象
    return new DefaultSqlSessionFactory(config);
  }
}

具体看到build方法中,先实例化XMLConfigBuilder,使用SAX读取XML文件内容,有兴趣的可以看下其大致读取流程。

1.2 XMLConfigBuilder调用parse方法

接下来看看XMLConfigBuilder类中如何一步一步的把XML文件中的节点解析出来,并形成Configuration对象的。

XMLConfigBuilder相关部分源码如下:

public class XMLConfigBuilder extends BaseBuilder {
  private boolean parsed;
  private XPathParser parser;
  private String environment;
  private ReflectorFactory localReflectorFactory = 
          new DefaultReflectorFactory();
  private XMLConfigBuilder(XPathParser parser, String environment, 
          Properties props) {
    // 直接实例化Configuration对象
    super(new Configuration());
    // 传进来的Properties如果不为空则在这里赋值
    this.configuration.setVariables(props);
    this.parsed = false;
    // 传进来的environment如果不为空则在这里赋值
    this.environment = environment;
    this.parser = parser;
  }
  public Configuration parse() {
    // 如果解析过再调用直接抛出异常
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used"+
              " once.");
    }
    parsed = true;
    // 开始解析Configuration节点下面的节点
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }
  private void parseConfiguration(XNode root) {
    // 开始一次解析十一种不同的节点
    try {
      Properties settings = 
              settingsAsPropertiess(root.evalNode("settings"));
      propertiesElement(root.evalNode("properties"));
      loadCustomVfs(settings);
      typeAliasesElement(root.evalNode("typeAliases"));
      pluginElement(root.evalNode("plugins"));
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      settingsElement(settings);
      environmentsElement(root.evalNode("environments"));
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      typeHandlerElement(root.evalNode("typeHandlers"));
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      ...
    }
  }
}

XMLConfigBuilder对象中的parse解析方法只能解析一次,第一次解析后parsed将会为true,后续再次调用这个方式会抛出BuilderException异常。

关于parser调用的方法evalNode其核心是使用XPath对其内部的节点进行查找,并将其解析为XNode对象,下面可以看看其对象内部封装的属性和后面将要使用到的方法。

public class XNode {
  private Node node;
  private String name;
  private String body;
  private Properties attributes;
  private Properties variables;
  private XPathParser xpathParser;
  public Properties getChildrenAsProperties() {
    Properties properties = new Properties();
    for (XNode child : getChildren()) {
      String name = child.getStringAttribute("name");
      String value = child.getStringAttribute("value");
      if (name != null && value != null) {
        properties.setProperty(name, value);
      }
    }
    // 返回的properties将会有name-value对
    return properties;
  }
  public String getStringAttribute(String name, String def) {
    String value = attributes.getProperty(name);
    if (value == null) {
      return def;
    } else {
      return value;
    }
  }
  public List<XNode> getChildren() {
    List<XNode> children = new ArrayList<XNode>();
    NodeList nodeList = node.getChildNodes();
    if (nodeList != null) {
      for (int i = 0, n = nodeList.getLength(); i < n; i++) {
        Node node = nodeList.item(i);
        if (node.getNodeType() == Node.ELEMENT_NODE) {
          children.add(new XNode(xpathParser, node, variables));
        }
      }
    }
    return children;
  }
}

该类里面有等下会经常用到的几个方法,现在详细介绍一下:

  1. getChildrenAsProperties方法:获得该节点property标签的name-value对;
  2. getStringAttribute方法:根据name来获得String类型的属性值;
  3. getChildren方法:获得该节点的所有子节点。

回到XMLConfigBuilder类的parseConfiguration方法中来,可以看到这个方法对Configuration节点的子节点分别设置了一个方法去解析,也可以看得出来Mybatis的xml配置文件具体有多少子节点,虽然前面一篇已经介绍过,但在这里也简单的带过一下依次介绍配置类中的各个节点作用:

  1. properties:用来将另外properties文件中的配置属性加入到Configuration对象的variables对象中,以便后续使用,需要注意的是只能有一个这样类型的标签,并且先读取resource属性,后读取url属性,且两者只能同时使用其中一种;
  2. settings:Mybatis框架的运行配置,诸如logImpl、cacheEnabled和defaultExecutorType这些常见的配置都是在这个标签中声明子标签完成的;
  3. typeAliases:用来手动添加一些类的别名,如有个XXX.XXX.XXX.NornalStudent类,便可以使用student来作为其类的别名;
  4. plugins:用来添加插件,可添加多个,内部的原理是拦截器链;
  5. objectFactory:确定objectFactory的类型,只能同时存在一个;
  6. objectWrapperFactory:同objectFactory;
  7. reflectorFactory:同objectFactory;
  8. environments:用来配置数据源和事务管理器,能有多个配置,但只会取其中的一个;
  9. databaseIdProvider:顾名思义,确定刚配置的数据源名称;
  10. typeHandlers:添加一些额外类型处理器,如自己实现了custom类型的类型处理,则在这个标签里面添加进去即可;
  11. mappers:多种配置方式,但大体上可分为两者,一种是直接确定mapper的接口包位置,然后将包下的添加进mapperRegistry对象中;第二种是决定单一的接口类或者指向一个XML文件,如果指定了单一的接口类,则直接添加进mapperRegistry对象中,如果是指向一个XML文件,则需要调用XMLMapperBuilder去解析这个XML文件,从而获得它的标签内容。

同时需要注意的是,在config.xml文件中,必须得按照properties->settings-> typeAliases-> typeHandlers->objectFactory->objectWrapperFactory->reflectorFactory->plugins-> environments->databaseIdProvider->mappers的顺序来创建标签内容,否则会抛出BuilderException异常。

1.3 properties标签的解析方法propertiesElement

方法源码如下:

public class XMLConfigBuilder extends BaseBuilder {
  private void propertiesElement(XNode context) throws Exception {
    // 如果properties节点不为空
    if (context != null) {
      Properties defaults = context.getChildrenAsProperties();
      String resource = context.getStringAttribute("resource");
      String url = context.getStringAttribute("url");
      // 如果url和resource属性同时存在则抛异常,只能同时存在一个
      if (resource != null && url != null) {
        throw new BuilderException();
      }
      // 两者取其一
      if (resource != null) {
        defaults.putAll(Resources.getResourceAsProperties(resource));
      } else if (url != null) {
        defaults.putAll(Resources.getUrlAsProperties(url));
      }
      Properties vars = configuration.getVariables();
      if (vars != null) {
        defaults.putAll(vars);
      }
      parser.setVariables(defaults);
      // 将解析获得的Properties对象赋值给variables
      configuration.setVariables(defaults);
    }
  }
}

properteis标签中可以有resource和url属性,并且可以同时存在,但存放的时候仅会存放其中一个,并且resource属性的值会被优先存放。在最开始的时候就会通过getChildrenAsProperties方法所有的name-value对,并形成初始的peroperties集合。

1.4 typeAliases标签的解析方法typeAliasesElement

方法源码如下:

public class XMLConfigBuilder extends BaseBuilder {
  private void typeAliasesElement(XNode parent) {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        if ("package".equals(child.getName())) {
          String typeAliasPackage = child.getStringAttribute("name");
          configuration.getTypeAliasRegistry()
                  .registerAliases(typeAliasPackage);
        } else {
          String alias = child.getStringAttribute("alias");
          String type = child.getStringAttribute("type");
          try {
            Class<?> clazz = Resources.classForName(type);
            if (alias == null) {
              typeAliasRegistry.registerAlias(clazz);
            } else {
              typeAliasRegistry.registerAlias(alias, clazz);
            }
          } catch (ClassNotFoundException e) {
            throw new BuilderException("Error registering typeAlias for '"+
                    alias + "'. Cause: " + e, e);
          }
        }
      }
    }
  }
}

typeAliases节点下可以有两种节点名字,一个是package,一个是typeAlias,此两种只能存在一种,package表示直接把包下面的所有实体都包括进来,用类的名字当做别名。而typeAlias标签中如果只有type,也把类的名字当成别名,否则使用alias当别名。

1.5 plugins标签的解析方法pluginElement

方法源码如下:

public class XMLConfigBuilder extends BaseBuilder {
  private void pluginElement(XNode parent) throws Exception {
    // 节点不为空
    if (parent != null) {
      // 获取里面的plugin节点
      for (XNode child : parent.getChildren()) {
        // interceptor对象将是class全路径
        String interceptor = child.getStringAttribute("interceptor");
        Properties properties = child.getChildrenAsProperties();
        Interceptor interceptorInstance = 
                (Interceptor) resolveClass(interceptor).newInstance();
        interceptorInstance.setProperties(properties);
        // 将拦截器添加到configuration对象中
        configuration.addInterceptor(interceptorInstance);
      }
    }
  }
}

为mybatis注册插件,从官方给出的变量名也可以看出可以把插件当成mybatis的拦截器,其每个拦截器标签中都有property的name-value对。

1.6 objectFactory等标签的解析方法objectFactoryElement

对于objectFactory、objectWrapperFactory和reflectorFactory这三个节点的解析基本上流程都是一致的,因此只需要看其中一个便可。

解析objectFactory方法源码如下:

public class XMLConfigBuilder extends BaseBuilder {
  private void objectFactoryElement(XNode context) throws Exception {
    // 节点不为空
    if (context != null) {
      // 指定的type类型
      String type = context.getStringAttribute("type");
      Properties properties = context.getChildrenAsProperties();
      ObjectFactory factory = (ObjectFactory) resolveClass(type)
              .newInstance();
      factory.setProperties(properties);
      configuration.setObjectFactory(factory);
    }
  }
}

获得name-value对并设置objectFactory对象(如果是objectWrapperFactory就设置configuration中对应的)。

1.7 settings标签的解析方法settingsElement

方法源码如下:

public class XMLConfigBuilder extends BaseBuilder {
  private Properties settingsAsPropertiess(XNode context) {
    // 如果setting节点为空则返回空的Properties对象
    if (context == null) {
      return new Properties();
    }
    Properties props = context.getChildrenAsProperties();
    MetaClass metaConfig = MetaClass.forClass(Configuration.class, 
            localReflectorFactory);
    // 循环判断setting的属性值
    for (Object key : props.keySet()) {
      // 如果对应的setting配置对象没有set否则抛异常
      if (!metaConfig.hasSetter(String.valueOf(key))) {
        throw new BuilderException();
      }
    }
    return props;
  }
  private void settingsElement(Properties props) throws Exception {
    // 获取setting节点中的属性,并set进configuration对应的对象中
    // 具体有哪些这里忽略,看了也没多大意义,有兴趣的可以去看下源码
    // 最常使用的也就四五个
    ...
  }
}

该方法直接将settingsAsPropertiess获得的属性依次进行类型转换并设置进Configuration对象相应的成员属性。属性有很多,但一般最常使用的配置就几个,说明如下:

  • localCacheScope:一级缓存,一共两种:SESSION,STATEMENT,默认SESSION,即启用一级缓存,STATEMENT则是不开启;
  • cacheEnabled:是否开启二级缓存,默认是开启的;
  • defaultExecutorType:默认的Executor类型,一共三种:SIMPLE、REUSE和BATCH;
  • jdbcTypeForNull:对参数值为null的应该使用哪种类型的jdbcType类处理,默认OTHER,如果想要传入参数允许为null,这个参数设置为NULL类型即可;
  • logImpl:指定mybatis的日志打印类。

1.8 environments标签的解析方法environmentsElement

方法源码如下:

public class XMLConfigBuilder extends BaseBuilder {
  private void environmentsElement(XNode context) throws Exception {
    // 节点不为空
    if (context != null) {
      if (environment == null) {
        // 如果environment对象为空,则取节点的default默认配置
        environment = context.getStringAttribute("default");
      }
      // environment配置可以有多个,因此循环
      for (XNode child : context.getChildren()) {
        String id = child.getStringAttribute("id");
        // 符合条件的id才会被解析:如果配置了environment,且id和environment
        // 一致则进入if代码块
        if (isSpecifiedEnvironment(id)) {
          TransactionFactory txFactory =  transactionManagerElement(
                  child.evalNode("transactionManager"));
          DataSourceFactory dsFactory = dataSourceElement(
                  child.evalNode("dataSource"));
          DataSource dataSource = dsFactory.getDataSource();
          // 获取到了数据源和对应的事务工厂,则构造一个Environment对象
          // 保存数据源等信息,尽管environment标签可以配置多个
          // 但最终只有一个会被构造并赋值给configuration对象
          Environment.Builder environmentBuilder = 
              new Environment.Builder(id)
              .transactionFactory(txFactory).dataSource(dataSource);
          configuration.setEnvironment(environmentBuilder.build());
        }
      }
    }
  }
}

从代码中可以看到首先会从environments节点中取到default属性,确认默认的environment,只有当后续节点有相应的id,才会设置,否则configuration对象中的environment将会为空,后续操作数据库将会报空指针异常,并且default属性必须得设置,否则会抛BuilderException异常。获得environment属性成功后将会解析dataSource节点,获得数据源信息和事务工厂信息。

1.9 databaseIdProvider标签的解析方法databaseIdProviderElement

方法源码如下:

public class XMLConfigBuilder extends BaseBuilder {
  private void databaseIdProviderElement(XNode context) throws Exception {
    DatabaseIdProvider databaseIdProvider = null;
    // 节点不为空
    if (context != null) {
      String type = context.getStringAttribute("type");
      // 兼容以前的老版本
      if ("VENDOR".equals(type)) {
          type = "DB_VENDOR";
      }
      Properties properties = context.getChildrenAsProperties();
      databaseIdProvider = (DatabaseIdProvider) resolveClass(type)
              .newInstance();
      databaseIdProvider.setProperties(properties);
    }
    Environment environment = configuration.getEnvironment();
    if (environment != null && databaseIdProvider != null) {
      String databaseId = databaseIdProvider
              .getDatabaseId(environment.getDataSource());
      // 设置数据源环境的databaseId绑定关系
      configuration.setDatabaseId(databaseId);
    }
  }
}

此方法为指定数据源提供id类,其中有个兼容旧版本补丁写在了代码中,即使用VENDOR来兼容以前的DB_VENDOR类型。

1.10 typeHandlers标签的解析方法typeHandlerElement

方法源码如下:

public class XMLConfigBuilder extends BaseBuilder {
  private void typeHandlerElement(XNode parent) throws Exception {
    // 节点不为空
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        if ("package".equals(child.getName())) {
          // 如果是package方式,则直接获取name属性,并直接注册,里面会把
          // 内部类和接口剔除,剩余的注册进TypeHandlerRegistry
          String typeHandlerPackage = child.getStringAttribute("name");
          typeHandlerRegistry.register(typeHandlerPackage);
        } else {
          // 单个的类型设置
          String javaTypeName = child.getStringAttribute("javaType");
          String jdbcTypeName = child.getStringAttribute("jdbcType");
          String handlerTypeName = child.getStringAttribute("handler");
          Class<?> javaTypeClass = resolveClass(javaTypeName);
          JdbcType jdbcType = resolveJdbcType(jdbcTypeName);
          Class<?> typeHandlerClass = resolveClass(handlerTypeName);
          // 各个属性不同的配置会进入不同的注册方法,但最终都会注册到
          // TypeHandlerRegistry的各个对象集合中
          if (javaTypeClass != null) {
            if (jdbcType == null) {
              typeHandlerRegistry.register(javaTypeClass, 
                      typeHandlerClass);
            } else {
              typeHandlerRegistry.register(javaTypeClass, jdbcType, 
                      typeHandlerClass);
            }
          } else {
            typeHandlerRegistry.register(typeHandlerClass);
          }
        }
      }
    }
  }
}

类似typeAliases标签一样,该标签的子标签也有两种,一个package,一个typeHandler,处理方法差不多一个逻辑。该方法是给用户自定义类型处理器的,注入string、integer都是mybatis为Java官方设置的类型处理器,该标签允许用户自定义类型处理器,确定具体的java类型,对应的数据库类型和具体的处理器类。

至此,除了mapper之外,config.xml文件的其它标签便已经解析完成。

2.mapper.xml文件的解析

2.1 解析mapper.xml构造器入口

方法源码如下:

public class XMLConfigBuilder extends BaseBuilder {
  private void mapperElement(XNode parent) throws Exception {
    // 节点不为空
    if (parent != null) {
      // 节点可能有多个,因此循环处理
      for (XNode child : parent.getChildren()) {
        if ("package".equals(child.getName())) {
          // 如果是package类型的,将会把包下的接口类都拿出来注册进mapperRegistry
          // 对象中,里面最终也会调用addMapper方法,这个后面会说
          String mapperPackage = child.getStringAttribute("name");
          configuration.addMappers(mapperPackage);
        } else {
          String resource = child.getStringAttribute("resource");
          String url = child.getStringAttribute("url");
          String mapperClass = child.getStringAttribute("class");
          // resource、url和class三个文件有且只能有一个,如果三个都为空
          // 则会抛出BuilderException异常,其中resource和url方式将会
          // 调用XMLMapperBuilder对象对引入的mapper.xml文件进行解析
          if (resource != null && url == null && mapperClass == null) {
            InputStream inputStream = Resources
                    .getResourceAsStream(resource);
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(
                    inputStream, configuration, resource, 
                    configuration.getSqlFragments());
            mapperParser.parse();
          } else if (resource == null && url != null && 
                  mapperClass == null) {
            // url和resource的处理方式差不多,只是调用Resources类的方法不一样
            InputStream inputStream = Resources.getUrlAsStream(url);
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(
                    inputStream, configuration, url, 
                    configuration.getSqlFragments());
            mapperParser.parse();
          } else if (resource == null && url == null && 
                  mapperClass != null) {
            // 如果是class类型,则直接把class对象添加进mapperRegistry中,
            // 当然这种配置方式和package配置方式都可以在接口里面配置mybatis的
            // 相关注解,当然如果不配置,在resource文件夹下名字相同且namespace
            // 指向同一个xml,mybatis还没有读取这个xml情况下,框架会自动读取
            // 对应的xml文件并解析(前提是xml和对应的接口在同一路径下)
            // 有兴趣的可以自行去看addMapper方法的逻辑
            Class<?> mapperInterface = Resources.classForName(mapperClass);
            configuration.addMapper(mapperInterface);
          } else {
            throw new BuilderException();
          }
        }
      }
    }
  }
}

类似typeAliases标签一样,该标签的子标签也有两种,一个package,一个typeHandler,处理方法差不多一个逻辑。只是针对mappers下面的mapper标签不同的处理是resource、url和class三个属性只能同时存在一个,否则会抛出BuilderException异常。

使用resource和url两个属性mybatis将会进行一些额外的处理,如果直接使用class指定类,那么调用configuration的addMapper方法即可,但也是需要解析的,具体的解析方法后续分析。

通过上面的这些步骤后,configuration对config.xml文件将解析完成,并把其中的信息放入configuration对象中,后续通过调用build方法实例化DefaultSqlSessionFactory对象并返回,此步骤将完成SqlSessionFactory的构建。

2.2 XMLMapperBuilder构造器调用parse方法

其中mapperParser.parse()方法和configuration.addMapper()这两个方法是将mapper添加进mybatis中的核心方法,我们先看mapperParser.parse()方法源码

public class XMLMapperBuilder extends BaseBuilder {
    public void parse() {
        // 如果resource没有被加载过则进行加载
        if (!configuration.isResourceLoaded(resource)) {
            // 解析mapper节点引入的xml文件
            configurationElement(parser.evalNode("/mapper"));
            // 把解析过的xml添加到loadedResources对象中
            configuration.addLoadedResource(resource);
            // 将xml文件和接口进行namespace绑定,并把绑定的接口添加进
            // mapperRegistry中
            bindMapperForNamespace();
        }
        // 对resultMap解析失败的进行再次解析
        parsePendingResultMaps();
        // 对cacheRef解析失败的进行再次解析
        parsePendingCacheRefs();
        // 对statement解析失败的进行再次解析
        parsePendingStatements();
    }
}

parse方法if语句中bindMapperForNamespace方法之前是为了判断resource字段是否已经被处理过,若处理过则不再处理。bindMapperForNamespace方法是处理resource和url类型mapper的真正方法,下面几个Pending方法则是为了处理在bindMapperForNamespace方法中处理失败的mapper。

2.3 解析mapper.xml文件方法configurationElement

configurationElement方法则是解析mapper.xml文件的主要方法,configurationElement方法源码如下:

public class XMLMapperBuilder extends BaseBuilder {
    private XPathParser parser;
    private MapperBuilderAssistant builderAssistant;
    private Map<String, XNode> sqlFragments;
    private void configurationElement(XNode context) {
        try {
            // 先获取mapper.xml文件的namespace,且这个属性不能为空
            String namespace = context.getStringAttribute("namespace");
            if (namespace == null || namespace.equals("")) {
                throw new BuilderException("");
            }
            // 设置mapper读取助理类的namespace命名空间
            builderAssistant.setCurrentNamespace(namespace);
            // 接下来解析mapper.xml文件中的积累标签,如果途中解析失败
            // 则抛出BuilderException异常
            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();
        }
    }
}

该方法将会对mapper中的parameterMap、resultMap、sql以及增删改查的各种标签进行解析,将其分别解析成ParameterMapping、ResultMapping、sqlFragments以及MappedStatement,其中MappedStatement将包含ParameterMapping和ResultMapping。

2.4 解析cache-ref标签方法cacheRefElement

其方法源码如下:

private void cacheRefElement(XNode context) {
  // 开始处理<cacheRef/>标签
  if (context != null) {
    configuration.addCacheRef(builderAssistant.getCurrentNamespace(), 
            context.getStringAttribute("namespace"));
    CacheRefResolver cacheRefResolver = new CacheRefResolver(
            builderAssistant, context.getStringAttribute("namespace"));
    try {
      cacheRefResolver.resolveCacheRef();
    } catch (IncompleteElementException e) {
      configuration.addIncompleteCacheRef(cacheRefResolver);
    }
  }
}

可以看到方法十分简单,单纯的将当前的namespace和引入的namespace做个关联,解析器里面做的操作也是类似的差不多。

2.5 解析cache标签方法cacheElement

关键方法源码如下:

private void cacheElement(XNode context) throws Exception {
  // 开始处理<cache/>标签
  if (context != null) {
    String type = context.getStringAttribute("type", "PERPETUAL");
    Class<? extends Cache> typeClass = 
        typeAliasRegistry.resolveAlias(type);
    // 确定不同的缓存算法
    // 缓存收回策略。LRU(最近最少使用的),FIFO(先进先出),SOFT( 软引用)
    // WEAK( 弱引用)
    String eviction = context.getStringAttribute("eviction", "LRU");
    Class<? extends Cache> evictionClass = 
        typeAliasRegistry.resolveAlias(eviction);
    // 刷新间隔
    Long flushInterval = context.getLongAttribute("flushInterval");
    // 可被设置为任意正整数,缓存的对象数目等于运行环境的可用内存
    // 资源数目,默认是1024
    Integer size = context.getIntAttribute("size");
    // 只读,true或false。只读的缓存会给所有的调用者返回缓存对象的相同实例
    boolean readWrite = !context.getBooleanAttribute("readOnly", false);
    boolean blocking = context.getBooleanAttribute("blocking", false);
    Properties props = context.getChildrenAsProperties();
    builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, 
        size, readWrite, blocking, props);
  }
}

2.6 解析parameterMap标签方法parameterMapElement

接下来请看parameterMapElement方法解析parameterMap源码:

private void parameterMapElement(List<XNode> list) throws Exception {
    // 开始处理<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>();
        // 对parameter再分别进行遍历
        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);
            // 使用前面获得的各个属性直接调用builderAssistant构造
            // ParameterMapping对象,实际上这里面也没做什么,只是把javaType和
            // TypeHandler两个类型具体确认了下来,然后再直接赋值
            ParameterMapping parameterMapping = 
                    builderAssistant.buildParameterMapping(parameterClass, 
                            property, javaTypeClass, jdbcTypeEnum, 
                            resultMap, modeEnum, typeHandlerClass, 
                            numericScale);
            parameterMappings.add(parameterMapping);
        }
        builderAssistant.addParameterMap(id, parameterClass, 
                parameterMappings);
    }
}
public ParameterMap addParameterMap(String id, Class<?> parameterClass, 
        List<ParameterMapping> parameterMappings) {
    id = applyCurrentNamespace(id, false);
    // 调用ParameterMap的内部类构造器,构造ParameterMap对象
    // 这里面的构造方法很简单,只是把parameterMappings对象赋值到
    // parameterMap对象中,并没有做特殊的操作(只进行了把parameterMappings
    // 变成不可变的集合对象)
    ParameterMap parameterMap = new ParameterMap.Builder(configuration, 
            id, parameterClass, parameterMappings).build();
    // 添加进configuration的对象中
    configuration.addParameterMap(parameterMap);
    return parameterMap;
}

该方法对应了xml中的parameterMap标签,parameterMap标签最多只能到parameter那一层,因此只需要将parameterMap标签的id和type拿到,进而对子标签(parameter)各个属性进行相应的解析并构造进ParameterMapping对象即可。最后将一个个的ParameterMapping对象通过builderAssistant.addParameterMap()方法将id、parameter类型和ParameterMapping对象构造成ParameterMap对象,最后添加进configuration对象中。

2.7 解析resultMap标签方法resultMapElements

resultMapElements方法解析resultMap源码:

private void resultMapElements(List<XNode> list) throws Exception {
    for (XNode resultMapNode : list) {
        try {
            // 直接遍历调用
            resultMapElement(resultMapNode);
        } catch (IncompleteElementException e) {
            ...
        }
    }
}
private ResultMap resultMapElement(XNode resultMapNode) throws Exception {
    // 默认additionalResultMappings是空集合
    return resultMapElement(resultMapNode, Collections.<ResultMapping> 
            emptyList());
}
private ResultMap resultMapElement(XNode resultMapNode, 
        List<ResultMapping> additionalResultMappings) throws Exception {
    // 开始处理<resultMap/>标签
    // getStringAttribute方法有两个参数,第一个参数key如果有则返回,否则返回
    // 第二个默认参数,因此这里的获取顺序是id>getValueBasedIdentifier值
    String id = resultMapNode.getStringAttribute("id",
        resultMapNode.getValueBasedIdentifier());
    // 根据上面的可知,type > ofType > resultType > javaType
    String type = resultMapNode.getStringAttribute("type",
        resultMapNode.getStringAttribute("ofType",
            resultMapNode.getStringAttribute("resultType",
                  resultMapNode.getStringAttribute("javaType"))));
    // 这是新的一层
    String extend = resultMapNode.getStringAttribute("extends");
    Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
    Class<?> typeClass = resolveClass(type);
    Discriminator discriminator = null;
    List<ResultMapping> resultMappings = new ArrayList<ResultMapping>();
    // 先把additionalResultMappings参数值加入进来,但实际上是空的
    resultMappings.addAll(additionalResultMappings);
    List<XNode> resultChildren = resultMapNode.getChildren();
    // 获得resultMap里面的各个子标签
    for (XNode resultChild : resultChildren) {
        // 这三种条件最终都会把id标签、discriminator等标签组合起来并添加到
        // resultMappings集合中,实际上ResultMapping对象也是通过
        // builderAssistant对象来构建的,实际进行的操作大致也是获得name-value
        // 对,然后进行类型转换,再直接赋值到ResultMapping对象中,基本上没有
        // 特殊处理
        if ("constructor".equals(resultChild.getName())) {
            processConstructorElement(resultChild, typeClass, 
                    resultMappings);
        } else if ("discriminator".equals(resultChild.getName())) {
            discriminator = processDiscriminatorElement(resultChild, 
                    typeClass, resultMappings);
        } else {
            List<ResultFlag> flags = new ArrayList<ResultFlag>();
            if ("id".equals(resultChild.getName())) {
                flags.add(ResultFlag.ID);
            }
            // buildResultMappingFromContext方法就是调用builderAssistant对象
            // 构建ResultMapping对象的地方,其它两种条件也都是调用这个方法
            resultMappings.add(buildResultMappingFromContext(resultChild, 
                    typeClass, flags));
        }
    }
    ResultMapResolver resultMapResolver = new ResultMapResolver(
            builderAssistant, id, typeClass, extend, discriminator, 
            resultMappings, autoMapping);
    try {
        // 调用解析方法,这个解析方法中只是将获得的resultMappings、id、type、
        // mappedColumns和mappedProperties等在resultMap标签中可以配置的属性
        // 依次赋值给ResultMap对象中对应的成员属性中
        return resultMapResolver.resolve();
    } catch (IncompleteElementException  e) {
        // 如果失败则添加到incompleteResultMaps集合中,等下再次解析
        configuration.addIncompleteResultMap(resultMapResolver);
        throw e;
    }
}
private void processConstructorElement(XNode resultChild, 
        Class<?> resultType, List<ResultMapping> resultMappings) 
                throws Exception {
    // 当标签类型是<constructor/>时将被调用
    List<XNode> argChildren = resultChild.getChildren();
    for (XNode argChild : argChildren) {
        List<ResultFlag> flags = new ArrayList<ResultFlag>();
        flags.add(ResultFlag.CONSTRUCTOR);
        // id标签类型
        if ("idArg".equals(argChild.getName())) {
            flags.add(ResultFlag.ID);
        }
        // 最终还是会调用buildResultMappingFromContext方法
        resultMappings.add(buildResultMappingFromContext(argChild, 
                resultType, flags));
    }
}
private ResultMapping buildResultMappingFromContext(XNode context, 
        Class<?> resultType, List<ResultFlag> flags) throws Exception {
    // 最终的constructor、discriminator以及其它的标签名字都会调用到这个方法中
    // 获取property对应的对象字段
    String property = context.getStringAttribute("property");
    // 数据库字段对应名称
    String column = context.getStringAttribute("column");
    // java类型
    String javaType = context.getStringAttribute("javaType");
    // jdbcType数据库类型
    String jdbcType = context.getStringAttribute("jdbcType");
    // 是否要调用其它的sql语句来进行额外的查询操作
    String nestedSelect = context.getStringAttribute("select");
    // 只有association、collection和case三种类型的标签才会有resultMap属性
    // processNestedResultMappings方法后续会仔细分析
    String nestedResultMap = context.getStringAttribute("resultMap",
        processNestedResultMappings(context, 
                Collections.<ResultMapping> emptyList()));
    String notNullColumn = context.getStringAttribute("notNullColumn");
    String columnPrefix = context.getStringAttribute("columnPrefix");
    String typeHandler = context.getStringAttribute("typeHandler");
    String resultSet = context.getStringAttribute("resultSet");
    String foreignColumn = context.getStringAttribute("foreignColumn");
    boolean lazy = "lazy".equals(context.getStringAttribute("fetchType", 
            configuration.isLazyLoadingEnabled() ? "lazy" : "eager"));
    // 熟悉的javaTypeClass和TypeHandler类型转换
    Class<?> javaTypeClass = resolveClass(javaType);
    @SuppressWarnings("unchecked")
    Class<? extends TypeHandler<?>> typeHandlerClass = 
            (Class<? extends TypeHandler<?>>) resolveClass(typeHandler);
    JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
    // 实际上只是对这些参数的简单赋值,没有影响全局的操作
    return builderAssistant.buildResultMapping(resultType, property, 
            column, javaTypeClass, jdbcTypeEnum, nestedSelect, 
            nestedResultMap, notNullColumn, columnPrefix, typeHandlerClass,
            flags, resultSet, foreignColumn, lazy);
}
private String processNestedResultMappings(XNode context, 
        List<ResultMapping> resultMappings) throws Exception {
    // 只有下面的三种类型标签才会有resultMap属性,这个属性会指向一个resultMap
    if ("association".equals(context.getName())
        || "collection".equals(context.getName())
        || "case".equals(context.getName())) {
        // 如果这里的select属性为空,则代表没有指定的sql查询语句
        if (context.getStringAttribute("select") == null) {
            // select为空,则调用resultMap属性,再去解析指向的resultMap
            // 一直循环,因此这里需要注意是否存在循环依赖调用
            ResultMap resultMap = resultMapElement(context, 
                    resultMappings);
            return resultMap.getId();
        }
    }
    return null;
}

resultMap标签相对parameterMap标签而言相对复杂,因为collection、collection以及case这三个标签又可以嵌入相同的子标签,并且标签中还可能含有resultMap属性,因此存在递归调用,但总体而言,思路和parameterMap标签的解析差不多。

最开始解析type、ofType、javaType和resultType这四个属性时,因为resultMap、collection、case和association四个标签表达属性名字不一样,因此需要分别解析这四种属性,随后将会解析该标签的子标签,并判断子标签不同的名字进行不同的处理,其中constructor和discriminator是单独处理的,其他的标签将会由buildResultMappingFromContext方法统一处理。

constructor属性解析解析id和idArg标签时,还是会调用buildResultMappingFromContext方法,而discriminator调回调用processDiscriminatorElement方法,解析本标签属性和子标签case后,将会构造成Discriminator对象。

buildResultMappingFromContext方法解析到resultMap属性时,将会调用processNestedResultMappings方法,如果该方法判断标签名字是association、collection和case,则会将该标签当成resultMap标签调用resultMapElement进行处理,这样就完成了resultMap的多层父子关系的解析。调到无子标签时,掉会调用MapperBuilderAssistant类的buildResultMapping方法,在此方法中会调用parseCompositeColumnName方法对特殊的columnName进行解析,当全部解析完成后将会构造成ResultMapping对象,并最后创建ResultMapResolver对象,再调用resolve方法,将所有的标签解析成ResultMap对象。

2.8 解析sql标签方法sqlElement

接下来看sqlElement方法解析sql标签源码:

private void sqlElement(List<XNode> list) throws Exception {
    // 两个方法都一样,只是会不会取databaseId而已
    if (configuration.getDatabaseId() != null) {
        sqlElement(list, configuration.getDatabaseId());
    }
    sqlElement(list, null);
}
private void sqlElement(List<XNode> list, String requiredDatabaseId) 
        throws Exception {
    for (XNode context : list) {
        String databaseId = context.getStringAttribute("databaseId");
        String id = context.getStringAttribute("id");
        id = builderAssistant.applyCurrentNamespace(id, false);
        if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) {
            // 如果databaseId一致,则直接添加进sqlFragments集合对象中
            sqlFragments.put(id, context);
        }
    }
}

该方法只是将sql标签的databaseId判断是否是属于当前的数据源,如果是则将标签和id存放进sqlFragments集合中。

2.9 解析select|insert|update|delete标签方法buildStatementFromContext

buildStatementFromContext方法解析select、insert、update和delete标签源码:

private void buildStatementFromContext(List<XNode> list) {
    // 和sql标签一样
    if (configuration.getDatabaseId() != null) {
        buildStatementFromContext(list, configuration.getDatabaseId());
    }
    buildStatementFromContext(list, null);
}
private void buildStatementFromContext(List<XNode> list, 
        String requiredDatabaseId) {
    // 循环遍历sql操作语句节点
    for (XNode context : list) {
        final XMLStatementBuilder statementParser = 
                new XMLStatementBuilder(configuration, builderAssistant, 
                        context, requiredDatabaseId);
        try {
            // 交给了XMLStatementBuilder 构造器来解决
            statementParser.parseStatementNode();
        } catch (IncompleteElementException e) {
            configuration.addIncompleteStatement(statementParser);
        }
    }
}

该类中的方法只是初步的将databaseId和节点分别封装成XMLStatementBuilder对象,进而调用parseStatementNode方法来解析sql语句标签,接下来看下XMLStatementBuilder中具体如何解析sql语句的。

2.10 XMLStatementBuilder构造器解析select|insert|update|delete标签

XMLStatementBuilder类源码如下:

public class XMLStatementBuilder extends BaseBuilder {
  // mapper构造器助理类
  private MapperBuilderAssistant builderAssistant;
  // 当前拥有的select|insert|update|delete四种标签的一个
  private XNode context;
  private String requiredDatabaseId;
  public void parseStatementNode() {
    // 当前标签在mapper.xml中唯一的标识符
    String id = context.getStringAttribute("id");
    String databaseId = context.getStringAttribute("databaseId");
    // 判断id、databaseId和requiredDatabaseId是否符合条件
    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");
    // langDriver的类型是需要注意的,如果没有设置取的实际上是configuration对象
    // 中的defaultDriverClass,否则就是从使用lang从languageRegistry中注册获取
    // 而languageRegistry没有在setting中设置属性的话,defaultDriverClass默认
    // 类型是XMLLanguageDriver,并且里面也会注册一个RawLanguageDriver类
    // 因此没有设置lang属性,langDriver类型就是XMLLanguageDriver,后面会用到
    LanguageDriver langDriver = getLanguageDriver(lang);
    Class<?> resultTypeClass = resolveClass(resultType);
    String resultSetType = context.getStringAttribute("resultSetType");
    // 一共有STATEMENT,PREPARED,CALLABLE三种类型,默认是PREPARED类型
    StatementType statementType = 
            StatementType.valueOf(
                    context.getStringAttribute("statementType", 
                            StatementType.PREPARED.toString()));
    // 默认为null
    ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
    String nodeName = context.getNode().getNodeName();
    // 共有UNKNOWN,INSERT,UPDATE,DELETE,SELECT,FLUSH六种类型,中间四种
    // 分别对应四种不同的标签select|insert|update|delete
    SqlCommandType sqlCommandType = SqlCommandType.valueOf(
            nodeName.toUpperCase(Locale.ENGLISH));
    // 字面意思,如果标签是SELECT,则isSelect为true
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    // 如果非select标签,则flushCache为false
    boolean flushCache = context.getBooleanAttribute("flushCache", 
            !isSelect);
    // 如果是isSelect,useCache则为true开启
    boolean useCache = context.getBooleanAttribute("useCache", isSelect);
    boolean resultOrdered = context.getBooleanAttribute("resultOrdered", 
            false);
    // 对sql操作中的<include/>标签进行转换的类
    XMLIncludeTransformer includeParser = 
            new XMLIncludeTransformer(configuration, builderAssistant);
    includeParser.applyIncludes(context.getNode());
    // 对<selectKey/>标签进行操作的方法
    processSelectKeyNodes(id, parameterTypeClass, langDriver);
    // 利用前面获得的langDriver对象创建对应的SqlSource对象,到这里我们已经知道了
    // 默认langDriver的类型是XMLLanguageDriver,后续再进行分析
    SqlSource sqlSource = langDriver.createSqlSource(configuration, 
            context, parameterTypeClass);
    String resultSets = context.getStringAttribute("resultSets");
    String keyProperty = context.getStringAttribute("keyProperty");
    String keyColumn = context.getStringAttribute("keyColumn");
    KeyGenerator keyGenerator;
    String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
    keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId,
            true);
    // 根据获得的keyStatementId对象生成对应的keyGenerator对象
    if (configuration.hasKeyGenerator(keyStatementId)) {
      keyGenerator = configuration.getKeyGenerator(keyStatementId);
    } else {
      keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
          configuration.isUseGeneratedKeys() && 
                  SqlCommandType.INSERT.equals(sqlCommandType))
          ? new Jdbc3KeyGenerator() : new NoKeyGenerator();
    }
    // 这里面实际上也都是将获得的resultMap、parameterMap和SqlSource等对象
    // 通过MappedStatement对象中的构造器依次赋值进去,里面没有特殊的处理逻辑
    builderAssistant.addMappedStatement(id, sqlSource, statementType, 
        sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, 
        resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered, 
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, 
        resultSets);
  }
  private void processSelectKeyNodes(String id, Class<?> parameterTypeClass,
           LanguageDriver langDriver) {
    // 从context节点中获取selectKey节点
    List<XNode> selectKeyNodes = context.evalNodes("selectKey");
    // 又是判断databaseId是否为空后续都会调用进parseSelectKeyNodes方法
    if (configuration.getDatabaseId() != null) {
      parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, 
              langDriver, configuration.getDatabaseId());
    }
    parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver,
            null);
    // 当selectKey节点被处理完之后便可以将此节点从context节点中删除了
    // 因为后续已经用不到了
    removeSelectKeyNodes(selectKeyNodes);
  }
  private void parseSelectKeyNodes(String parentId, List<XNode> list, 
          Class<?> parameterTypeClass, LanguageDriver langDriver, 
          String skRequiredDatabaseId) {
    // 可能有多个
    for (XNode nodeToHandle : list) {
      String id = parentId + SelectKeyGenerator.SELECT_KEY_SUFFIX;
      String databaseId = nodeToHandle.getStringAttribute("databaseId");
      // 如果前一条语句的databaseId不为空,则跳过此语句里面的判断
      if (databaseIdMatchesCurrent(id, databaseId, skRequiredDatabaseId)) {
        // 执行真正的selectKey标签
        parseSelectKeyNode(id, nodeToHandle, parameterTypeClass, 
                langDriver, databaseId);
      }
    }
  }
  private void parseSelectKeyNode(String id, XNode nodeToHandle, 
          Class<?> parameterTypeClass, LanguageDriver langDriver, 
                  String databaseId) {
    // 解析返回类型
    String resultType = nodeToHandle.getStringAttribute("resultType");
    Class<?> resultTypeClass = resolveClass(resultType);
    // 默认是PREPARED类型的
    StatementType statementType = StatementType.valueOf(
            nodeToHandle.getStringAttribute("statementType", 
                    StatementType.PREPARED.toString()));
    // 对应的property名称,如果需要赋值到字段为id的,这个值设置成id即可
    String keyProperty = nodeToHandle.getStringAttribute("keyProperty");
    // 对应从数据库查询出来的名称,如果要获取数据库查出来的字段,设置成字段名称
    // 即可获取
    String keyColumn = nodeToHandle.getStringAttribute("keyColumn");
    // 执行顺序,是在主要sql的前面(before)或者后面(after)执行
    boolean executeBefore = "BEFORE".equals(
            nodeToHandle.getStringAttribute("order", "AFTER"));
    // 设置查询参数,不使用缓存(因为是伴生sql)
    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;
    // 根据前面的langDriver的对象再获得SqlSource,因此langDriver的分析是必要的
    SqlSource sqlSource = langDriver.createSqlSource(configuration, 
            nodeToHandle, parameterTypeClass);
    // 只能是select
    SqlCommandType sqlCommandType = SqlCommandType.SELECT;
    // 直接根据前面的参数构造一个MappedStatement并添加进configuration中
    builderAssistant.addMappedStatement(id, sqlSource, statementType, 
        sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, 
        resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered,
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, 
        null);
    id = builderAssistant.applyCurrentNamespace(id, false);
    MappedStatement keyStatement = configuration.getMappedStatement(id, 
            false);
    // 生成一个KeyGenerator对象,并拥有MappedStatement对象和对应的id
    configuration.addKeyGenerator(id, new SelectKeyGenerator(keyStatement,
            executeBefore));
  }
}

先看入口方法parseStatementNode,此方法最开始会判断被封装的标签databaseId是否一致,如果不一致则返回。否则会开始解析标签的timeout、parameterMap、resultMap、resultType等属性,其中statementType属性默认是PREPARED预处理语句,而SqlCommandType将会根据标签转为大写后的值设置类型,并得到是否是插入的标识,而langDriver会被赋值成默认XMLLanguageDriver类型。

后续调用XMLIncludeTransformer类applyIncludes方法,在此方法中判断有include标签,则根据refid从sqlFragments集合中拿取对应的节点,最后将sql节点替换include标签,完成include标签功能。其关键源码如下:

public class XMLIncludeTransformer {
  public void applyIncludes(Node source) {
    Properties variablesContext = new Properties();
    Properties configurationVariables = configuration.getVariables();
    // 将configuration中variables对象的值添加到variablesContext对象中
    if (configurationVariables != null) {
      variablesContext.putAll(configurationVariables);
    }
    // 获得前面的变量后执行真正的操作方法
    applyIncludes(source, variablesContext, false);
  }
  private void applyIncludes(Node source, 
          final Properties variablesContext, boolean included) {
    // 这个方法实际上是一个递归方法,因此可以再include里面疯狂套娃都没事
    // 确保当前节点是include
    if (source.getNodeName().equals("include")) {
      // 从方法名字也可以看出来这个方法的大致作用,从sqlFragments集合对象中
      // 根据refid指向的<sql/>标签内容获得节点信息
      Node toInclude = findSqlFragment(getStringAttribute(source, "refid"),
        variablesContext);
      // 默认情况下variablesContext是空的,因此暂时不用看,有兴趣可以看下
      Properties toIncludeContext = getVariablesContext(source, 
        variablesContext);
      // 再递归toInclude对象标签
      applyIncludes(toInclude, toIncludeContext, true);
      if (toInclude.getOwnerDocument() != source.getOwnerDocument()) {
        toInclude = source.getOwnerDocument().importNode(toInclude, true);
      }
      // 前面递归已完成,现在到了最深层(即可处理的地方)
      source.getParentNode().replaceChild(toInclude, source);
      // 将toInclude节点插入到第一个节点前面,也就是把toInclude变成第一个节点
      while (toInclude.hasChildNodes()) {
        toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), 
          toInclude);
      }
      // 移除节点
      toInclude.getParentNode().removeChild(toInclude);
    } else if (source.getNodeType() == Node.ELEMENT_NODE) {
      // 如果source节点类型,则继续递归调用
      NodeList children = source.getChildNodes();
      for (int i = 0; i < children.getLength(); i++) {
        applyIncludes(children.item(i), variablesContext, included);
      }
    } else if (included && source.getNodeType() == Node.TEXT_NODE
        && !variablesContext.isEmpty()) {
      // 如果节点是纯文本,则替换所有文本节点中的变量,替换标识符为${}
      source.setNodeValue(PropertyParser.parse(source.getNodeValue(), 
        variablesContext));
    }
  }
}

流程如代码中的注释所述,include标签会一直进行递归调用,并把include这个递归链中${}可替换变量都使用variables对象中的值替换。

2.11 XMLScriptBuilder构造器创建SqlSource对象

在解析完include和selectKey之后,将会通过langDriver驱动器来创建sql语句SqlSource对象,先看一下调用进XMLScriptBuilder构造器的方法源码:

public class XMLLanguageDriver implements LanguageDriver {
  @Override
  public SqlSource createSqlSource(Configuration configuration, 
          XNode script, Class<?> parameterType) {
    XMLScriptBuilder builder = new XMLScriptBuilder(configuration, 
            script, parameterType);
    // 直接调用构造器解析节点
    return builder.parseScriptNode();
  }
}

具体的XMLScriptBuilder解析方法关键源码如下:

public class XMLScriptBuilder extends BaseBuilder {
    public SqlSource parseScriptNode() {
        // 解析动态XML标签节点并返回
        List<SqlNode> contents = parseDynamicTags(context);
        // 封装成混合SqlNode节点,后续分析SqlNode的时候可以再来分析这些
        MixedSqlNode rootSqlNode = new MixedSqlNode(contents);
        SqlSource sqlSource = null;
        // parseDynamicTags方法中会设置isDynamic对象值,根据不同的值再为
        // SqlSource创建不同的对象,并封装rootSqlNode
        if (isDynamic) {
            sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
        } else {
            sqlSource = new RawSqlSource(configuration, rootSqlNode, 
                    parameterType);
        }
        // 当获得具体的SqlSource类型,将会在进行数据库操作时解析,启动时不会
        return sqlSource;
    }
    List<SqlNode> parseDynamicTags(XNode node) {
        List<SqlNode> contents = new ArrayList<SqlNode>();
        // 获取Sql操作标签的所有子节点并遍历
        NodeList children = node.getNode().getChildNodes();
        for (int i = 0; i < children.getLength(); i++) {
            XNode child = node.newXNode(children.item(i));
            // 如果是文本节点
            if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE ||
                    child.getNode().getNodeType() == Node.TEXT_NODE) {
                String data = child.getStringBody("");
                TextSqlNode textSqlNode = new TextSqlNode(data);
                // 如果节点是动态的,则直接添加到contents集合中并设置isDynamic
                if (textSqlNode.isDynamic()) {
                    contents.add(textSqlNode);
                    isDynamic = true;
                } else {
                    // 如果是静态的,则使用StaticTextSqlNode封装并添加
                    contents.add(new StaticTextSqlNode(data));
                }
            } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE){
                // 如果是节点类型
                String nodeName = child.getNode().getNodeName();
                // 获取节点标签名称,并根据标签名称使用不同的handler处理器处理
                NodeHandler handler = nodeHandlers(nodeName);
                // 如果获取到的为空,则代表无法识别的标签类型
                if (handler == null) {
                    throw new BuilderException("Unknown element <"
                             + nodeName + "> in SQL statement.");
                }
                // 调用处理器的处理节点方法,不同的处理便不做具体分析了
                // 需要注意的便是contents会一直被传下去,因此这个对象中
                // 将包含了所有的标签对象节点
                handler.handleNode(child, contents);
                // 这种类型一定是动态的
                isDynamic = true;
            }
        }
        return contents;
    }
    NodeHandler nodeHandlers(String nodeName) {
        // 和if/else、switch一样的功能,借助HashMap实现条件判断
        Map<String, NodeHandler> map = new HashMap<String, NodeHandler>();
        map.put("trim", new TrimHandler());
        map.put("where", new WhereHandler());
        map.put("set", new SetHandler());
        map.put("foreach", new ForEachHandler());
        map.put("if", new IfHandler());
        map.put("choose", new ChooseHandler());
        map.put("when", new IfHandler());
        map.put("otherwise", new OtherwiseHandler());
        map.put("bind", new BindHandler());
        return map.get(nodeName);
    }
}

在此方法中,最核心的方法便是parseDynamicTags,此方法是解析mybatis动态sql的核心方法,再判断是否是字节点,如果是则从九种的动态标签根据名称拿取相应的处理器,并调用handleNode解析节点,将节点一次转换为SqlNode不同的子节点,随后返回SqlNode集合,并使用MixedSqlNode封装起来,根据是否是动态sql创建不同的SqlSource对象并返回。之后使用MappedStatement.Builder构造器构造MappedStatement对象。而bindMapperForNamespace方法则是为了将mapper.xml文件和mapper接口对应起来。

至此,mapper.xml文件中所涉及到的标签便已经全部处理完,并转换成了文章二所说的存储解析内容的对象,完成了执行Sql前的必要属性参数解析。

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值