Mybatis源码简单解读----构建

Mybatis源码简单解读—构建

参考源代码:Github搜索mybatis源码第一个,中文注释
同时maven下载mybatis较新版本的源码对照阅读
参考博客:https://www.cnblogs.com/javazhiyin/p/12340498.html
其他的小知识点也借鉴了很多其他博客的内容

首先mybatis的工作流程主要分为两个部分:

  • 构建(解析xml和注解,映射成对象形成配置类)
  • 执行(执行sql,完成jdbc与数据库交互)

这一部分只讲解mybatis的构建
单独使用mybatis框架的时候,会需要两个配置文件,分别是mybatis-config.xml和mapper.xml,采用官网给出案例mybatis的xml

我们不难看出,在mybatis-config.xml这个文件主要是用于配置数据源、配置别名、加载mapper.xml,并且我们可以看到这个文件的<mappers>节点中包含了一个<mapper>,而这个mapper所指向的路径就是另外一个xml文件:DemoMapper.xml,而这个文件中写了我们查询数据库所用的SQL。

而,MyBatis实际上就是将这两个xml文件,解析成配置对象,在执行中去使用它。

xml配置:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
  <environments default="development">
    <environment id="development">
      <transactionManager type="JDBC"/>
      <dataSource type="POOLED">
        <property name="driver" value="${driver}"/>
        <property name="url" value="${url}"/>
        <property name="username" value="${username}"/>
        <property name="password" value="${password}"/>
      </dataSource>
    </environment>
  </environments>
  <mappers>
    <mapper resource="org/mybatis/example/BlogMapper.xml"/>
  </mappers>
</configuration>
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.mybatis.example.BlogMapper">
  <select id="selectBlog" resultType="Blog">
    select * from Blog where id = #{id}
  </select>
</mapper>

使用mybatis,调用代码如下:

public static void main(String[] args) throws Exception {
    String resource = "org/mybatis/example/mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    //创建SqlSessionFacory
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    /******************************分割线******************************/
    SqlSession sqlSession = sqlSessionFactory.openSession();
    //获取Mapper
    try (SqlSession session = sqlSessionFactory.openSession()) {
      BlogMapper mapper = session.getMapper(BlogMapper.class);
      Blog blog = mapper.selectBlog(101);
	}
    sqlSession.commit();
  }

这段程序显示通过字节流读取了mybatis-config.xml文件,然后通过SqlSessionFactoryBuilder.build()方法,创建了一个SqlSessionFactory(这里用到了工厂模式和构建者模式),前面说过,MyBatis就是通过我们写的xml配置文件,来构建配置对象的,那么配置文件所在的地方,就一定是构建开始的地方,也就是build方法。

进入build方法


创建XMLConfigBuilder解析配置信息,之后调用parser()解析返回Configuration对象,调用build方法来创建SqlSessionFactoryfactory接口的默认实现DefaultSqlSessionFactory

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
      //创建出  XMLConfigBuilder
      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
      //解析xml文件分析
      return build(parser.parse());
        
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
      ErrorContext.instance().reset();
      try {
        inputStream.close();
      } catch (IOException e) {
        // Intentionally ignore. Prefer previous error.
      }
    }
  }
  //build()方法--构建者模式创建工厂对象
  public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
  }

XMLConfigBuilder类

创建XmlConfigbuilder对象,mybaitis中的构建起都继承了一个BaseBuilder的类,该类维护了三个变量

  • Configuration 配置类–主配置
  • TypeAliasRegistry 类型别名注册器
  • TypeHandlerRegistry 类型处理器注册器(用于类型转换)

configuration实际上就是一个维护了mybatis配置信息和执行相关的执行器和处理器的配置类(配置信息映射关系)

//上面6个构造函数最后都合流到这个函数,传入XPathParser
  private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
    //首先调用父类初始化Configuration
    super(new Configuration());
    //错误上下文设置成SQL Mapper Configuration(XML文件配置),以便后面出错了报错用吧
    ErrorContext.instance().resource("SQL Mapper Configuration");
    //将Properties全部设置到Configuration里面去
    this.configuration.setVariables(props);
    this.parsed = false;
    this.environment = environment;
    this.parser = parser;
  }

构建该类初始化了Configuration,用于后面解析配置类解析完注入,特别是多个继承了BaseBuilder的构建器都用来操作该Configuration

初始化完,调用parse()方法

