MyBatis 源碼解析(一):初始化和動態代理

簡介

MyBatis 是 Java 開發中很是流行的 ORM 框架,其封裝了 JDBC 而且解決了 Java 對象與輸入參數和結果集的映射,同時又可以讓用戶方便地手寫 SQL 語句。MyBatis 的行爲相似於如下幾行代碼:java

Class.forName("com.mysql.jdbc.Driver");
Connection conn = DriverManager.getConnection(url, usr, password);
PraparedStatement st = conn.prepareStatement(sql);
st.setInt(0, 1);
st.execute();
ResultSet rs = st.getResultSet();
while (rs.next()) {
    String result = rs.getString(colname);
}

上面是 JDBC 的使用流程,MyBatis 其實就是對上面的代碼進行分解包裝。本文將對 MyBatis 的代碼進行分析,探究其中的邏輯。mysql

基本用法

首先從 MyBatis 的基本用法開始,下面是 MyBatis 官網的入門示例:sql

String resource = "org/mybatis/example/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

其中 mabatis-config.xml 是 MyBatis 的核心配置文件,其中包括數據源、事務管理器、別名以及 SQL 對應的 Mapper 文件等,以下所示:segmentfault

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
  <environments default="development">
    <environment id="development">
      <transactionManager type="JDBC"/>
      <dataSource type="POOLED">
        <property name="driver" value="${driver}"/>
        <property name="url" value="${url}"/>
        <property name="username" value="${username}"/>
        <property name="password" value="${password}"/>
      </dataSource>
    </environment>
  </environments>
  <mappers>
    <mapper resource="org/mybatis/example/BlogMapper.xml"/>
  </mappers>
</configuration>

有了 SqlSessionFactory 後就能夠建立 SqlSession 來調用 select 以及 update 等方法請求數據了:緩存

try {
  BlogMapper mapper = session.getMapper(BlogMapper.class);
  Blog blog = mapper.selectBlog(101);
} finally {
  session.close();
}

配置文件解析

咱們按照上面的代碼流程開始分析源碼,首先是配置文件的解析:SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream) SqlSessionFactoryBuilder 顯然是爲了構建 SqlSessionFactory,並且是從配置文件的輸入流構建,代碼以下:session

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
      // 建立 XMLConfigBuilder
      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
      // parse.parse() 進行解析
      return build(parser.parse());
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
      ErrorContext.instance().reset();
      try {
        inputStream.close();
      } catch (IOException e) {
        // Intentionally ignore. Prefer previous error.
      }
    }
  }

首先是建立了一個 XMLConfigBuilder 對象,它是用來解析 Config 文件的。XMLConfigBuilder 繼承自 BaseBuilderBaseBuilder 中有個 Configuration 類型的變量,這個類須要重點關注,Config 文件中解析出來的全部信息都保存在這個變量中。mybatis

建立了 XMLConfigBuilder 後調用了其 parse 方法:app

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 parseConfiguration(XNode root) {
    try {
      // 解析 properties
      propertiesElement(root.evalNode("properties")); //issue #117 read properties first
      // 解析 type alias
      typeAliasesElement(root.evalNode("typeAliases"));
      pluginElement(root.evalNode("plugins"));
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));      
      // 解析 setting
      settingsElement(root.evalNode("settings"));
      // 解析 environment 
      environmentsElement(root.evalNode("environments")); // read it after objectFactory and objectWrapperFactory issue #631
      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);
    }
  }

這裏解析了 config 文件中全部的標籤,包括 propertiessettings 以及 mappers 等,下面挑幾個看一下。ide

settings

settings 是對 MyBatis 的一些配置項,包括緩存的開啓以及是否使用駝峯轉換(mapUnderscoreToCamelCase)等,代碼以下:

private void settingsElement(XNode context) throws Exception {
   if (context != null) {
   // 將配置項保存到 Properties 中
     Properties props = context.getChildrenAsProperties();
     // Check that all settings are known to the configuration class
     MetaClass metaConfig = MetaClass.forClass(Configuration.class);
     for (Object key : props.keySet()) {
       if (!metaConfig.hasSetter(String.valueOf(key))) {
         throw new BuilderException("The setting " + key + " is not known.  Make sure you spelled it correctly (case sensitive).");
       }
     }
     configuration.setAutoMappingBehavior(AutoMappingBehavior.valueOf(props.getProperty("autoMappingBehavior", "PARTIAL")));
     // 默認開啓緩存
     configuration.setCacheEnabled(booleanValueOf(props.getProperty("cacheEnabled"), true));
     configuration.setProxyFactory((ProxyFactory) createInstance(props.getProperty("proxyFactory")));      
     configuration.setLazyLoadingEnabled(booleanValueOf(props.getProperty("lazyLoadingEnabled"), false));
     configuration.setAggressiveLazyLoading(booleanValueOf(props.getProperty("aggressiveLazyLoading"), true));
     configuration.setMultipleResultSetsEnabled(booleanValueOf(props.getProperty("multipleResultSetsEnabled"), true));
     configuration.setUseColumnLabel(booleanValueOf(props.getProperty("useColumnLabel"), true));
     configuration.setUseGeneratedKeys(booleanValueOf(props.getProperty("useGeneratedKeys"), false));
     configuration.setDefaultExecutorType(ExecutorType.valueOf(props.getProperty("defaultExecutorType", "SIMPLE")));
     configuration.setDefaultStatementTimeout(integerValueOf(props.getProperty("defaultStatementTimeout"), null));
     configuration.setMapUnderscoreToCamelCase(booleanValueOf(props.getProperty("mapUnderscoreToCamelCase"), false));
     configuration.setSafeRowBoundsEnabled(booleanValueOf(props.getProperty("safeRowBoundsEnabled"), false));
     configuration.setLocalCacheScope(LocalCacheScope.valueOf(props.getProperty("localCacheScope", "SESSION")));
     configuration.setJdbcTypeForNull(JdbcType.valueOf(props.getProperty("jdbcTypeForNull", "OTHER")));
     configuration.setLazyLoadTriggerMethods(stringSetValueOf(props.getProperty("lazyLoadTriggerMethods"), "equals,clone,hashCode,toString"));
     configuration.setSafeResultHandlerEnabled(booleanValueOf(props.getProperty("safeResultHandlerEnabled"), true));
     configuration.setDefaultScriptingLanguage(resolveClass(props.getProperty("defaultScriptingLanguage")));
     configuration.setCallSettersOnNulls(booleanValueOf(props.getProperty("callSettersOnNulls"), false));
     configuration.setLogPrefix(props.getProperty("logPrefix"));
     configuration.setLogImpl(resolveClass(props.getProperty("logImpl")));
   }
 }

能夠看出,settings 的子節點保存在 Properties 中,而後校驗是否有不合法的子節點,最後提取出其中的屬性保存到 Configuration 中,上面提到這個類專門用於保存 Config 文件解析出的信息。

從上面也能夠看到 MyBatis 的一些默認屬性,例如一級緩存若是沒有配置,那麼默認是開啓的。

environments

environments 包含了數據源(dataSource) 和事務管理器(transactionManager) 的配置,代碼以下:

private void environmentsElement(XNode context) throws Exception {
    if (context != null) {
      if (environment == null) {
        environment = context.getStringAttribute("default");
      }
      for (XNode child : context.getChildren()) {
        String id = child.getStringAttribute("id");
        if (isSpecifiedEnvironment(id)) {
          // 解析 transactionManager
          TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
          // 解析 dataSource
          DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
          DataSource dataSource = dsFactory.getDataSource();
          Environment.Builder environmentBuilder = new Environment.Builder(id)
              .transactionFactory(txFactory)
              .dataSource(dataSource);
          // 設置 environment 到 configuration
          configuration.setEnvironment(environmentBuilder.build());
        }
      }
    }
  }

