1. 程式人生 > >mybatis 原始碼分析(二)mapper 初始化

mybatis 原始碼分析(二)mapper 初始化

mybatis 的初始化還是相對比較複雜,但是作者在初始化過程中使用了多種設計模式,包括建造者、動態代理、策略、外觀等,使得程式碼的邏輯仍然非常清晰,這一點非常值得我們學習;

一、mapper 初始化主要流程

mybatis 初始化的過程中,主要是 XML 配置的解析,不同的部分又分別委託給了不同的解析器;

解析流程為:

XMLConfigBuilder -> XMLMapperBuilder -> XMLStatementBuilder -> XMLScriptBuilder -> SqlSourceBuilder

  • XMLConfigBuilder:負責全域性的 mybatis-conf.xml 配置解析;
  • XMLMapperBuilder:負責 sql 配置的 mapper 配置解析;
  • XMLStatementBuilder:負責 mapper 配置檔案中 select|insert|update|delete 節點解析;
  • XMLScriptBuilder:負責各 sql 節點解析,主要是動態 sql 解析;
  • SqlSourceBuilder:負責構建 SqlSource;

原始碼分析:

首先在 XMLConfigBuilder 確定了主要的解析流程:

private void parseConfiguration(XNode root) { // 解析的程式碼和xml的配置一一對應
  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);
  }
}

private void mapperElement(XNode parent) throws Exception {
  if (parent != null) {
    for (XNode child : parent.getChildren()) {
      if ("package".equals(child.getName())) {  // package 方式,mapper 必須和 xml 配置檔案在同一目錄下
        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.");
        }
      }
    }
  }
}

然後在 XMLMapperBuilder 中解析 mapper

public void parse() {
  if (!configuration.isResourceLoaded(resource)) {
    configurationElement(parser.evalNode("/mapper"));
    configuration.addLoadedResource(resource);
    bindMapperForNamespace();  // 繫結 mapper 和 xml 配置
  }

  // 下面的三個方式是繼續之前未完成的節點解析;比如在 cache-ref 解析的時候,依賴的 cache namespace 還未建立的時候,就需要暫停
  parsePendingResultMaps();
  parsePendingCacheRefs();
  parsePendingStatements();
}

private void cacheRefElement(XNode context) {
  if (context != null) {
    configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace"));
    CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace"));
    try {
      cacheRefResolver.resolveCacheRef();  // 未找到依賴的 cache 時,暫停解析
    } catch (IncompleteElementException e) {
      configuration.addIncompleteCacheRef(cacheRefResolver);
    }
  }
}

二、動態 sql 解析

此外在 mapper 各節點的解析過程中 resultMap 和 sql 節點的解析最為複雜,resultMap 解析主要是 xml 和 反射的處理,有一點繁瑣有興趣可以自己看一下;這裡主要講一下 sql 節點的解析要點;

buildStatementFromContext(context.evalNodes("select|insert|update|delete"));

private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
  for (XNode context : list) {
    final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
    try {
      statementParser.parseStatementNode();  // 主要的解析過程放到了XMLStatementBuilder中
    } catch (IncompleteElementException e) {
      configuration.addIncompleteStatement(statementParser);
    }
  }
}
// XMLStatementBuilder
public void parseStatementNode() {
  ...
  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 = langDriver.createSqlSource(configuration, context, parameterTypeClass);
  ...

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

程式碼中的 LanguageDriver 就封裝了動態 sql 的解析規則,通過這個介面也可以使用其他的模版引擎或者解析規則(可以通過配置或者註解指定);

其中 XMLLanguageDriver 主要處理動態 sql,RawLanguageDriver 主要處理靜態 sql;

從程式碼中可以看到最後 LanguageDriver 將 xml 配置解析成了 SqlSource,其結構如下:

其中:

  • RawSqlSource:處理靜態sql,去掉xml標籤;
  • DynamicSqlSource:處理動態sql,去掉xml標籤;
  • ProviderSqlSource:處理註解形式的sql;
  • StaticSqlSource:最終將上面 SqlSource 處理結果中的佔位符,替換為 "?",構成真正可執行的sql;

其解析的整體流程如下:

從圖中可以看到 sql 節點的主要解析邏輯就在於 parseDynamicTags,MixedSqlNode rootSqlNode = parseDynamicTags(context);

在看原始碼之前先看一下 SqlNode 的結構;

這裡的每個 node 和 sql 節點下的子節點一一對應;

protected MixedSqlNode parseDynamicTags(XNode node) {
  List<SqlNode> contents = new ArrayList<>();
  NodeList children = node.getNode().getChildNodes();
  for (int i = 0; i < children.getLength(); i++) {
    XNode child = node.newXNode(children.item(i));
    if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
      String data = child.getStringBody("");
      TextSqlNode textSqlNode = new TextSqlNode(data);
      if (textSqlNode.isDynamic()) {
        contents.add(textSqlNode);
        isDynamic = true;
      } else {
        contents.add(new StaticTextSqlNode(data));
      }
    } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
      String nodeName = child.getNode().getNodeName();
      NodeHandler handler = nodeHandlerMap.get(nodeName);
      if (handler == null) {
        throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
      }
      handler.handleNode(child, contents);
      isDynamic = true;
    }
  }
  return new MixedSqlNode(contents);
}

