Mybatis原始碼系列1-Mybaits初始化

享學原始碼發表於2019-12-27

人非要經歷一番不同平時的劫難才能脫胎換骨,成為真正能解決問題的人

簡介初始化過程1.解析XML配置檔案1.1 Config檔案的解析1.2 Mapper檔案的解析1.2.1 解析CURD模板1.2.2 繫結Mapper到名稱空間2.建立SqlSessionFactory總結

簡介

首先我們再回顧下Mybaits的基本使用。

//載入配置檔案
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//SqlSession 的獲取
SqlSession sqlSession = sqlSessionFactory.openSession();
try{
//執行sql
User user = sqlSession.selectOne("MyMapper.selectUser"1);//(SQL通過名稱空間+SQLID 的格式定位)
}finally {
sqlSession .close();
}
複製程式碼

一切都從SqlSessionFactoryBuilder說起。SqlSessionFactoryBuilder是通過builder設計模式來建立一個SqlSessionFactory 工廠。

建立SqlSessionFactory 最主要分為兩步,

public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
    try {
      // 1.xml配置檔案解析器。
      XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
      Configuration config = parser.parse()
      // 2.根據解析到的配置,建立DefaultSqlSessionFactory 
      return build(config);
  }
  //建立預設的sqlsesion工廠。
public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
}
複製程式碼

初始化過程

1.解析XML配置檔案

SqlSessionFactoryBuilder的第一步就是配置的解析。
配置檔案主要分為兩種:

  • Conifg檔案: 包括資料連線配置,全域性設定配置。

  • Mapper檔案:用於SQL的統一管理,對SQL的配置。

    在這裡插入圖片描述
    在這裡插入圖片描述

1.1 Config檔案的解析

Config檔案的解析是由XMLConfigBuilder做的。

 private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
    super(new Configuration());//建立一個Configuration物件
    ErrorContext.instance().resource("SQL Mapper Configuration");
    this.configuration.setVariables(props);
    this.parsed = false;
    this.environment = environment;
    this.parser = parser;
  }
複製程式碼

我們來看看parse()方法

 public Configuration parse() {
    parsed = true;
    //從根節點開始解析
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }
  private void parseConfiguration(XNode root) {
    try {
      // 解析 properties配置
      propertiesElement(root.evalNode("properties"));
      // 解析 settings配置
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      loadCustomVfs(settings);
      typeAliasesElement(root.evalNode("typeAliases"));
      pluginElement(root.evalNode("plugins"));
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      settingsElement(settings);
      //解析environment  配置
      environmentsElement(root.evalNode("environments"));
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      typeHandlerElement(root.evalNode("typeHandlers"));
      // 解析 mapper 配置
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }
複製程式碼

XMLConfigBuilder 根據約定,解析config 配置檔案中的各個標籤。並將相關配置資訊放到Configuration 物件中返回。

列如:

Properties settings = settingsAsProperties(root.evalNode("settings"));//解析settings標籤
settingsElement(settings);//設定settings配置到configuration 物件。
複製程式碼

1.2 Mapper檔案的解析

在解析Config配置檔案過程中,會伴隨Mapper.xml檔案的解析。這個解析的工作是由XMLMapperBuilder 完成的。

config中的mapper檔案位置配置

<mappers>
    <mapper resource="mappers/UserMapper.xml"/>
</mappers>
複製程式碼

Mapper.xml

<mapper namespace="com.wqd.dao.UserMapper">
   <select id="selectUser" resultType="com.wqd.model.User">
      select * from user where id= #{id}
   </select>
</mapper>
複製程式碼

XMLMapperBuilder

public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
        //1.從根標籤mapper開始解析mapper檔案
      configurationElement(parser.evalNode("/mapper"));
      configuration.addLoadedResource(resource);
      //2.繫結mapper到名稱空間
      bindMapperForNamespace();
    }
    parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();
  }

