Mybatis源碼解析(三) —— Mapper代理類的生成

Mybatis源碼解析(三) —— Mapper代理類的生成

  在本系列第一篇文章已經講述過在Mybatis-Spring項目中,是經過 MapperFactoryBean 的 getObject()方法來獲取到Mapper的代理類並注入到Spring容器中的。在學習本章以前咱們先提出如下幾點問題:sql

  • 一、 Mapper接口是如何被加載 到 Configuration 中的?bash

  • 二、 Mapper代理類是如何生成的?mybatis

  • 三、 Mapper代理類是如何實現接口方法的?app

   本章內容就是圍繞着上面三個問題進行解析,那麼帶着問題去看源碼吧!框架

1、加載 Mapper接口

  針對Mybatis項目,Mapper的配置加載是從XmlConfigBuilder.mapperElement()方法中觸發的。咱們來看下源碼:ide

private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        if ("package".equals(child.getName())) {
          // 經過  package 形式加載 ,內部其實也是獲取到 package 路徑下的全部class再經過  class 形式加載
          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) {
            // 加載 Mapper.xml 的
            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) {
            // 加載 Mapper.xml 的
            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 形式加載
            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接口有2種形式: 一種是根據設置的 package 找到路徑下面全部的class並經過configuration.addMapper() 加載。 另外一種是根據指定設置的Mapper接口路徑直接經過 configuration.addMapper()加載class。因此加載Mapper接口最終都是 configuration.addMapper() 來加載的。學習

  而針對Mybatis-Spring項目則是 獲取 MapperScannerConfigurer 的 basePackage 參數,並經過 ClassPathMapperScanner 掃描到 設置的 basePackage 路徑下的全部class ,並獲得 BeanDefinition ,後面的狀況在第一篇文章已經講過,最終是獲得了 MapperFactoryBean ,而且還到了 MapperFactoryBean 內部的 checkDaoConfig() 方法加載Mapper接口的內容,那麼咱們再次回顧下這個方法的源碼:ui

@Override
  protected void checkDaoConfig() {
    super.checkDaoConfig();

    notNull(this.mapperInterface, "Property 'mapperInterface' is required");

    Configuration configuration = getSqlSession().getConfiguration();
    if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) {
      try {
        // 最終都是經過 addMapper() 方法加載的。
        configuration.addMapper(this.mapperInterface);
      } catch (Throwable t) {
        logger.error("Error while adding the mapper '" + this.mapperInterface + "' to configuration.", t);
        throw new IllegalArgumentException(t);
      } finally {
        ErrorContext.instance().reset();
      }
    }
  }
 
複製代碼

   從源碼中咱們發現 內部其實仍是經過 configuration.addMapper() 加載的。可能有些同窗會問,checkDaoConfig()何時被調用的,這個能夠追溯到 MapperFactoryBean 的繼承關係圖,能夠發現實現了 InitializingBean接口, 而 checkDaoConfig() 就是 經過afterPropertiesSet() 調用的。因此在MapperFactoryBean 初始化建立的時候就會調用checkDaoConfig(),即 加載Mapper接口。this

   根據上面的分析,咱們能夠發現 configuration.addMapper() 是實現加載Mapper接口的最核心的方法,那麼咱們就來好好分析下這個方法內部實現源碼:url

public <T> void addMapper(Class<T> type) {
    mapperRegistry.addMapper(type);
  }
  
複製代碼

  其內部是經過 委託 mapperRegistry 來就行加載的,那繼續往下看:

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 {
        // 三、 將 Mapper的class做爲 MapperProxyFactory(生成Mapper代理對象的工廠類) 的構參,並保存到 knownMappers 中。
        knownMappers.put(type, new MapperProxyFactory<T>(type));
        // 四、 解析 Class對象中,包含的全部mybatis框架中定義的註解,並生成Cache、ResultMap、MappedStatement。
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        parser.parse();
        loadCompleted = true;
      } finally {
        if (!loadCompleted) {
          knownMappers.remove(type);
        }
      }
    }
  }

