跳至主要內容

SqlSession执行Mapper过程

xw大约 4 分钟MybatisMybatis

概述

SqlSession执行Mapper过程可以分为四个部分,Mapper接口的注册过程、MappedStatement对象的注册过程、Mapper方法的调用过程、SqlSession执行Mapper的过程。在这里先抛下几个疑问。

  • mapper为接口,并没有编写实现类,为什么可以直接调用?
  • mappeer的方法怎么跟xml上的方法对应上的?

Mapper接口注册

在Mybatis框架启动时,会调用MapperRegistryaddMapper接口将mapper进行注册,核心代码如下:

public <T> void addMapper(Class<T> type) {
    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<>(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.
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        parser.parse();
        loadCompleted = true;
      } finally {
        if (!loadCompleted) {
          knownMappers.remove(type);
        }
      }
    }
  }

knownMappers是一个Map,key为传入的class类型,value是MapperProxyFactory对象,查看MapperProxyFactory相关逻辑,关键方法如下:

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

MapperProxy使用JDK的动态代理,实现了InvocationHandler接口,定义方法执行拦截逻辑后,还需要调用java.lang.reflect.Proxy类的newProxyInstance()方法创建代理对象。

MappedStatement注册

MappedStatement类描述Mapper的SQL配置信息。SQL配置有两种方式:一种是通过XML文件配置;另一种是通过Java注解。

在Configuration类中有一个mappedStatements属性,该属性用于注册MyBatis中所有的MappedStatement对象,声明代码如下:

  protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection")
      .conflictMessageProducer((savedValue, targetValue) ->
          ". please check " + savedValue.getResource() + " and " + targetValue.getResource());

Key为Mapper SQL配置的Id,如果SQL是通过XML配置的,则Id为命名空间加上<select|update|delete|insert>标签的Id,如果SQL通过Java注解配置,则Id为Mapper接口的完全限定名(包括包名)加上方法名称。

MappedStatement对象的生成是通过org.apache.ibatis.builder.xml.XMLConfigBuilder#mapperElement方法实现,

 private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        //通过package指定包名  
        if ("package".equals(child.getName())) {
          String mapperPackage = child.getStringAttribute("name");
          configuration.addMappers(mapperPackage);
        } else {
          //通过resource属性指定XML文件路径  
          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)) {
              XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
              mapperParser.parse();
            }
           //通过URL属性指定XML文件路径   
          } else if (resource == null && url != null && mapperClass == null) {
            ErrorContext.instance().resource(url);
            try(InputStream inputStream = Resources.getUrlAsStream(url)){
              XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
              mapperParser.parse();
            }
           // 通过class属性指定接口的完全限定名   
          } 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.");
          }
        }
      }
    }
  }

具体解析的关键调用如下:

  XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
              mapperParser.parse();

查看parse方法

 public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
      configurationElement(parser.evalNode("/mapper"));
      configuration.addLoadedResource(resource);
      bindMapperForNamespace();
    }
    //解析之前出现异常的的ResultMap对象
    parsePendingResultMaps();
    //继续解析之前出现异常的CacheRef对象
    parsePendingCacheRefs();
    //继续解析之前出现异常的<select|update|delete|insert>标签
    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);
      //解析<cache-ref>标签  
      cacheRefElement(context.evalNode("cache-ref"));
      //解析<cache>标签  
      cacheElement(context.evalNode("cache"));
      //解析<parameterMap>标签  
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      //解析<resultMap>标签  
      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);
    }
  }

Mapper方法调用过程

为了执行Mapper接口中定义的方法,我们首先需要调用SqlSession对象的getMapper()方法获取一个动态代理对象,然后通过代理对象调用方法,为了执行Mapper接口中定义的方法,我们首先需要调用SqlSession对象的getMapper()方法获取一个动态代理对象,然后通过代理对象调用方法,MyBatis中的MapperProxy实现了InvocationHandler接口,用于实现动态代理相关逻辑。当我们调用动态代理对象方法的时候,会执行MapperProxy类的invoke()方法。

 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      //从Object类继承的方法  
      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);
    }
  }