//1.解析mapper檔案
private void configurationElement(XNode context) {
    try {
    //名稱空間必須要有
      String namespace = context.getStringAttribute("namespace");
      if (namespace == null || namespace.equals("")) {
        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"));
      //解析sql標籤配置的sql 片段
      sqlElement(context.evalNodes("/mapper/sql"));
       //解析select|insert|update|delete 標籤配置的sql 模板(重點)  
     buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
    }
  }
//2.繫結名稱空間
private void bindMapperForNamespace() {
    String namespace = builderAssistant.getCurrentNamespace();
    if (namespace != null) {
      Class<?> boundType = null;
      try {
        boundType = Resources.classForName(namespace);
      } catch (ClassNotFoundException e) {
      }
      if (boundType != null) {//如果有對應的Class
        if (!configuration.hasMapper(boundType)) {
          configuration.addLoadedResource("namespace:" + namespace);
          configuration.addMapper(boundType);//新增到快取
        }
      }
    }
  }
複製程式碼

這裡我們重點講講XMLMapperBuilder 中有兩個重要的點

1.2.1 解析CURD模板

也就是解析select|insert|update|delete代表的SQL 模板。這四種標籤配置的SQL模板是我們運算元據庫時的SQL執行語句的模板。 我們可以理解為:一個select 標籤表示一類動態SQL。

buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
複製程式碼

每一個select|insert|update|delete由XMLStatementBuilder 解析

private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    for (XNode context : list) {
       //解析"select|insert|update|delete"標籤
      final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
      try {
        statementParser.parseStatementNode();
      } catch (IncompleteElementException e) {
        configuration.addIncompleteStatement(statementParser);
      }
    }
  }
複製程式碼

XMLStatementBuilder 除了按照約定,解析"select|insert|update|delete"對應的標籤屬性以及子標籤外。最重要的是還會通過MapperBuilderAssistant(構建助手),把解析出來的資訊 封裝成一個MappedStatement 放入到Configuration.mappedStatements 快取中

類XMLStatementBuilder
public class XMLStatementBuilder extends BaseBuilder {
    private MapperBuilderAssistant builderAssistant;//構建助手

    //解析"select|insert|update|delete"標籤
    public void parseStatementNode() {
    String id = context.getStringAttribute("id");
        ...解析
     //助手輔助封裝成MappedStatement  
     builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered, 
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
    }
}

類MapperBuilderAssistant
public class MapperBuilderAssistant extends BaseBuilder {
    public MappedStatement addMappedStatement(...){
         id = applyCurrentNamespace(id, false);//id的處理

         //建立一個MappedStatement物件封裝每一個SQL模板資訊
        MappedStatement statement = statementBuilder.build();
        configuration.addMappedStatement(statement);    
    }
}

//Configuration類
public class Configuration {
    //mappedStatements快取
    protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection");

    //新增MappedStatement
    public void addMappedStatement(MappedStatement ms) {
    mappedStatements.put(ms.getId(), ms);//把MappedStatement 作為KEY
      }
}
複製程式碼

值得一提是id的處理。雖然我們僅僅在select標籤配置了id = "selectUser",

 <select id="selectUser" resultType="com.wqd.model.User">
複製程式碼

但是在構建MappedStatement時 ,並不是把“selectUser”作為id, 而是經過applyCurrentNamespace方法進行處理。

public String applyCurrentNamespace(String base, boolean isReference) {
    if (base == null) {
      return null;
    }
    if (isReference) {
      // is it qualified with any namespace yet?
      if (base.contains(".")) {
        return base;
      }
    } else {
      // is it qualified with this namespace yet?
      if (base.startsWith(currentNamespace + ".")) {
        return base;
      }
      if (base.contains(".")) {
        throw new BuilderException("Dots are not allowed in element names, please remove it from " + base);
      }
    }
    return currentNamespace + "." + base;
  }
複製程式碼