//解析配置
  public Configuration parse() {
    //如果已经解析过了,报错
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
//  <?xml version="1.0" encoding="UTF-8" ?> 
//  <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" 
//  "http://mybatis.org/dtd/mybatis-3-config.dtd"> 
//  <configuration> 
//  <environments default="development"> 
//  <environment id="development"> 
//  <transactionManager type="JDBC"/> 
//  <dataSource type="POOLED"> 
//  <property name="driver" value="${driver}"/> 
//  <property name="url" value="${url}"/> 
//  <property name="username" value="${username}"/> 
//  <property name="password" value="${password}"/> 
//  </dataSource> 
//  </environment> 
//  </environments>
//  <mappers> 
//  <mapper resource="org/mybatis/example/BlogMapper.xml"/> 
//  </mappers> 
//  </configuration>
    
    //根节点是configuration
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }

该配置解析了主配置类中的configuration节点

xml解析使用了java提供的xml解析工具,mybatis进一步封装了相关的解析方法,原理就是解析xml语法树


初始化生成Configuration流程图

在这里插入图片描述

进一步查看parser.evalNode("/configuration")

该方法分步骤解析根节点下的配置信息(使用 Xpath解析器来解析)

//解析配置
  private void parseConfiguration(XNode root) {
    try {
      //分步骤解析
      //issue #117 read properties first
      //1.properties
      propertiesElement(root.evalNode("properties"));
      //2.类型别名
      typeAliasesElement(root.evalNode("typeAliases"));
      //3.插件
      pluginElement(root.evalNode("plugins"));
      //4.对象工厂
      objectFactoryElement(root.evalNode("objectFactory"));
      //5.对象包装工厂
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      //6.设置
      settingsElement(root.evalNode("settings"));
      // read it after objectFactory and objectWrapperFactory issue #631
      //7.环境
      environmentsElement(root.evalNode("environments"));
      //8.databaseIdProvider
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      //9.类型处理器
      typeHandlerElement(root.evalNode("typeHandlers"));
      //10.映射器
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }

解析映射器Mappers

其他部分代码不是主要了解内容,我们主要了解mapper映射器以及映射器对应xxxMapper.xml文件时是如何被解析的

跟踪映射器部分代码
mapperElement(root.evalNode(“mappers”));

首先我们先了解映射器的几种配置,查看官方文档可知有如下配置:

<!-- 使用相对于类路径的资源引用 -->
<mappers>
  <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
  <mapper resource="org/mybatis/builder/BlogMapper.xml"/>
  <mapper resource="org/mybatis/builder/PostMapper.xml"/>
</mappers>
<!-- 使用完全限定资源定位符(URL) -->
<mappers>
  <mapper url="file:///var/mappers/AuthorMapper.xml"/>
  <mapper url="file:///var/mappers/BlogMapper.xml"/>
  <mapper url="file:///var/mappers/PostMapper.xml"/>
</mappers>
<!-- 使用映射器接口实现类的完全限定类名 -->
<mappers>
  <mapper class="org.mybatis.builder.AuthorMapper"/>
  <mapper class="org.mybatis.builder.BlogMapper"/>
  <mapper class="org.mybatis.builder.PostMapper"/>
</mappers>
<!-- 将包内的映射器接口实现全部注册为映射器 -->
<mappers>
  <package name="org.mybatis.builder"/>
</mappers>

mapperElement()方法中也分别解析了下面几种配置方法

private void mapperElement(XNode parent) throws Exception {
  if (parent != null) {
    for (XNode child : parent.getChildren()) {
      if ("package".equals(child.getName())) {
        //10.4自动扫描包下所有映射器
        String mapperPackage = child.getStringAttribute("name");
        configuration.addMappers(mapperPackage);
      } else {
        String resource = child.getStringAttribute("resource");
        String url = child.getStringAttribute("url");
        String mapperClass = child.getStringAttribute("class");
        if (resource != null && url == null && mapperClass == null) {
          //10.1使用类路径
          ErrorContext.instance().resource(resource);
          InputStream inputStream = Resources.getResourceAsStream(resource);
          //映射器比较复杂,调用XMLMapperBuilder
          //注意在for循环里每个mapper都重新new一个XMLMapperBuilder,来解析
          XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
          mapperParser.parse();
        } else if (resource == null && url != null && mapperClass == null) {
          //10.2使用绝对url路径
          ErrorContext.instance().resource(url);
          InputStream inputStream = Resources.getUrlAsStream(url);
          //映射器比较复杂,调用XMLMapperBuilder
          XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
          mapperParser.parse();
        } else if (resource == null && url == null && mapperClass != null) {
          //10.3使用java类名
          Class<?> mapperInterface = Resources.classForName(mapperClass);
          //直接把这个映射加入配置
          configuration.addMapper(mapperInterface);
        } else {
          throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
        }
      }
    }
  }
}

分析上面代码可以看出:

  • 映射器配置 使用类接口的-----直接调用addMapper()方法
  • 映射器配置 使用xxxMapper.xml则调用XmlMapper来解析

分别分析这两种方式的

Mapper配置使用类接口

跟踪源码

MapperRegistry

public void addMappers(String packageName) {
  mapperRegistry.addMappers(packageName);
}
//查找包下所有是superType的类
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
for (Class<?> mapperClass : mapperSet) {
  addMapper(mapperClass);
}
//看一下如何添加一个映射
public <T> void addMapper(Class<T> type) {
    //mapper必须是接口!才会添加
    if (type.isInterface()) {
      if (hasMapper(type)) {
        //如果重复添加了,报错
        throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
      }
      boolean loadCompleted = false;
      try {
        knownMappers.put(type, new MapperProxyFactory<T>(type));
       //在运行解析器之前添加类型很重要
	   //否则,映射器解析器可能会自动尝试绑定。 如果类型是已知的,则不会尝试。
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        parser.parse();
        loadCompleted = true;
      } finally {
        //如果加载过程中出现异常需要再将这个mapper从mybatis中删除,这种方式比较丑陋吧,难道是不得已而为之?
        if (!loadCompleted) {
          knownMappers.remove(type);
        }
      }
    }
  }

从这里发现映射器的关系通过MapperRegistry注册机来维护,该类维护了配置类和映射关系map,

public class MapperRegistry {

  private Configuration config;
  //将已经添加的映射都放入HashMap
  private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>();

  public MapperRegistry(Configuration config) {
    this.config = config;
  }
  ...
}

将类加入注册机后,使用MapperAnnotationBuilder 来解析parse()

查看该方法

public void parse() {
  //type即接口类,resource为该类全路径
  String resource = type.toString();
  if (!configuration.isResourceLoaded(resource)) {
    //解析对应xxxMapper.xml,并标识为已加载了
    loadXmlResource();
    configuration.addLoadedResource(resource);
    assistant.setCurrentNamespace(type.getName());
    //缓存
    parseCache();
    parseCacheRef();
    //解析类上的接口
    Method[] methods = type.getMethods();
    for (Method method : methods) {
      try {
        // 如果不是桥接方法
        if (!method.isBridge()) {
          //解析method中的sql注解  
          parseStatement(method);
        }
      } catch (IncompleteElementException e) {  
        configuration.addIncompleteMethod(new MethodResolver(this, method));
      } 
    }
  }
  parsePendingMethods();
}

Method.isBrige()桥接方法

查看loadXmlResource

private void loadXmlResource() {
  if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
    //查找同包下的xml文件,并解析  
    String xmlResource = type.getName().replace('.', '/') + ".xml";
    InputStream inputStream = type.getResourceAsStream("/" + xmlResource);
    if (inputStream == null) {
      // Search XML mapper that is not in the module but in the classpath.
      try {
        inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
      } catch (IOException e2) {
        // ignore, resource is not required
      }
    }
    if (inputStream != null) {
      //构建xml解析工具来解析  
      XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
      xmlParser.parse();
    }
  }
}