private TransactionFactory transactionManagerElement(XNode context) throws Exception {
    if (context != null) {
      String type = context.getStringAttribute("type");
      Properties props = context.getChildrenAsProperties();
      // 經過反射實例化
      TransactionFactory factory = (TransactionFactory) resolveClass(type).newInstance();
      factory.setProperties(props);
      return factory;
    }
    throw new BuilderException("Environment declaration requires a TransactionFactory.");
  }

  private DataSourceFactory dataSourceElement(XNode context) throws Exception {
    if (context != null) {
      String type = context.getStringAttribute("type");
      Properties props = context.getChildrenAsProperties();
      // 經過反射實例化
      DataSourceFactory factory = (DataSourceFactory) resolveClass(type).newInstance();
      factory.setProperties(props);
      return factory;
    }
    throw new BuilderException("Environment declaration requires a DataSourceFactory.");
  }

其中主要是兩部分,第一部分解析 transactionManager,第二部分解析 dataSource。從 transactionManagerElementdataSourceElement 中能夠看出經過對應 Class 文件的 newInstance 實例化出對應的工廠對象。最終解析出的 transactionManagerdataSource 依然是設置到 Configuration 中。

mappers

mappers 對應了具體的 SQL Mapper 文件,也是咱們要分析的重點。

mappers 標籤能夠多種子標籤,上面的示例中是 mapper 配合 resource

<mappers>
    <mapper resource="org/mybatis/example/BlogMapper.xml"/>
  </mappers>

咱們下面看一下此種形式在源碼中的解析:

private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        if ("package".equals(child.getName())) {
          String mapperPackage = child.getStringAttribute("name");
          configuration.addMappers(mapperPackage);
        } else {
          String resource = child.getStringAttribute("resource");
          String url = child.getStringAttribute("url");
          String mapperClass = child.getStringAttribute("class");
          // 這個分支解析 resource 形式的標籤
          if (resource != null && url == null && mapperClass == null) {
            ErrorContext.instance().resource(resource);
            InputStream inputStream = Resources.getResourceAsStream(resource);
            // 建立 XMLMapperBuilder 
            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.");
          }
        }
      }
    }
  }

resource 標籤解析的對應分支是 (resource != null && url == null && mapperClass == null),其中建立了一個 XMLMapperBuilder 對象而後調用 parse 方法進行解析:

public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
      // 解析 mapper 下面的標籤,包括 namespace、cache、parameterMap、resultMap 等
      configurationElement(parser.evalNode("/mapper"));
      configuration.addLoadedResource(resource);
      bindMapperForNamespace();
    }

    parsePendingResultMaps();
    parsePendingChacheRefs();
    parsePendingStatements();
  }

 private void configurationElement(XNode context) {
    try {
      // namespace 對應 Mapper 對應接口的全名(包名 + 類名)
      String namespace = context.getStringAttribute("namespace");
      builderAssistant.setCurrentNamespace(namespace);
      cacheRefElement(context.evalNode("cache-ref"));
      cacheElement(context.evalNode("cache"));
      // 解析生成 ParameterMap
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      // 解析生成 ResultMap
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      sqlElement(context.evalNodes("/mapper/sql"));
      // 每個 sql 語句生成一個 MappedStatement
      buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
      throw new RuntimeException("Error parsing Mapper XML. Cause: " + e, e);
    }
  }

configurationElement 用於解析具體的子標籤,如 namespacecacheparameterMapresultMap 以及 select|insert|update|delete 等。

namespace 對應了 Mapper 接口類的包名 + 類名,經過 namespace 能夠惟必定位一個 Class 文件,解析的 namespace 保存在 builderAssistant 中,後面會用到。

parameterMapresultMap 解析會生成 ParameterMapResultMap 對象。每一個 SQL 語句解析會生成 MappedStatement