我們可以看出,此方法會返回 namespacec.id 作為MappedStatement.id ,同時在作為Configuration.mappedStatements 快取中MappedStatement的Key. 這就是為啥我們是通過namespace.id的形式來定位SQL的原因

最後解析的結果就是每一個SQL 模板都會建立一個MappedStatement物件(封裝sql模板相關資訊),放入到Configuration.mappedStatements 中。

這樣我們就可以 通過namespace.id 定位到對應的SQL了。

<mapper namespace="com.wqd.dao.UserMapper">
   <select id="selectUser" resultType="com.wqd.model.User">
      select * from user where id= #{id}
   </select>
</mapper>
複製程式碼
User user = sqlSession.selectOne("com.wqd.dao.UserMapper.selectUser"1);
複製程式碼
1.2.2 繫結Mapper到名稱空間

這一步也非常重要,如果namerspacer配置為一個Mpper類的全限定名。那麼就會以namespace對應的類的Class為KEY,以MapperProxyFactory 為value 放入到 Configuration.MapperRegistry.knownMappers快取中。

bindMapperForNamespace();
private void bindMapperForNamespace() {
    String namespace = builderAssistant.getCurrentNamespace();
    if (namespace != null) {
      Class<?> boundType = null;
      try {
        //根據名稱空間名,查詢類Class類
        boundType = Resources.classForName(namespace);
      } catch (ClassNotFoundException e) {
        //ignore, bound type is not required
      }
      if (boundType != null) {
        if (!configuration.hasMapper(boundType)) {
          configuration.addLoadedResource("namespace:" + namespace);
          configuration.addMapper(boundType);//如果找到,就把關聯關係放到快取中
        }
      }
    }
  }

//Configuration類
public class Configuration {
    //mapperRegistry快取
    protected final MapperRegistry mapperRegistry = new MapperRegistry(this);

   public <T> void addMapper(Class<T> type) {
    mapperRegistry.addMapper(type);
    }
}
//Mapper註冊器
public class 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 {
          //以namespace對應的Class類為KEY。新建一個MapperProxyFactory為Value 
        knownMappers.put(type, new MapperProxyFactory<T>(type));
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        parser.parse();
        loadCompleted = true;
      } finally {
        if (!loadCompleted) {
          knownMappers.remove(type);
        }
      }
    }
  }
}
複製程式碼

當我們獲取通過UserMapper獲取Mapper時,MapperProxyFactory 會為我們建立一個代理類,來執行對應CURD操作。

這樣我們就可以通過類名獲取到Mapper#selectUser了。

<mapper namespace="com.wqd.dao.UserMapper">
   <select id="selectUser" resultType="com.wqd.model.User">
      select * from user where id= #{id}
   </select>
</mapper>
複製程式碼
UserMapper userMapper = sqlSession.getMapper(UserMapper.class); 
複製程式碼

config配置檔案與mapper配置檔案解析完成後。最終得到了一個Configuration物件。 而這個Configuration物件就是Mybatis 所需要的所有配置資訊

2.建立SqlSessionFactory

SqlSessionFactoryBuilder 的第二步就是通過Configuration 物件建立一個預設的SqlSessionFactory工廠出來。

public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
  }
複製程式碼

DefaultSqlSessionFactory 用於建立Sqlsession

Sqlsession sqlsession  = DefaultSqlSessionFactory.openSession()
複製程式碼

總結

Mybatis初始化的過程,其就是Config配置檔案,Mapper檔案被解析, Configuration物件被建立的過程。

  • 所有的配置資訊都包含在Configuration 這個大物件中。
  • 每一個SQL模板資訊會被解析成一個MappedStatement物件。根據MappedStatement物件內的SQL模板資訊,我們可以生成一類SQL。

如果本文任何錯誤,請批評指教,不勝感激 !

微信公眾號:原始碼行動
享學原始碼,行動起來,來原始碼行動

相關文章