這裡主要邏輯是首先通過子標籤的名字,獲取對應的處理器,然後將所有的子標籤生成的 SqlNode 合成 MixedSqlNode;

private void initNodeHandlerMap() {
  nodeHandlerMap.put("trim", new TrimHandler());
  nodeHandlerMap.put("where", new WhereHandler());
  nodeHandlerMap.put("set", new SetHandler());
  nodeHandlerMap.put("foreach", new ForEachHandler());
  nodeHandlerMap.put("if", new IfHandler());
  nodeHandlerMap.put("choose", new ChooseHandler());
  nodeHandlerMap.put("when", new IfHandler());
  nodeHandlerMap.put("otherwise", new OtherwiseHandler());
  nodeHandlerMap.put("bind", new BindHandler());
}

到這裡就已經比較清楚了,這個 sql 節點的解析過程使用的是策略模式,整個 sql 節點被封裝成 SqlSource,其子節點封裝為 SqlNode,每個 Node 的解析行為又封裝到 NodeHandler 中;整個流程雖然比較長,但是每個模組都非常的清晰,這裡非常值得我們學習;

三、mapper 動態代理

首先簡單看一個動態代理的 demo

interface Car { void run(String name); }

@Test
public void testDynamic() {
  Car car = (Car) Proxy.newProxyInstance(
    Car.class.getClassLoader(), // 代理目標的類載入器
    new Class[]{Car.class},     // 代理的介面陣列,因為可以實現多個介面
    new InvocationHandler() {   // 動態代理的邏輯程式碼
      @Override
      public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("----動態代理開始----");
        // 目標邏輯程式碼
        System.out.println("----動態代理結束----");
        return null;
      }
  });

  car.run("sdf");
}

從上面的程式碼可以看到,我們只定義一個介面並沒有實現類,但是通過動態代理就可以動態生成實現類;在使用 mapper 的時候也是一樣的,每次呼叫mapper方法的時候,都會動態生成一個實現類;

初始化:

// 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 {
      knownMappers.put(type, new MapperProxyFactory<>(type));  // 為每一個介面新增一個動態代理工廠
      MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);  // 解析註解配置
      parser.parse();
      loadCompleted = true;
    } finally {
      if (!loadCompleted) {
        knownMappers.remove(type);
      }
    }
  }
}

使用:

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 {
    return mapperProxyFactory.newInstance(sqlSession);
  } catch (Exception e) {
    throw new BindingException("Error getting mapper instance. Cause: " + e, e);
  }
}

其主要流程大致如下:

相關推薦

mybatis 原始碼分析mapper 初始

mybatis 的初始化還是相對比較複雜,但是作者在初始化過程中使用了多種設計模式,包括建造者、動態代理、策略、外觀等,使得程式碼的邏輯仍然非常清晰,這一點非常值得我們學習; 一、mapper 初始化主要流程 mybatis 初始化的過程中,主要是 XML 配置的解析,不同的部分又分別委託給了不同的解析器;

Mybatis原始碼分析4—— Mapper的建立和獲取

Mybatis我們一般都是和Spring一起使用的,它們是怎麼融合到一起的,又各自發揮了什麼作用? 就拿這個Mapper來說,我們定義了一個介面,聲明瞭一個方法,然後對應的xml寫了這個sql語句, 它怎麼就執行成功了?這傢伙是怎麼實現的,帶著這個好奇心,我一步步跟蹤,慢慢揭開了它的

Mybatis原始碼分析1—— Mapper檔案解析

