mybatis源码分析(2) mybatis如何将dao和xml中sql进行绑定

1、流程分析

相信大家了解mybatis的话,一定会知道mybatis是一门orm语言,其中最先要了解的当然是mybatis是如何将dao层的接口函数和sql语句进行绑定,从而能够通过调用接口函数来执行相应的sql语句。

首先,mybatis是通过动态代理的方式将接口语句和sql进行绑定的,mybatis会自己创建一个动态的中介类,并且根据相应的接口返回具有实例的代理类。同时在一开始mybatis会解析xml信息来初始化参数。为之后创建中介类提供原始数据。总结一下mybatis的流程主要是解析xml文件–>dao层动态绑定–>执行语句。dao层是如何进行绑定的我已经在源码分析一种详细介绍了,接下来我首先来主要分析下mybatis是如何解析xml文件的。

2、解析xml文件
2.1、使用方法

每个基于 MyBatis 的应用都是以一个 SqlSessionFactory 的实例为核心的。SqlSessionFactory 的实例可以通过 SqlSessionFactoryBuilder 获得。而 SqlSessionFactoryBuilder 则可以从 XML 配置文件或一个预先配置的 Configuration 实例来构建出 SqlSessionFactory 实例。

String resource = "org/mybatis/example/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

上面是一般我们获取mybatis-config.xml文件的写法,XML 配置文件中包含了对 MyBatis 系统的核心设置,包括获取数据库连接实例的数据源(DataSource)以及决定事务作用域和控制方式的事务管理器(TransactionManager)。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>

然后我们可以通过sqlSessionFactory来得到sqlsession对象。

try (SqlSession session = sqlSessionFactory.openSession()) {
  BlogMapper mapper = session.getMapper(BlogMapper.class);
  Blog blog = mapper.selectBlog(101);
}

然后利用getMapper函数可以得到相应的代理对象,最后可以利用代理对象来调用相应的函数,具体这个函数是如何一一对应xml中的sql的呢?让我们先来看下xml文件

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

这份xml文件mapper标签前面的是xml文件的前缀,我们可以先不需要去关心它,只需要从mapper标签开始看,可以看到mapper标签后面跟随了namespace,这个可以唯一确定一个类,那我们如果确定是这一个类中的哪一个函数呢,那就是下面的id号了,这个id号必须与相对应的类的函数名一致,否则会报错,这样我们可以将下面的sql语句和dao层的接口函数一一对应起来。

2.2、解析xml流程

解析config.xml

首先需要解析mybatis的总体的配置文件,在SqlSessionFactoryBuilder类的build函数中对调用到XMLConfigBuilder类的函数,也就是说,我们在一开始利用resource这个指定总体配置文件的位置,然后调用SqlSessionFactoryBuilder的build函数时就开始读取相应的配置文件信息。

public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
    try {
      XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
      return build(parser.parse());
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
      ErrorContext.instance().reset();
      try {
        reader.close();
      } catch (IOException e) {
        // Intentionally ignore. Prefer previous error.
      }
    }
  }

这边看到XMLConfigBuilder的对象调用了parse函数

ublic Configuration parse() {
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }

这边也主要是调用了parseConfiguration函数,parser.evalNode("/configuration")函数的作用是将xml中的信息利用XPathParser类的evalNode函数,将信息组装成XNode的形式。

private void parseConfiguration(XNode root) {
    try {
      // issue #117 read properties first
      propertiesElement(root.evalNode("properties"));
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      loadCustomVfs(settings);
      loadCustomLogImpl(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"));
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }

可以看到这些都是mybatis处理配置文件中的顶级标签,相信如果前面的顶级标签大家不熟悉,但是mapper标签一定是了解的,在文章前面的配置文件中也有这个顶级标签。

private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        if ("package".equals(child.getName())) {
          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) {
            ErrorContext.instance().resource(resource);
            InputStream inputStream = Resources.getResourceAsStream(resource);
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
            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);
            configuration.addMapper(mapperInterface);
          } else {
            throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
          }
        }
      }
    }
  }

所以解析mapper标签的函数是mapperElement(XNode parent)函数,但是对象的mapper标签的作用是指引到相应的映射的mapper.xml的配置文件中,所以为了方便,mybatis中有相应的处理这个mapper.xml的类,就是XMLMapperBuilder类,在上面函数中一开始也是解析相应的mapper信息,例如上文中的mappers标签的内容

<mappers>
    <mapper resource="org/mybatis/example/BlogMapper.xml"/>
