Mybatis源码解析《一》

导语

在当前的日常开发中,mybatis这样的一个框架的使用,是很多程序员都无法避开的。大多数人都知道mybatis 的作用是为了避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。因为在开始接触使用Java操作数据库的时候,我们都是使用JDBC的。

自从有了持久化框架之后,使用持久化框架已经是“理所当然”的了,虽然我们已经脱离了使用JDBC是阶段了,但是这毕竟是基础的知识,所以本篇文章将会从JDBC入手。其实Mybatis就是对JDBC进行的封装。那就言归正传吧!

一、原始JDBC的使用

废话不多数,先来段代码来说明问题:

public class TestMain {
    public static void main(String[] args) throws Exception {
        Class.forName("com.mysql.jdbc.Driver");
        Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "zfy123456");
        connection.setAutoCommit(false);
        PreparedStatement ps = connection.prepareStatement("insert into dept values(?,?,?)");
        ps.setInt(1,10000);
        ps.setString(2,"test");
        ps.setString(3,"test");
        try{
            ps.executeUpdate();
        }catch (Exception e) {
            connection.rollback();
            e.printStackTrace();
        }finally {
            if(ps != null) {
                ps.close();
            }
            if (connection != null) {
                connection.close();
            }
        }


    }
}

对于上面的代码中其大体流程就是:

  1. 加载驱动并进行初始化
  2. 连接数据库
  3. 执行SQL语句
  4. 处理数据库响应并返回的结果
  5. 最后释放资源

二、Mybatis操作数据库

mybatis学习日常文档:http://www.mybatis.org/mybatis-3/zh/index.html,以下代码参考mybatis官网。

测试类:

public class MybatisTest {

    @Test
    public void test() throws Exception {

        User user = new User();
        user.setAddress("北京市海淀区");
        user.setBirthday(new Date(2000-10-01));
        user.setSex("男");
        user.setUsername("李清源");

        InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        SqlSession sqlSession = sqlSessionFactory.openSession();
        sqlSession.insert("insertUser", user);
        sqlSession.commit();
        sqlSession.close();

    }
}

实体类:

public class User {
    private int id;
    private String username;
    private Date birthday;
    private String sex;
    private String address;
    // 省略get、set、toString方法
}

Mapper接口:

public interface UserMapper {
    void insertUser(User user) throws Exception;
}

Mapper配置文件:

<?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="com.zfy.mybatis.mapper.UserMapper">
    <insert id="insertUser">
      insert into user(username,birthday,sex,address) values (#{username},#{birthday},#{sex},#{address})
    </insert>
</mapper>

mybatis配置文件:

<?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>

    <!-- 数据库连接配置文件 -->
    <properties resource="config.properties"> </properties>
    <settings>
        <setting name="mapUnderscoreToCamelCase" value="true"/>
    </settings>

    <typeAliases>
        <package name="com.zfy.mybatis.bean"/>
    </typeAliases>

    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="${jdbc.driver}"/>
                <property name="url" value="${jdbc.url}"/>
                <property name="username" value="${jdbc.username}"/>
                <property name="password" value="${jdbc.password}"/>
            </dataSource>
        </environment>
    </environments>

    <mappers>
        <package name="com.zfy.mybatis.mapper"/>
    </mappers>
</configuration>

此处由于代码太多就先省略config.properties配置文件,网上可自己参考。从上面的测试类代码中可以看出,mybatis的操作流程大体如下:

  • 读取mybatis配置文件
  • 使用SqlSessionFactoryBuilder中的build方法根据读取到的文件流信息,创建Configuration对象,并将数据存储在其中。
  • 再创建SqlSession对象提供属性
  • 执行SQL
  • SqlSession.commit()
  • SqlSession.close()

三、mybatis核心配置文件的加载

对于Resources.getResourceAsStream("mybatis-config.xml")代码中,关于对配置文件加载成输入流的代码,就不赘述了,直接来看SqlSessionFactoryBuilder中的build方法吧。来看看mybatis的核心配置文件时如何被加载的。那就先来看下build方法的源码:

SqlSessionFactoryBuilder.java
  // 调用读取流的方法入口,这里的读取流就是指向所创建的工程中的核心配置文件
  public SqlSessionFactory build(InputStream inputStream) {
    // 调用重载的build方法,这三个参数的含义分别是:读取的配置文件的信息、将要指定的环境、所要使用的web的属性文件,
    // 不过这里后面两个参数都是为null
    return build(inputStream, null, null);
  }
  public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
      // 首先创建XML解析对象,这对象实际上是对XPathParser封装的工具对象,这个对象主要是针对核心配置文件进行相关读取的
      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
      // parser.parse()才是对XML进行真正的解析,解析完之后然后调用重载方法把parser对象放到DefaultSqlSessionFactory中去
      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.
      }
    }
  }