感覺CSDN對markdown的支援不夠友好,總是伴隨各種問題,很惱火! xxMapper.xml的解析主要由XMLMapperBuilder類完成,parse方法來完成解析: public void parse() { if (!configuration.isRes

Mybatis原始碼分析3—— 從Mybatis的視角去看Bean的初始流程

不涉及Spring完整的啟動流程,僅僅從Mybatis的視角去分析幾個關鍵的方法,找到Mybatis是如何通過這幾個擴充套件點植入進去的,反過來看Spring是如何設計,埋下這些伏筆,實現其可擴充套件性。 springContext-mybatis.xml的配置: <!--

Spring原始碼分析-Spring IoC容器的初始No.2

Spring原始碼分析(一)-Spring IoC容器的初始化No.1中已經分析了Bean的載入過程,本章將分析Bean的例項化過程 本章圍繞refresh().finishBeanFactoryInitialization(beanFactory)方法,

Spring原始碼分析-Spring IoC容器的初始No.1

Spring IoC容器的初始化 Spring原始碼分析(一)中提到了很多類,比如BeanDefinition、BeanDefinitionReader、BeanDefintionParser、BeanWrapper等都是ApplicationContext中

Mybatis學習系列Mapper映射文件

tst 轉換 tin 是個 sql註入 eas 屬性。 object spl Mapper映射文件,作用是用來配置SQL映射語句,根據不同的SQL語句性質,使用不同的標簽,mapper文件中常用的標簽有<iselect>、<insert>、<

Mybatis 原始碼分析2—— 引數處理

Mybatis對引數的處理是值得推敲的,不然在使用的過程中對發生的一系列錯誤直接懵逼了。 以前遇到引數繫結相關的錯誤我就是直接給加@param註解,也稀裡糊塗地解決了,但是後來遇到了一些問題推翻了我的假設:單個引數不需要使用 @param 。由此產生了一個疑問,Mybatis到底是怎

Mybatis 原始碼分析9—— 事物管理

Mybatis 提供了事物的頂層介面: public interface Transaction { /** * Retrieve inner database connection * @return DataBase connection * @throw

Mybatis 原始碼分析8—— 一二級快取

一級快取 其實關於 Mybatis 的一級快取是比較抽象的,並沒有什麼特別的配置,都是在程式碼中體現出來的。 當呼叫 Configuration 的 newExecutor 方法來建立 executor: public Executor newExecutor(Transac

Mybatis原始碼分析7—— 結果集處理

解析封裝 ResultMap 是和結果集相關的東西,最初在解析 XML 的時候,於 parseStatementNode 方法中,針對每一個 select 節點進行解析,轉換為 MappedStatement(類似 Spring 的 bean 配置和 BeanDefinition 的

Mybatis原始碼分析6—— 從JDBC看Mybatis的設計

Java資料庫連線,(Java Database Connectivity,簡稱JDBC)是Java語言中用來規範客戶端程式如何來訪問資料庫的應用程式介面,提供了諸如查詢和更新資料庫中資料的方法。 六步流程: 載入驅動(5.x驅動包不需要這步了) 建立

Mybatis原始碼分析5—— 外掛的原理

MyBatis 允許你在已對映語句執行過程中的某一點進行攔截呼叫。 預設情況下,可以使用外掛來攔截的方法呼叫包括: Executor (update, query, flushStatements, commit, rollback, getTransaction, cl

Flume NG原始碼分析支援執行時動態修改配置的配置模組

在上一篇中講了Flume NG配置模組基本的介面的類,PropertiesConfigurationProvider提供了基於properties配置檔案的靜態配置的能力,這篇細說一下PollingPropertiesFileConfigurationProvider提供的執行時動態修改配置並生效的

GCC原始碼分析——前端

原文連結:http://blog.csdn.net/sonicling/article/details/6706152   從這一篇開始,我們將從原始碼的角度來分析GCC如何完成對C語言原始檔的處理。GCC的內部構架在GCC Internals(搜“gccint.pdf”,或者見[

Glide原始碼分析——從用法來看之load&into方法

上一篇,我們分析了with方法,文章連結: https://blog.csdn.net/qq_36391075/article/details/82833260 在with方法中,進行了Glide的初始化,建立了RequesManger,並且綁定了生命週期,最終返回了一個Reques

YOLOv2原始碼分析

文章全部YOLOv2原始碼分析 接著上一講沒有講完的make_convolutional_layer函式 0x01 make_convolutional_layer //make_convolutional_laye

zigbee 之ZStack-2.5.1a原始碼分析 無線接收控制LED

本文描述ZStack-2.5.1a 模板及無線接收移植相關內容。 main HAL_BOARD_INIT // HAL_TURN_OFF_LED1 InitBoard HalDriverInit HalAdcInit

兄弟連區塊鏈入門教程eth原始碼分析p2p-udp.go原始碼分析

ping方法與pending的處理,之前談到了pending是等待一個reply。 這裡通過程式碼來分析是如何實現等待reply的。pending方法把pending結構體傳送給addpending. 然後等待訊息的處理和接收。 // ping sends a ping message to the giv

Spring原始碼分析IoC容器的實現1

    Ioc(Inversion of Control)——“控制反轉”,不是什麼技術,而是一種設計思想。在Java開發中,Ioc意味著將你設計好的物件交給容器控制,而不是傳統的在你的物件內部直接控制。理解好Ioc的關鍵是要明確“誰控制誰,控制什麼,為何是反轉(有