代码分析到这里,我们可以看出大概的流程:

  1. 将该类接口注入MapperRegistry中,通过Map<Class<?>, MapperProxyFactory<?>> knownMappers集合来维护,把类接口和新生成的MapperProxyFactory代理工厂作为键值对
  2. 解析该接口类下的xml文件
  3. 解析类接口上的注解

Mapper配置使用xml文件

xml文件的方式首先通过XMLMapperBuilder解析xml文件【该类也在接口方式中调用】

这里跟踪下XMLMapperBuilder是如何解析信息的

public void parse() {
    //判断文件是否之前解析过
    if (!configuration.isResourceLoaded(resource)) {
        //解析mapper文件节点(主要)(下面贴了代码)
      configurationElement(parser.evalNode("/mapper"));
      configuration.addLoadedResource(resource);
      //绑定Namespace里面的Class对象---实际上就是添加到MapperRegistry中
      bindMapperForNamespace();
    }
    //重新解析之前解析不了的节点
    parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();
  }


//解析mapper文件里面的节点
// 拿到里面配置的配置项 最终封装成一个MapperedStatemanet
private void configurationElement(XNode context) {
  try {
      //获取命名空间 namespace,这个很重要,后期mybatis会通过这个动态代理我们的Mapper接口
    String namespace = context.getStringAttribute("namespace");
    if (namespace == null || namespace.equals("")) {
        //如果namespace为空则抛一个异常
      throw new BuilderException("Mapper's namespace cannot be empty");
    }
    builderAssistant.setCurrentNamespace(namespace);
    //解析缓存节点
    cacheRefElement(context.evalNode("cache-ref"));
    cacheElement(context.evalNode("cache"));

    //解析parameterMap(过时)和resultMap  <resultMap></resultMap>
    parameterMapElement(context.evalNodes("/mapper/parameterMap"));
    resultMapElements(context.evalNodes("/mapper/resultMap"));
    //解析<sql>节点 
    //<sql id="staticSql">select * from test</sql> (可重用的代码段)
    //<select> <include refid="staticSql"></select>
    sqlElement(context.evalNodes("/mapper/sql"));
    //解析增删改查节点<select> <insert> <update> <delete>
    buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
  } catch (Exception e) {
    throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
  }
}