</mappers>

在下面利用Resources.getResourceAsStream(resource)可以获取相应的mapper.xml的信息,然后传入到XMLMapperBuilder中进行解析,解析函数时parse函数

public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
      configurationElement(parser.evalNode("/mapper"));
      configuration.addLoadedResource(resource);
      bindMapperForNamespace();
    }

    parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();
  }

在configurationElement(parser.evalNode("/mapper"));函数中读取相应的配置信息然后放入到Configure这个mybatis的大管家中。

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

到这一步就已经解析到下面xml文档中的那步了

<mapper namespace="org.mybatis.example.BlogMapper">
  <select id="selectBlog" resultType="Blog">
    select * from Blog where id = #{id}
  </select>
</mapper>

前面当然也是处理相应的顶级标签,具体这些标签的作用大家可以到mybatis的官方文档中去查看,这边贴出来文章就太长了。在buildStatementFromContext(context.evalNodes(“select|insert|update|delete”))函数中就是解析了上面的select标签。

/*这边getDatabaseId表示数据库id,可以进行识别,如果我们只有一个数据库,一般可以不设置,直接默认的调用下面那个函数*/
private void buildStatementFromContext(List<XNode> list) {
  if (configuration.getDatabaseId() != null) {
    buildStatementFromContext(list, configuration.getDatabaseId());
  }
  buildStatementFromContext(list, null);
}

/*因为一个mapper.xml映射文件中可以有多个select或者insert这些sql语句,所以为了区分,mybatis处理这些同一个mapper文档中的不同sql语句,
创建了XMLStatementBuilder对象,一个sql语句对应一个XMLStatementBuilder,后面也会有一个sql语句对应一个sqlSource对象
因为sqlSource对象的作用就是为了将动态的sql语句合理组装成一个map对象*/
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    for (XNode context : list) {
      final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
      try {
        statementParser.parseStatementNode();
      } catch (IncompleteElementException e) {
        configuration.addIncompleteStatement(statementParser);
      }
    }
  }

在XMLStatementBuilder类的parseStatementNode中开始解析sql语句

public void parseStatementNode() {
    String id = context.getStringAttribute("id");
    String databaseId = context.getStringAttribute("databaseId");

    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
      return;
    }

    String nodeName = context.getNode().getNodeName();
    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);
    boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

    // Include Fragments before parsing
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    includeParser.applyIncludes(context.getNode());

    String parameterType = context.getStringAttribute("parameterType");
    Class<?> parameterTypeClass = resolveClass(parameterType);

    String lang = context.getStringAttribute("lang");
    LanguageDriver langDriver = getLanguageDriver(lang);

    // Parse selectKey after includes and remove them.
    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;
    }
	/*创建一个sqlSource对象*/
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    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<?> resultTypeClass = resolveClass(resultType);
    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");

    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered,
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
  }

当然这边解析的是下面这个xml内容

<select id="selectBlog" resultType="Blog">
    select * from Blog where id = #{id}
</select>

上面函数中首先主要关注的是SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);SqlSource是一个接口类,在MappedStatement对象中是作为一个属性出现的。SqlSource接口只有一个getBoundSql(Object parameterObject)方法,返回一个BoundSql对象。一个BoundSql对象,代表了一次sql语句的实际执行,而SqlSource对象的责任,就是根据传入的参数对象,动态计算出这个BoundSql,也就是说Mapper文件中的节点的计算,是由SqlSource对象完成的。

最后主要看下builderAssistant.addMappedStatement函数,后面参数非常多,这个函数创建了一个MappedStatement对象。其实对于mybatis中mapper.xml映射文件,如果这个文件中有多个select,insert等这些sql语句,那么有几个这样的sql语句,就会生成几个MappedStatement对象,这个MappedStatement对象中存储有id这个参数,这个id的组成就是namespace和select标签中的id,也就是唯一指明了一个类中的函数,这个类中也有SqlSource对象,这就已经说明了一个SqlSource是和一个sql语句绑定的。addMappedStatement函数将最后创建好的MappedStatement对象放入configuration配置类中。

从这里我们可以看MappedStatement存储有一个sql语句的所有配置信息。

3、dao层和xml中sql如何进行绑定

这个问题也就是mybatis如何创建中介类,使得创建的中介类可以和xml中的sql语句进行匹配,然后利用Proxy.newProxyInstance函数结合接口的配置信息和中介类返回对应的代理类对象。首先在MapperProxyFactory类中

public T newInstance(SqlSession sqlSession) {
   final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
   return newInstance(mapperProxy);
 }