上面的代码中,首先通过所传入的inputStream, environment, properties这几个参数创建XMLConfigBuilder 对象,然后嗲用这个对象中的parse()方法来进行解析,最后把解析完的对象放到DefaultSqlSessionFactory对象中去。代码如下:

  public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
  }

前面说到parser.parse()才是对mybatis核心配置文件的解析,那么继续看这个方法的代码到底做了什么。代码如下:

XMLConfigBuilder.java 
  public Configuration parse() {
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    // parser.evalNode("/configuration")是为了定位核心配置文件中'configuration'元素的节点(根目录标签)
    // 在获得根标签之后,然后对根标签下的信息进行解析
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }

上面代码中首先只是做了一个防止多线程加载的操作,然后把parsed 设置为true,然后定位到mybatis核心配置文件的根标签configuration,在定位好根标签后,再对其根标签下的所有字标签进行逐一的解析。最后返回一个configuration对象。那来继续看下parseConfiguration方法中是如何解析根标签下的所有字标签。这里就先只对mappers字标签进行解析,对于上面开始的核心配置代码中的properties、typeAliases、environments,就先不赘述了,否则本篇文章将会太长。先不多说,继续看代码:

  private void parseConfiguration(XNode root) {
    try {
      //issue #117 read properties first
      // 对核心文件中的各种标签进行解析
      propertiesElement(root.evalNode("properties"));
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      loadCustomVfs(settings);
      typeAliasesElement(root.evalNode("typeAliases"));
      pluginElement(root.evalNode("plugins"));
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      settingsElement(settings);
      // read it after objectFactory and objectWrapperFactory issue #631
      environmentsElement(root.evalNode("environments"));
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      typeHandlerElement(root.evalNode("typeHandlers"));
      // 解析mappers子标签
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }

上面的代码中就是对configuration根标签下的所有字标签进行解析的,这里就以mappers标签的解析为例。那来继续看下 mapperElement(root.evalNode("mappers"))方法:

  private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      // 因为mapper标签中的子标签存在两种写法,分别是:package、mapper
      for (XNode child : parent.getChildren()) {
        // 如果子标签的存在"package"的名称,则走此段代码
        if ("package".equals(child.getName())) {
          String mapperPackage = child.getStringAttribute("name");
          // 在和获取mapperPackage信息后,然后把它添加到configuration对象中,其实mapperPackage就是当前文件的路径
          configuration.addMappers(mapperPackage);
        } else {
          String resource = child.getStringAttribute("resource");
          String url = child.getStringAttribute("url");
          String mapperClass = child.getStringAttribute("class");
          // 在获取到resource、url、mapperClass信息之后,下面便对这些信息是否存在进行判断,然后走相应的逻辑代码
          if (resource != null && url == null && mapperClass == null) {
            ErrorContext.instance().resource(resource);
            InputStream inputStream = Resources.getResourceAsStream(resource);
            // 当resource != null时,在获取到对应的resource信息,后然后放到新建的XMLMapperBuilder对象中
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
            // 最后通过mapperParser对象去解析,这后面所做的一切工作就是把mapper文件中的信息解析出来后,然后放到configuration对象中去,为后续最准备
            mapperParser.parse();
          } else if (resource == null && url != null && mapperClass == null) {
            ErrorContext.instance().resource(url);
            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<?> mapperInterface = Resources.classForName(mapperClass);
            // 同"package".equals(child.getName())的情况
            configuration.addMapper(mapperInterface);
          } else {
            throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
          }
        }
      }
    }
}

这段代码首先判断parent != null的情况下,才执行后续操作,否则则什么都不做。当不为null的情况下,便开始对mappers下的所有子标签进行遍历并解析。后面的操作大体流程就是,如果子标签的存在"package"的名称,则执行相关代码,否则进入else代码,在else代码块中,先获取esource、url、mapperClass,然后对各自是否为空的条件,执行相关代码。具体看代码中的注释。因为开始所给的配置文件中的mappers标签下的子标签是package,所有这里我们就只对这个逻辑下的代码进行解析。