在这个parse()方法中,调用了一个configuationElement代码,用于解析XXXMapper.xml文件中的各种节点,包括<cache><cache-ref><paramaterMap>(已过时)、<resultMap><sql>、还有增删改查节点,和上面相同的是,我们也挑一个主要的来说,因为解析过程都大同小异。

同时将xml文件上namespace对应的接口注册到MappperRegistry注册机中。

这里我们介绍下增删改查节点的方法——buildStatementFromContext(),和JDBC一样该Statement就是操作数据库的对象

  //7.配置select|insert|update|delete
  private void buildStatementFromContext(List<XNode> list) {
    //调用7.1构建语句
    if (configuration.getDatabaseId() != null) {
      buildStatementFromContext(list, configuration.getDatabaseId());
    }
    buildStatementFromContext(list, null);
  }

  //7.1构建语句
  private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    for (XNode context : list) {
      //构建所有语句,一个mapper下可以有很多select
      //语句比较复杂,核心都在这里面,所以调用XMLStatementBuilder
      final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
      try {
          //核心XMLStatementBuilder.parseStatementNode--这部分和类接口中解析sql注解的逻辑一样
        statementParser.parseStatementNode();
      } catch (IncompleteElementException e) {
          //如果出现SQL语句不完整,把它记下来,塞到configuration去
        configuration.addIncompleteStatement(statementParser);
      }
    }
  }


public void parseStatementNode() {
    //获取<select id="xxx">中的id
    String id = context.getStringAttribute("id");
    //获取databaseId 用于多数据库,这里为null
    String databaseId = context.getStringAttribute("databaseId");

    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
      return;
    }
    //获取节点名  select update delete insert
    String nodeName = context.getNode().getNodeName();
    //根据节点名,得到SQL操作的类型
    SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
    //判断是否是查询
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    //是否刷新缓存 默认:增删改刷新 查询不刷新
    boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
    //是否使用二级缓存 默认值:查询使用 增删改不使用
    boolean useCache = context.getBooleanAttribute("useCache", isSelect);
    //是否需要处理嵌套查询结果 group by

    // 三组数据 分成一个嵌套的查询结果
    boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

    // Include Fragments before parsing
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    //替换Includes标签为对应的sql标签里面的值
    includeParser.applyIncludes(context.getNode());

    //获取parameterType名
    String parameterType = context.getStringAttribute("parameterType");
    //获取parameterType的Class
    Class<?> parameterTypeClass = resolveClass(parameterType);

    //解析配置的自定义脚本语言驱动 这里为null
    String lang = context.getStringAttribute("lang");
    LanguageDriver langDriver = getLanguageDriver(lang);

    // Parse selectKey after includes and remove them.
    //解析selectKey
    processSelectKeyNodes(id, parameterTypeClass, langDriver);

    // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
    //设置主键自增规则
    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))
          ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
    }