在这里会新建一个MapperProxy对象就是中介类,在其中,调用相应的接口函数会调用到MapperProxy类的invoke函数

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
   try {
     if (Object.class.equals(method.getDeclaringClass())) {
       return method.invoke(this, args);
     } else {
       return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
     }
   } catch (Throwable t) {
     throw ExceptionUtil.unwrapThrowable(t);
   }
 }

上面我们的接口函数不是一般直接java中定义的函数,所以会走到下面的cachedInvoker(method).invoke(proxy, method, args, sqlSession);函数中。

private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
    try {
      return methodCache.computeIfAbsent(method, m -> {
        if (m.isDefault()) {
          try {
            if (privateLookupInMethod == null) {
              return new DefaultMethodInvoker(getMethodHandleJava8(method));
            } else {
              return new DefaultMethodInvoker(getMethodHandleJava9(method));
            }
          } catch (IllegalAccessException | InstantiationException | InvocationTargetException
              | NoSuchMethodException e) {
            throw new RuntimeException(e);
          }
        } else {
          return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
        }
      });
    } catch (RuntimeException re) {
      Throwable cause = re.getCause();
      throw cause == null ? re : cause;
    }
  }

m.isDefault()这个函数是用来判断是否是接口的默认函数,接口有两类函数,分别是

  • 抽象方法,接口的子类需要实现
  • 默认方法,接口的子类不需要实现,可以直接使用。我们只需在方法名前面加个 default 关键字即可实现默认方法。

而我们一般定义的接口函数都是抽象方法,所以函数进入到new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()))这个语句中,首先我们先来看下new MapperMethod(mapperInterface, method, sqlSession.getConfiguration())函数

public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
  this.command = new SqlCommand(config, mapperInterface, method);
   this.method = new MethodSignature(config, mapperInterface, method);
 }

上面SqlCommand是可以去MappedStatement类对象中获取,MethodSignature参数可以通过反射机制,利用方法中的参数来获取,具体如何获取将放到后面的mybatis解析命令分析篇讲解,他的作用主要是获取这个接口函数的各个参数,一对几的情况。最后在生成的PlainMethodInvoker类中调用invoke函数

public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
      return mapperMethod.execute(sqlSession, args);
    }
public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) {
      case INSERT: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.insert(command.getName(), param));
        break;
      }
      case UPDATE: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.update(command.getName(), param));
        break;
      }
      case DELETE: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.delete(command.getName(), param));
        break;
      }
      case SELECT:
        if (method.returnsVoid() && method.hasResultHandler()) {
          executeWithResultHandler(sqlSession, args);
          result = null;
        } else if (method.returnsMany()) {
          result = executeForMany(sqlSession, args);
        } else if (method.returnsMap()) {
          result = executeForMap(sqlSession, args);
        } else if (method.returnsCursor()) {
          result = executeForCursor(sqlSession, args);
        } else {
          Object param = method.convertArgsToSqlCommandParam(args);
          result = sqlSession.selectOne(command.getName(), param);
          if (method.returnsOptional()
              && (result == null || !method.getReturnType().equals(result.getClass()))) {
            result = Optional.ofNullable(result);
          }
        }
        break;
      case FLUSH:
        result = sqlSession.flushStatements();
        break;
      default:
        throw new BindingException("Unknown execution method for: " + command.getName());
    }
    if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
      throw new BindingException("Mapper method '" + command.getName()
          + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
    }
    return result;
  }

从而可以知道根据几对几和sqlcommand可以分别调用相应的函数,具体还是利用sqlSession去执行命令,而sqlSession中包含有自己对应的configuration数据,从而可以知道调用哪一个sql语句来执行。

4、总结

在这里插入图片描述
mybatis在创建SqlSessionFactory对象时会解析配置文件,并将每一个sql语句配置信息保存各自的MappedStatement中,sqlsource会解析动态sql语句。并将MappedStatement对象数据放入Configuration对象中,同时利用MappedStatement中的id,这个id是namespace和函数id组成的,可以唯一执行一个接口类中的一个函数。这样我们可以利用id来找到sql语句对应的MappedStatement。

同时mybatis利用动态代理主动创建中介类,返回一个代理对象,这样在代理对象调用相应的函数实际上调用的是中介类的invoke函数,最后调用还是sqlsession对象中的方法,并且这个对象中也包含了Configuration对象,这样我们可以在最后读取到Configuration配置中的信息,最终调用合适的sql语句执行。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值