在上面的 parse 方法中,解析完標籤後調用了 bindMapperForNamespace,這個實現了加載 namespace 對應的 Class,而且爲每一個 Class 建立了代理類工廠對象(MapperProxyFactory)。

MapperProxyFactory

MapperProxyFactory 用於爲 Mapper 接口類建立代理對象,代理對象指的是
BlogMapper mapper = session.getMapper(BlogMapper.class) 生成的對象。

下面從 bindMapperForNamespace 開始:

private void bindMapperForNamespace() {
    String namespace = builderAssistant.getCurrentNamespace();
    if (namespace != null) {
      Class<?> boundType = null;
      try {
        // 加載類
        boundType = Resources.classForName(namespace);
      } catch (ClassNotFoundException e) {
        //ignore, bound type is not required
      }
      if (boundType != null) {
        if (!configuration.hasMapper(boundType)) {
          // Spring may not know the real resource name so we set a flag
          // to prevent loading again this resource from the mapper interface
          // look at MapperAnnotationBuilder#loadXmlResource
          configuration.addLoadedResource("namespace:" + namespace);
          // 添加 mapper 和 MapperProxyFactory
          configuration.addMapper(boundType);
        }
      }
    }
  }

其中先從 builderAssistant 取出 namespace,而後加載對應的 Class(boundType = Resources.classForName(namespace))。最後調用 configuration.addMapper(boundType) 添加到 configuration 中。 configuration.addMapper(boundType) 很關鍵,看代碼:

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 {
        // 添加到  Map<Class<?>, MapperProxyFactory<?>> 中
        knownMappers.put(type, new MapperProxyFactory<T>(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.put(type, new MapperProxyFactory<T>(type)),其中 knownMappers 的類型是 Map<Class<?>, MapperProxyFactory<?>>,即 key 是 Class,value 是 MapperProxyFactory。這裏的 MapperProxyFactory 便是動態代理對象的工廠,下面是其 newInstance 方法的代碼:

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

從中能夠看出,這裏用的是 Java 的動態代理,Proxy.newProxyInstance 方法生成指定接口的代理對象,這個方法的第三個參數是用於方法攔截的對象,這裏是 MapperProxy 的實例。

由此能夠知道,具體的執行 SQL 語句的操做是由這個類攔截而且執行的,看看這個類的 invoke 方法:

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    if (Object.class.equals(method.getDeclaringClass())) {
      return method.invoke(this, args);
    }
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);
  }

若是是 Object 類中聲明的方法,則直接執行,不然調用 MapperMethodexecute,其中即是 JDBC 相關的邏輯了。限於篇幅,具體內容留到下一篇文章再看。

在分析完配置文件的解析後,再回到 XMLConfigBuilder 中:

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
      // 構建 SqlSessionFactory
      return build(parser.parse());
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
      ErrorContext.instance().reset();
      try {
        inputStream.close();
      } catch (IOException e) {
        // Intentionally ignore. Prefer previous error.
      }
    }
  }

parser.parse() 執行完後,生成一個 Configuration 對象,最後調用 build 構建 SqlSessionFactory,代碼以下:

public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
  }

能夠看到,最終建立的是 DefaultSqlSessionFactory,這個類內部持有 Configuration,而且提供了多個重載的 openSession 方法用於建立 SqlSession

到這裏,初始化部分就結束了。

總結

MyBatis 的初始化流程主要是解析配置文件,將相關信息保存在 Configuration 中,同時對每一個 namespace 表明的 Class 生成代理對象工廠。最後,利用 Configuration 生成了一個 DefaultSqlSessionFactory,經過這個對象能夠建立 SqlSession 執行 SQL 請求,相關內容將在下一篇(MyBatis 源碼解析(二):SqlSession 執行流程)分析。

若是個人文章對您有幫助,不妨點個贊支持一下(^_^)

相關文章
相關標籤/搜索