从源码层面分析Mybatis中Dao接口和XML文件的SQL是如何关联的

Posted 犀牛饲养员

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从源码层面分析Mybatis中Dao接口和XML文件的SQL是如何关联的相关的知识,希望对你有一定的参考价值。

为了能清楚的说明问题,源码我尽量加上详细的注释。有些大段的源码我只是截取了一部分能说明问题就好。

xml文件解析

我们知道SqlSessionFactory是mybatis非常重要的一个类,它是单个数据库映射关系经过编译后的内存镜像.SqlSessionFactory对象的实例可以通过SqlSessionFactoryBuilder对象类的build方法创建,而xml文件的解析就是在这个方法里调用的。

public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
    try {
      XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
      return build(parser.parse());//这里是解析的入口
      ...

接着看下parse方法,

public Configuration parse() {
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    parseConfiguration(parser.evalNode("/configuration"));//这里开始解析
    return configuration;
  }

接着看parseConfiguration方法,

private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        if ("package".equals(child.getName())) {
          //package标签
          String mapperPackage = child.getStringAttribute("name");
          configuration.addMappers(mapperPackage);
        } else {
          //mapper标签
          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);
            try(InputStream inputStream = Resources.getResourceAsStream(resource)) {
              // 这里就会加载resource,解析mapper文件,构建mapperStatement对象,
              XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
              mapperParser.parse();
            }
            ...

注意这里还是解析mybatis的配置文件,还没到我们的xml sql文件。有人可能有疑问,这里的package、resource是啥啊,在mybatis的配置文件好像也没看到啊?事实上,mybatis的配置文件是可以这样写的:

<mappers>
    <mapper resource="Mapper xml的路径(相对于classes的路径)"/>
</mappers>

或者,

<mappers>
        <mapper class="接口的完整类名" />
</mappers>

不过我们大部分是用spring+mybatis的方式,这种配置比较少见了,更多的可能是这样的:

mybatis:
    # 配置类型别名
  type-aliases-package: com.xxx.xxx.system.model
    # 配置mapper的扫描,找到所有的mapper.xml映射文件
  mapper-locations: 'classpath*:/mybatis/*/**Mapper.xml'
    # 加载全局的配置文件
  config-location: 'classpath:/mybatis/mybatis-config.xml'

作用其实是一样的。

继续往下看,XMLMapperBuilder#parse方法,

public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
      //xml sql文件都是mapper开始的
      configurationElement(parser.evalNode("/mapper"));
      configuration.addLoadedResource(resource);
      bindMapperForNamespace();//尝试通过nameSpace来加载配置文件。
    }

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

configurationElement方法,

private void configurationElement(XNode context) {
    try {
       解析namespace
      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"));
      //解析parameterMap,最后添加到 Configuration对象parameterMaps属性里面,全局通用
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      //解析resultMap,放在Configuration中,全局通用
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      解析sql
      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);
    }
  }

Configuration类是mybatis非常核心的一个类,由很多全局配置都会解析后放在这里,比如parameterMaps、resultMap等。继续看buildStatementFromContext方法,这个方法最终调用org.apache.ibatis.builder.xml.XMLStatementBuilder#parseStatementNode,如下:

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

    ....

    // 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;
    }

    //Mybatis会把每个SQL标签封装成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,将组装好的MappedStatement添加到configuration里面维护了statement的map,key就是namespace+sql的id、
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered,
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
  }

可以看到,最终会生成MappedStatement对象,并添加到configuration里面维护的一个map,这个map的key就是namespace+sql的id,value是对应的MappedStatement对象。这个MappedStatement对象非常重要,它是连接我们两个部分的关键,记住这个类。

总结下:

XML文件中的每一个SQL标签就对应一个MappedStatement对象,这里面有两个属性很重要。

  • id:全限定类名+方法名组成的ID。
  • sqlSource:当前SQL标签对应的SqlSource对象。

MappedStatement对象会被缓存到Configuration#mappedStatements中,全局有效。Configuration对象就是Mybatis中的核心类,基本所有的配置信息都维护在这里。把所有的XML都解析完成之后,Configuration就包含了所有的SQL信息。

动态代理

了解了解析的流程,接着看另外一个问题:

我们定义的Dao接口并没有实现类,那么在调用它的时候,它是怎样最终执行到我们的SQL语句的呢?我先给出答案,动态代理。下面就来具体分析下。

这里先引入一个非常重要的类MapperProxy,来看看它的定义:

public class MapperProxy<T> implements InvocationHandler, Serializable {