当子标签是package时,先获取其mapperPackage,然后放到mapper里。这里主要的工作在onfiguration.addMappers(mapperPackage),那就继续看下这块代码。

Configuration.java 

  public void addMappers(String packageName) {
    mapperRegistry.addMappers(packageName);
  }

MapperRegistry.java

  public void addMappers(String packageName) {
    addMappers(packageName, Object.class);
  }
  public void addMappers(String packageName, Class<?> superType) {
    ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
    // 获取此路径下后缀为.lass 的文件
    resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
    Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
    for (Class<?> mapperClass : mapperSet) {
      addMapper(mapperClass);
    }
  }

上面代码中,先获取后缀为.lass 的文件,然后再把这些文件信息传递给Set,最后遍历Set,同时调用addMapper(mapperClass)方法。

  public <T> void addMapper(Class<T> type) {
    // 判断所获取到的类类型是否为Interface类型
    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));
        // It's important that the type is added before the parser is run
        // otherwise the binding may automatically be attempted by the
        // mapper parser. If the type is already known, it won't try.
        // 将type和config信息放到新建的MapperAnnotationBuilder对象中,config中主要包含environment这些信息
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        // 然后继续解析
        parser.parse();
        loadCompleted = true;
      } finally {
        if (!loadCompleted) {
          knownMappers.remove(type);
        }
      }
    }
  }

上述代码中,首先判断锁获取到的类是否为接口类型的,如果是则先把loadCompleted 为false,然后以type为key放到knownMappers中去,后面再通过config和type创建MapperAnnotationBuilder,这里这个对象是设计注解的,因为我们没有用到注解,这里就不赘述了。那就看下parser.parse()的代码是如何做操作的。

MapperAnnotationBuilder.java
  public void parse() {
    String resource = type.toString();
    // 判断configuration是否包含resource信息
    if (!configuration.isResourceLoaded(resource)) {
      // 重点:这里才真正的加载后缀为.xml文件的信息
      loadXmlResource();
      // 把resource添加到configuration中
      configuration.addLoadedResource(resource);
      // 设置MapperBuilderAssistant当前的namespace
      assistant.setCurrentNamespace(type.getName());
      // 解析缓存
      parseCache();
      // 解析缓存引用
      parseCacheRef();
      Method[] methods = type.getMethods();
      // 对接口中的方法进行解析
      for (Method method : methods) {
        try {
          // issue #237
          if (!method.isBridge()) {
            parseStatement(method);
          }
        } catch (IncompleteElementException e) {
          configuration.addIncompleteMethod(new MethodResolver(this, method));
        }
      }
    }
    // 解析方法
    parsePendingMethods();
  }

上面代码的逻辑很清晰,首先判断configuration是否包含resource信息,如果不包含,那么就继续后续的流程。当进入后续流程时,首先就是加载xml,这里的就开始正式的加载mapper的xml了。然后再进行一些设置和解析缓存等一些东西。这里主要看下loadXmlResource()这个方法:

  private void loadXmlResource() {
    // Spring may not know the real resource name so we check a flag
    // to prevent loading again a resource twice
    // this flag is set at XMLMapperBuilder#bindMapperForNamespace
    // 判断configuration中是否包含了"namespace:" + type.getName())的mapper文件
    if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
      // 获取到后缀信息为.xml的文件路径
      String xmlResource = type.getName().replace('.', '/') + ".xml";
      InputStream inputStream = null;
      try {
        // 获取mapper文件流
        inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
      } catch (IOException e) {
        // ignore, resource is not required
      }
      if (inputStream != null) {
        // 如果流信息不为空,把流信息、assistant.getConfiguration()、xmlResource、configuration.getSqlFragments()和type的name放到XMLMapperBuilder中
        XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
        // 然后进行解析
        xmlParser.parse();
      }
    }
  }

这里也没有什么复杂的逻辑,无非还是先判断下是否包含,然后获取到后缀为.xml的文件,当获取到后再通过锁获取到的xmlResource和ype.getClassLoader()这两个参数获取文件的输入流,在获得输入流后,再通过输入流等一些相关信息,去新建一个xml的解析对象,新建完成后便通过此对象中的解析方法去解析,那就来看看这个方法:

XMLMapperBuilder.java
  public void parse() {
    // 判断resource是否在configuration中
    if (!configuration.isResourceLoaded(resource)) {
      // 1.首先定位到mapper文件中的根节点mapper,2.然后对该节点下的所有节点进行解析
      configurationElement(parser.evalNode("/mapper"));
      configuration.addLoadedResource(resource);
      // 绑定mapper到工作空间
      bindMapperForNamespace();
    }

    // 解析mapper文件中ResultMaps节点下的信息
    parsePendingResultMaps();
    // 解析缓存引用
    parsePendingCacheRefs();
    // 解析Statement
    parsePendingStatements();
  }

这段代码很简单,无非就是一些方法调用的逻辑,但这里所要关注的重点还是configurationElement(parser.evalNode("/mapper"))这个方法,这里便开始对于每个mapper文件的正式调用了,来看看这个方法中具体做了什么。代码如下:

  private void configurationElement(XNode context) {
    try {
      // 获取mapper节点的namespace
      String namespace = context.getStringAttribute("namespace");
      if (namespace == null || namespace.equals("")) {
        throw new BuilderException("Mapper's namespace cannot be empty");
      }
      builderAssistant.setCurrentNamespace(namespace);
      // 接下来就是对mapper节点下的各种节点进行解析了,不准备赘述,但或许讲下buildStatementFromContext方法
      cacheRefElement(context.evalNode("cache-ref"));
      cacheElement(context.evalNode("cache"));
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      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);
    }
  }

上面代码中,基本上就是对mapper配置文件中的一些标签的解析,由于我开始所提供的配置文件中之涉及了<insert标签,那么在这里还是只关注这个标签的解析代码,请看代码:

  private void buildStatementFromContext(List<XNode> list) {
    // 如果configuration.getDatabaseId() != null,走此段代码,否则跳过
    if (configuration.getDatabaseId() != null) {
      buildStatementFromContext(list, configuration.getDatabaseId());
    }
    // 上面的判断是判断是否存在其他数据源的设置,我们这里没有设置,所以就看这段代码了
    buildStatementFromContext(list, null);
  }
  private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    for (XNode context : list) {
      // 新建XMLStatementBuilder对象,这个对象包含信息有configuration(这个对象貌似无处不在)、builderAssistant(mapper resource这些重要信息)、所读取到的insert这些标签的信息(context)、
      // 最后就是数据库的操作SQL信息
      final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
      try {
        // statement节点的解析
        statementParser.parseStatementNode();
      } catch (IncompleteElementException e) {
        configuration.addIncompleteStatement(statementParser);
      }
    }
  }

上面的两段代码的逻辑还是很简单的,还是只是做一个简单的判断,符合相关条件就执行相关的逻辑代码,buildStatementFromContext方法中所遍历的list的内容,其实就是"select|insert|update|delete"等标签下的SQL模板,然后再通过一些相关参数新建XMLStatementBuilder对象,新建对象之后再调用statementParser.parseStatementNode(),继续看这个方法的代码:

XMLStatementBuilder.java
  public void parseStatementNode() {
    // 从这里开始,各种或许信息
    String id = context.getStringAttribute("id");
    String databaseId = context.getStringAttribute("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 statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);

    String nodeName = context.getNode().getNodeName();
    // 获取SQL命令的类型
    SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
    // 判断当前的SQL命令类型是否是select类型的
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
    boolean useCache = context.getBooleanAttribute("useCache", isSelect);
    boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

    // Include Fragments before parsing
    // 关于include标签的解析
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    includeParser.applyIncludes(context.getNode());

    // Parse selectKey after includes and remove them.
    // 解析 selectKey 标签
    processSelectKeyNodes(id, parameterTypeClass, langDriver);
    
    // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
    // 解析 SQL(pre: <selectKey> and <include> were parsed and removed)
    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);
    if (configuration.hasKeyGenerator(keyStatementId)) {
      keyGenerator = configuration.getKeyGenerator(keyStatementId);
    } else {
      keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
          configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
          ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
    }

    // 上面所有所获取到的信息,都是在这里使用的,到这里select|insert|update|delete这些标签的解析应该算是差不多了
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered, 
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
  }