/************************************************************************************/
    //解析Sql(重要)  根据sql文本来判断是否需要动态解析 如果没有动态sql语句且 只有#{}的时候 直接静态解析使用?占位 当有 ${} 不解析
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    //获取StatementType,可以理解为Statement和PreparedStatement
    StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    //没用过
    Integer fetchSize = context.getIntAttribute("fetchSize");
    //超时时间
    Integer timeout = context.getIntAttribute("timeout");
    //已过时
    String parameterMap = context.getStringAttribute("parameterMap");
    //获取返回值类型名
    String resultType = context.getStringAttribute("resultType");
    //获取返回值烈性的Class
    Class<?> resultTypeClass = resolveClass(resultType);
    //获取resultMap的id
    String resultMap = context.getStringAttribute("resultMap");
    //获取结果集类型
    String resultSetType = context.getStringAttribute("resultSetType");
    ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
    if (resultSetTypeEnum == null) {
      resultSetTypeEnum = configuration.getDefaultResultSetType();
    }
    String keyProperty = context.getStringAttribute("keyProperty");
    String keyColumn = context.getStringAttribute("keyColumn");
    String resultSets = context.getStringAttribute("resultSets");

    //将刚才获取到的属性,封装成MappedStatement对象(代码贴在下面)
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered,
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
  }

//将刚才获取到的属性,封装成MappedStatement对象
  public MappedStatement addMappedStatement(
      String id,
      SqlSource sqlSource,
      StatementType statementType,
      SqlCommandType sqlCommandType,
      Integer fetchSize,
      Integer timeout,
      String parameterMap,
      Class<?> parameterType,
      String resultMap,
      Class<?> resultType,
      ResultSetType resultSetType,
      boolean flushCache,
      boolean useCache,
      boolean resultOrdered,
      KeyGenerator keyGenerator,
      String keyProperty,
      String keyColumn,
      String databaseId,
      LanguageDriver lang,
      String resultSets) {

    if (unresolvedCacheRef) {
      throw new IncompleteElementException("Cache-ref not yet resolved");
    }

    //id = namespace
    id = applyCurrentNamespace(id, false);
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;

      //通过构造者模式+链式变成,构造一个MappedStatement的构造者
    MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
        .resource(resource)
        .fetchSize(fetchSize)
        .timeout(timeout)
        .statementType(statementType)
        .keyGenerator(keyGenerator)
        .keyProperty(keyProperty)
        .keyColumn(keyColumn)
        .databaseId(databaseId)
        .lang(lang)
        .resultOrdered(resultOrdered)
        .resultSets(resultSets)
        .resultMaps(getStatementResultMaps(resultMap, resultType, id))
        .resultSetType(resultSetType)
        .flushCacheRequired(valueOrDefault(flushCache, !isSelect))
        .useCache(valueOrDefault(useCache, isSelect))
        .cache(currentCache);

    ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
    if (statementParameterMap != null) {
      statementBuilder.parameterMap(statementParameterMap);
    }

      //通过构造者构造MappedStatement
    MappedStatement statement = statementBuilder.build();
     //将MappedStatement对象封装到Configuration对象中
    configuration.addMappedStatement(statement);
    return statement;
  }

将xml中的节点解析,并封装一个MappedStatement对象,并添加在Configuration中和Map集合中

构建过程流程图:
在这里插入图片描述

SQL语句解析

在刚才过程中包含了SQL语句的生成,在这里进一步分析

//解析Sql(重要)  根据sql文本来判断是否需要动态解析 如果没有动态sql语句且 只有#{}的时候 直接静态解析使用?占位 当有 ${} 不解析
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);

这里就是生成Sql的入口,以单步调试的角度接着往下看。

/*进入createSqlSource方法*/
@Override
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
    //进入这个构造
    XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
    //进入parseScriptNode
    return builder.parseScriptNode();
}
/**
进入这个方法
*/
public SqlSource parseScriptNode() {
    //#
    //会先解析一遍
    MixedSqlNode rootSqlNode = parseDynamicTags(context);
    SqlSource sqlSource;
    if (isDynamic) {
      //如果是${}会直接不解析,等待执行的时候直接赋值
      sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {
      //用占位符方式来解析  #{} --> ?
      sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    return sqlSource;
}
protected MixedSqlNode parseDynamicTags(XNode node) {
    List<SqlNode> contents = new ArrayList<>();
    //获取select标签下的子标签
    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) {
          //如果是查询
        //获取原生SQL语句 这里是 select * from test where id = #{id}
        String data = child.getStringBody("");
        TextSqlNode textSqlNode = new TextSqlNode(data);
        //检查sql是否是${}
        if (textSqlNode.isDynamic()) {
            //如果是${}那么直接不解析
          contents.add(textSqlNode);
          isDynamic = true;
        } else {
            //如果不是,则直接生成静态SQL
            //#{} -> ?
          contents.add(new StaticTextSqlNode(data));
        }
      } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
          //如果是增删改
        String nodeName = child.getNode().getNodeName();
        NodeHandler handler = nodeHandlerMap.get(nodeName);
        if (handler == null) {
          throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
        }
        handler.handleNode(child, contents);
        isDynamic = true;
      }
    }
    return new MixedSqlNode(contents);
  }