  private static final long serialVersionUID = -4724728412955527868L;
  private static final int ALLOWED_MODES = MethodHandles.Lookup.PRIVATE | MethodHandles.Lookup.PROTECTED
      | MethodHandles.Lookup.PACKAGE | MethodHandles.Lookup.PUBLIC;
  private static final Constructor<Lookup> lookupConstructor;
  private static final Method privateLookupInMethod;
  private final SqlSession sqlSession;
  private final Class<T> mapperInterface;
  private final Map<Method, MapperMethodInvoker> methodCache;
  ...

这个类继承了InvocationHandler,肯定是动态代理了。如果有小伙伴对动态代理不熟悉,可以先补充下这部分知识,下面的内容会更好理解一些

思考一个问题:MapperProxy是什么时候创建的呢?是在SqlSession的getMapper这个抽象方法的实现中调用的,最终调用的是org.apache.ibatis.binding.MapperRegistry#getMapper,代码如下:

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
    if (mapperProxyFactory == null) {
      throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    }
    try {
      //通过动态代理创建一个Mapper实例
      return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
      throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
  }

当我们声明一个dao接口的时候,通常是这样做:

@MapperScan("com.fcbox.uniorder.system.dao")

这是在springboot中的用法,或者也可以使用xml配置的方式。这个注解的作用是,将路径下的所有类注册到Spring Bean中,并且将它们的beanClass设置为MapperFactoryBean。MapperFactoryBean实现了FactoryBean接口,俗称工厂Bean。那么,当我们注入这个Dao接口的时候,返回的对象就是MapperFactoryBean这个工厂Bean中的getObject()方法对象。(getObject方法返回就是FactoryBean创建的Bean实例)

org.mybatis.spring.mapper.MapperFactoryBean#getObject代码如下:

public T getObject() throws Exception {
        return this.getSqlSession().getMapper(this.mapperInterface);
    }

这个getMapper方法就会一路调用到我们上面说的org.apache.ibatis.binding.MapperRegistry#getMapper方法。

总结下,也就是说我们通过注入Dao接口的时候,注入的就是MapperProxy这个代理对象,那么自然的,根据动态代理的原理,当
我们调用到Dao接口的方法时,则会调用到MapperProxy对象的invoke方法。那我们就来看看这个invoke方法:

@Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      //判断接口是否有实现类(一般我们的dao接口没有实现类)
      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);
    }
  }

然后会继续cachedInvoker的invoke方法,看看cachedInvoker是个啥东西,

private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
    try {
      return MapUtil.computeIfAbsent(methodCache, 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 {
          /**
           * 会走到这里来
           * PlainMethodInvoker是封装的一个mapper调用的工具类
           * MapperMethod 对象里面包含了两个对象的引用:
           * SqlCommand 包含了方法名(全限定名)和命令类型(insert、delete等等)
           */
          return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
      ...
  }

所以调用的是org.apache.ibatis.binding.MapperProxy.PlainMethodInvoker#invoke方法,

public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
      return mapperMethod.execute(sqlSession, args);
    }

这里调用的是MapperMethod的execute方法,继续看,

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;
      }
      ...

MapperMethod源码发现最终还是调用sqlSession中的相关方法,sqlSession再委托给Excutor去执行,比如我们拿update举例,如下:

@Override
  public int update(String statement, Object parameter) {
    try {
      dirty = true;
      MappedStatement ms = configuration.getMappedStatement(statement);
      return executor.update(ms, wrapCollection(parameter));
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error updating database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

咦?这个MappedStatement怎么看着这么眼熟,这不就是我们第一部分讲的内容吗?是不是有种柳暗花明又一村的感觉。

总结下,当我们调用到Dao接口的方法时,则会调用到MapperProxy对象的invoke方法,最终会通过接口的全路径名从Configuration这个大管家的某个map里找到MappedStatement 对象,然后通过执行器Executor去执行具体SQL并返回。


参考:

  • https://juejin.cn/post/7004047712664420382

以上是关于从源码层面分析Mybatis中Dao接口和XML文件的SQL是如何关联的的主要内容,如果未能解决你的问题,请参考以下文章

2021阿里最新面试题:Mybatis中的Dao接口和XML文件里的SQL是如何建立关系的?学习完这份资料带你成功上岸阿里(2021最新版)

MyBatis学习04mapper代理方法开发dao

MyBatis是如何为Dao接口创建实现类的

MyBatis是如何为Dao接口创建实现类的

mybatis中dao接口与mapper关联的理解

MyBatis xml和dao层接口组合使用