这段代码看上好像真的很复杂,因为这里涉及到很多参数的获取和标签的解析,但是在这段代码中现在所需要关注的只是SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass)这行代码,其他代码功能请看注释吧,有兴趣的同学可以自己在去细看一下里面的代码。

XMLLanguageDriver.java
  public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
    XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
    return builder.parseScriptNode();
  }

这段代码没什么说的,主要的还是builder.parseScriptNode()的调用。代码如下:

XMLScriptBuilder.java
  public SqlSource parseScriptNode() {
    MixedSqlNode rootSqlNode = parseDynamicTags(context);
    SqlSource sqlSource = null;
    // 判断是否是动态SQL
    if (isDynamic) {
      sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {
      // 不是动态SQL
      sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    return sqlSource;
  }

这里就是判断所使用的是否是动态SQL,这里并不是动态SQL,那就看非动态的逻辑代码了。

RawSqlSource.java
  public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class<?> parameterType) {
    // 在这个构造函数里做getSql的操作
    this(configuration, getSql(configuration, rootSqlNode), parameterType);
  }
  public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {
    // 生成SQLSource解析器
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class<?> clazz = parameterType == null ? Object.class : parameterType;
    // 对SQL进行具体的解析,这里的sqlSource中包含sql语句、字段属性映射信息、configuration信息
    sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<String, Object>());
  }

上面第一个方法代码里,主要要关注的是getSql(configuration, rootSqlNode)这个方法,下面的那个就是this 的构造函数了,先继续探索吧!

  private static String getSql(Configuration configuration, SqlNode rootSqlNode) {
    // 根据configuration对象,创建一个动态对象
    DynamicContext context = new DynamicContext(configuration, null);
    // 把节点中SQL模板变成一个字符串
    rootSqlNode.apply(context);
    return context.getSql();
  }

这段代码还是只是根据一些参数创建对象,context.getSql()和rootSqlNode.apply(context)其实只是把配置文件里的SQL模板变成个字符串,代码同学们可以自己看一下。那我们先看下构造函数代码做了什么。在构造函数的代码里会根据configuration来创建个SQLSource解析器对象,然后通过sqlSourceParser.parse(sql, clazz, new HashMap<String, Object>())方法来进行具体是解析。代码如下:

SqlSourceBuilder.java
  public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
    ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
    // 解析SQL模板中的#{username},#{birthday},#{sex},#{address}这些标签
    GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
    // 开始真正的SQL解析,其实就是字符串拼接过程,不信你点进去看看
    String sql = parser.parse(originalSql);
    return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
  }

在这段代码里首先创建ParameterMappingTokenHandler这个对象,然后通过这个对象先解析SQL模板中的#{username},#{birthday},#{sex},#{address}这些标签,然后就开始真正的SQL解析,其实就是字符串拼接过程,不信你点进去看看。

GenericTokenParser.java
  public String parse(String text) {
    if (text == null || text.isEmpty()) {
      return "";
    }
    // search open token
    int start = text.indexOf(openToken, 0);
    if (start == -1) {
      return text;
    }
    char[] src = text.toCharArray();
    int offset = 0;
    final StringBuilder builder = new StringBuilder();
    StringBuilder expression = null;
    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);
            offset = end + closeToken.length();
            break;
          }
        }
        if (end == -1) {
          // close token was not found.
          builder.append(src, start, src.length - start);
          offset = src.length;
        } else {
          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();
  }

是吧,我没有骗你吧,从return builder.toString()这里就可以看出来了,这段代码的主要的流程只是拼接字符串而已,没什么好说的,如果对字符串拼接感兴趣的小伙伴感兴趣的话,可以自己去研究下。

在SqlSourceBuilder.java代码中的parse方法中,最后所返回的就是一个StaticSqlSource对象,这里就是这个对象中包含,我们所解析出来的SQL语句就是如下图中的sql这个所指向的SQL语句,已经不是originalSql所指向的SQL模板了。

本篇文章就先结束了,由于篇幅原因,对于SQL语句的执行这些操作,将会在下一篇文章进行解析。谢谢同学们的阅读,如果有错误,也请同学们指正。

发布了41 篇原创文章 · 获赞 8 · 访问量 4257

猜你喜欢

转载自blog.csdn.net/zfy163520/article/details/97930261