複製代碼

   根據源碼能夠把整個加載流程分4個步驟:

  • 一、 判斷是否爲接口

  • 二、 判斷是否已加載

  • 三、 將 Mapper的class做爲 MapperProxyFactory(生成Mapper代理對象的工廠類) 的構參,並保存到 knownMappers 中。

  • 四、 解析 Class對象中,包含的全部mybatis框架中定義的註解,並生成Cache、ResultMap、MappedStatement。

   其中第三步是加載Mapper的核心,也就是同建立了一個生成Mapper代理對象的工廠對象,並將其放到map,等須要建立Mapper代理對象的是再經過獲取map中的工廠對象便可。 關於第四步,就是最近幾年比較流行的經過註解編寫SQl形式的解析方法。咱們知道mybatis支持xml和註解形式的Sql編寫。因此 MapperAnnotationBuilder 就是解析註解形式,根解析xml同樣, 最終也會生成 ResultMap、MappedStatement對象封裝到 configuration 中。關於它是如何解析的,有興趣的同窗能夠看啊可能源碼,這裏不在描述。

2、 Mapper動態代理對象(MapperProxy)的建立

   經過以前的文章,咱們知道 MapperFactoryBean 實現了 FactoryBean,也就是說在Spring 根據BeanDefinition 加載Bean的時候會調用 MapperFactoryBean.getObject() 獲取真實的Bean並注入到容器中。不用想,getObject()獲取到的必定是Mapper接口的代理實現類 MapperProxy,那麼咱們來一步步分析是如何建立 MapperProxy

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

   getObject()方法是經過 getSqlSession().getMapper() 獲取到 MapperProxy 的,相信你們對這個不陌生吧。至於這裏的SqlSession 實際上是 SqlSessionTemplate ,這個以前也講過,因此繼續查看:

public <T> T getMapper(Class<T> type) {
    return getConfiguration().getMapper(type, this);
  }
  
複製代碼

   最終仍是經過 configuration().getMapper() 獲取到 MapperProxy。繼續查看:

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    return mapperRegistry.getMapper(type, sqlSession);
  }
  
複製代碼

   絕不意外的確定是委託 mapperRegistry.getMapper() 來獲取,繼續查看:

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    // 一、 從 knownMappers 中獲取到  MapperProxyFactory 
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
    if (mapperProxyFactory == null)
      throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    try {
      // 二、 經過  mapperProxyFactory.newInstance() 建立 MapperProxy
      return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
      throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
  }
  
複製代碼

   也絕不意外的是 從 knownMappers 中獲取到 MapperProxyFactory ,再 經過 mapperProxyFactory.newInstance() 建立 MapperProxy。 繼續查看 mapperProxyFactory.newInstance() 內部實現:

@SuppressWarnings("unchecked")
  protected T newInstance(MapperProxy<T> mapperProxy) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }

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

複製代碼

   從上面源碼能夠清晰的看到 Proxy.newProxyInstance() 指定了被代理的類是 mapperInterface,其代理類是 mapperProxy,因此最終動態建立出 mapperInterface 的動態代理類 MapperProxy@xxxx (動態代理類名)

3、 MapperProxy 接口方法的實現

  經過上面的解析,咱們明確了 MapperProxy 代理是經過JDK動態生成,但接口方法是如何實現的呢? 這裏就得看 MapperProxy 源碼:

public class MapperProxy<T> implements InvocationHandler, Serializable {

  private static final long serialVersionUID = -6424540398559729838L;
  // 注意: 這個 SqlSession 其實是 SqlSessionTemplate
  private final SqlSession sqlSession;
  private final Class<T> mapperInterface;
  private final Map<Method, MapperMethod> methodCache;

  public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
    this.sqlSession = sqlSession;
    this.mapperInterface = mapperInterface;
    this.methodCache = methodCache;
  }

  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    // 若是調用的方法是 Object 種定義的方法,直接執行
    if (Object.class.equals(method.getDeclaringClass())) {
      try {
        return method.invoke(this, args);
      } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
      }
    }
    // 接口方法的調用都是經過 MapperMethod 來執行的
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);
  }

  // Mapper接口的每一個方法 都會生成一個 MapperMethod 對象,並經過 methodCache 來維護它們之間的關係
  private MapperMethod cachedMapperMethod(Method method) {
    MapperMethod mapperMethod = methodCache.get(method);
    if (mapperMethod == null) {
      // 注意這裏 傳入了 要執行的 方法信息
      mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
      methodCache.put(method, mapperMethod);
    }
    return mapperMethod;
  }

}

複製代碼

  從上面源碼中,咱們能夠發現,接口方法的實現實際上是經過 MapperMethod 來實現的,且 Mapper接口的每一個方法 都會生成一個 MapperMethod 對象,並經過 methodCache 來維護它們之間的關係,而 methodCache 是經過 MapperProxyFactory 傳遞下來的。

MapperMethod

   MapperMethod 實現接口方法的入口是 execute()方法,咱們來看下其內部源碼:

// 實際上都是調用 SqlSession的方法實現
  public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    // 判斷 索要執行的方法類型
    if (SqlCommandType.INSERT == command.getType()) {
      // 參數轉換
      Object param = method.convertArgsToSqlCommandParam(args);
      // 執行insert
      result = rowCountResult(sqlSession.insert(command.getName(), param));
    } else if (SqlCommandType.UPDATE == command.getType()) {
      Object param = method.convertArgsToSqlCommandParam(args);
      // 執行 update
      result = rowCountResult(sqlSession.update(command.getName(), param));
    } else if (SqlCommandType.DELETE == command.getType()) {
      Object param = method.convertArgsToSqlCommandParam(args);
      // 執行 delete
      result = rowCountResult(sqlSession.delete(command.getName(), param));
    } else if (SqlCommandType.SELECT == command.getType()) {
      // 執行 select
      if (method.returnsVoid() && method.hasResultHandler()) {
        // 沒有返回
        executeWithResultHandler(sqlSession, args);
        result = null;
      } else if (method.returnsMany()) {
        // 返回 List
        result = executeForMany(sqlSession, args);
      } else if (method.returnsMap()) {
        // 返回 Map
        result = executeForMap(sqlSession, args);
      } else {
        // 返回 一個對象
        Object param = method.convertArgsToSqlCommandParam(args);
        result = sqlSession.selectOne(command.getName(), param);
      }
    } else {
      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;
  }


複製代碼

   根據源碼咱們能夠發現其實內部都是 委託 SqlSession 的方法實現的,但它是如何區別何時調用哪一個 SqlSession 的方法呢?這個就不得不說 MapperMethod 內部維護的 SqlCommand 對象,咱們查看 SqlCommand 的構造方法:

public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) throws BindingException {
      // 補全 方法的全名稱路徑 即 com.xxx.selectByName 
      String statementName = mapperInterface.getName() + "." + method.getName();
      MappedStatement ms = null;
      // 從 configuration 中獲取到 MappedStatement 對象
      if (configuration.hasStatement(statementName)) {
        ms = configuration.getMappedStatement(statementName);
      } else if (!mapperInterface.equals(method.getDeclaringClass().getName())) { // issue #35
        String parentStatementName = method.getDeclaringClass().getName() + "." + method.getName();
        if (configuration.hasStatement(parentStatementName)) {
          ms = configuration.getMappedStatement(parentStatementName);
        }
      }
      if (ms == null) {
        throw new BindingException("Invalid bound statement (not found): " + statementName);
      }
      // 從 MappedStatement 中獲取到方法名 (注意: 節點中的id屬性包含命名空間)
      name = ms.getId();
      // 從 MappedStatement 中獲取到 方法的節點標籤,即 select|insert|update|delete
      type = ms.getSqlCommandType();
      if (type == SqlCommandType.UNKNOWN) {
        throw new BindingException("Unknown execution method for: " + name);
      }
    }

複製代碼

   咱們經過分析知道 ,SqlCommand 實際上是經過從 MappedStatement 中獲取到 方法名,以及所要執行的SQl命令類型(select|insert|update|delete)。 這裏咱們能夠明確的發現從configuration 中 獲取 MappedStatement 是經過 全稱路徑的方法去獲取的,即 com.xxx.selectByName 這種,調用SqlSession的方法雖然是從 MappedStatement 中獲取 id (注意: 節點中的id屬性包含命名空間),但實質上都是 com.xxx.selectByName。但咱們能夠經過這裏看出mybatis的Mapper接口方法是不能夠重載的。

4、 我的總結

  • 一、 Configuration 維護了一個 MapperRegistry 對象,該對象主要做用就是加載Mapper接口和獲取MapperProxy。

  • 二、 MapperRegistry 維護了一個key爲 mapper接口class對象,value爲 MapperProxyFactory 的map

  • 三、 MapperProxy 是 經過 MapperProxyFactory 建立的。

  • 四、 MapperProxy 實現 Mapper接口方法是委託 MapperMethod 執行的。

  • 五、 MapperMethod 執行接口方法時是經過 SqlCommand 來判斷要執行的具體 SQL節點,而且最終委託 SqlSession執行。

  • 六、 SqlCommand 內部的 信息是經過從 MappedStatement 中獲取的。

         若是您對這些感興趣,歡迎star、follow、收藏、轉發給予支持!

本文由博客一文多發平臺 OpenWrite 發佈!

相關文章
相關標籤/搜索