/*从上面的代码段到这一段中间需要经过很多代码,就不一段一段贴了*/
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
    ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
    //这里会生成一个GenericTokenParser,传入#{}作为开始和结束,然后调用其parse方法,即可将#{}换为 ?
    GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
    //这里可以解析#{} 将其替换为?
    String sql = parser.parse(originalSql);
    return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
  }

//经过一段复杂的解析过程
public String parse(String text) {
    if (text == null || text.isEmpty()) {
      return "";
    }
    // search open token
    int start = text.indexOf(openToken);
    if (start == -1) {
      return text;
    }
    char[] src = text.toCharArray();
    int offset = 0;
    final StringBuilder builder = new StringBuilder();
    StringBuilder expression = null;
    //遍历里面所有的#{} select ?  ,#{id1} ${}
    while (start > -1) {
      if (start > 0 && src[start - 1] == '\') {
        // this open token is escaped. remove the backslash and continue.
        builder.append(src, offset, start - offset - 1).append(openToken);
        offset = start + openToken.length();
      } else {
        // found open token. let's search close token.
        if (expression == null) {
          expression = new StringBuilder();
        } else {
          expression.setLength(0);
        }
        builder.append(src, offset, start - offset);
        offset = start + openToken.length();
        int end = text.indexOf(closeToken, offset);
        while (end > -1) {
          if (end > offset && src[end - 1] == '\') {
            // this close token is escaped. remove the backslash and continue.
            expression.append(src, offset, end - offset - 1).append(closeToken);
            offset = end + closeToken.length();
            end = text.indexOf(closeToken, offset);
          } else {
            expression.append(src, offset, end - offset);
            break;
          }
        }
        if (end == -1) {
          // close token was not found.
          builder.append(src, start, src.length - start);
          offset = src.length;
        } else {
            //使用占位符 ?
            //注意handler.handleToken()方法,这个方法是核心
          builder.append(handler.handleToken(expression.toString()));
          offset = end + closeToken.length();
        }
      }
      start = text.indexOf(openToken, offset);
    }
    if (offset < src.length) {
      builder.append(src, offset, src.length - offset);
    }
    return builder.toString();
}

//BindingTokenParser 的handleToken
//当扫描到${}的时候调用此方法  其实就是不解析 在运行时候在替换成具体的值
@Override
public String handleToken(String content) {
  this.isDynamic = true;
  return null;
}
//ParameterMappingTokenHandler的handleToken
//全局扫描#{id} 字符串之后  会把里面所有 #{} 调用handleToken 替换为?
@Override
public String handleToken(String content) {
      parameterMappings.add(buildParameterMapping(content));
      return "?";
}

首先这里会通过<select>节点获取到我们的SQL语句,假设SQL语句中只有${},那么直接就什么都不做,在运行的时候直接进行赋值。

而如果扫描到了#{}字符串之后,会进行替换,将#{}替换为 ?

那么他是怎么进行判断的呢?

这里会生成一个GenericTokenParser,这个对象可以传入一个openToken和closeToken,如果是#{},那么openToken就是#{,closeToken就是 },然后通过parse方法中的handler.handleToken()方法进行替换。

在这之前由于已经进行过SQL是否含有#{}的判断了,所以在这里如果是只有${},那么handler就是BindingTokenParser的实例化对象,如果存在#{},那么handler就是ParameterMappingTokenHandler的实例化对象。

分别进行处理。

小结

至此整个MyBatis的查询前构建的过程就基本说完了,简单地总结就是,MyBatis会在执行查询之前,对配置文件进行解析成配置对象:Configuration,以便在后面执行的时候去使用,而存放SQL的xml又会解析成MappedStatement对象,但是最终这个对象也会加入Configuration中,将Configuration对象通过build()方法来创建工厂对象

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值