mybatis 源碼賞析(一)sql解析篇


本系列主要分為三部分,前兩部分主要分析mybatis的實現原理,最后一部分結合spring,來看看mybtais是如何與spring結合的就是就是mybatis-spring的源碼。

相較於spring,mybatis源碼算是比較容易理解的,因為很少用一層層的抽象,類所做的事一目了然,但是要說質量的話,我還是偏向與spring,只是個人意見,好了我們開始:

為了便於理解,我們分兩部分介紹mybatis,本篇着重介紹mybtais是如何解析xml或者注解,並將其結構化為自己的類。

先看mybatis官網上的一個例子:

 public static void main(String[] args) throws IOException {
        InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); //本篇只分析到這
        SqlSession sqlSession = sqlSessionFactory.openSession();
        BlogMapper blogMapper = sqlSession.getMapper(BlogMapper.class);
        Blog blog = blogMapper.getById(1);
}

拋開其他框架,我們只用mybatis的話可以看到,核心的幾個類:

SqlSessionFactory,
SqlSession,
SqlSessionFactoryBuilder

老規矩,在正真看代碼之前,我們先把核心的幾個類拿出來解釋一下,然后看一下類圖,從整體上了解mybatis的設計:

SqlSessionFactory:顧名思義,sqlsession的工廠類,從上面的例子可以看出,一個SqlSessionFactory實例對應一個mybatis-config.xml配置即一個數據源
SqlSessionFactoryBuilder:SqlSessionFactory的組裝車間,這里用了類似建造者模式,為啥是類似,因為這不是標准的建造者模式,這里說一句,mybatis里很多地方都用了這種不是很標准的建造者模式。
SqlSession:一次數據庫會話對應一個SqlSession實例
Configuration:這個類是本篇的重點,它和sqlsessionfactory的重要產出,我們在xml或者注解中的幾乎所有配置都會被解析並裝載到configuration的實例中。
MappedStatement:這個類實際上是被configuration持有的,之所以拿出來單獨說,是因為它太重要了,我們的sql相關的配置,都會被解析放在這個類的實例中,因此,很多分頁插件也是通過改變這個類中的sql,來將分頁的邏輯切入正常邏輯中。(當然不僅僅可以做分頁,也可以做加密等等)。
 public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
    try {
      XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
      return build(parser.parse());
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
      ErrorContext.instance().reset();
      try {
        reader.close();
      } catch (IOException e) {
        // Intentionally ignore. Prefer previous error.
      }
    }
  }

Configuration這個類的實例是在XMLConfigBuilder的構造方法中被創造出來的,我們重點來看解析的邏輯:

public Configuration parse() {
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }

順便提一句,mybatis解析xml用的是java中自帶的解析器的,有關xml解析的知識這里不會細講,有興趣的同學可以去了解一下dom4j,dom,sax,jdom等的區別和優劣。

 private void parseConfiguration(XNode root) {
    try {
      //issue #117 read properties first
      propertiesElement(root.evalNode("properties"));
      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); // 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);
    }
  }

這里我們主要看這幾個方法,typeAliasesElement方法是注冊類的別名,我們在xml中指定resultType或paramterType時會用到。這里使用了之前配置的VFS的實現類去裝載指定package底下的類的字節碼,然后通過反射獲取類的信息。

settingsElement方法是將之前的配置賦值給configuration實例,簡單的賦值,這里就不上源碼了。

typeHandlerElement注冊了類型處理器,同樣,和typeAliasesElement方法類似,這個方法也可以去掃描指定包路徑底下的類,並為這些類創建別名,默認使用的是Class.getSimpleName(),即類名稱的縮寫。

接下來會解析插件,然后實例化注冊到Configuration中,等運行時動態代理目標類, 這部分我們會在下一章重點分析,這里不做介紹,然后會把所有的配置都設置到configuration中,后面的解析datasource和解析typehandler的邏輯這里就不分析了,都是簡單的解析賦值操作,我們重點來看mapperElement方法:

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

這里主要是兩部分,上面是掃描包,根據注解生成對應的mappedStatement,第二部分是解析xml配置,根據xml配置來生成mappedstatement,我們先看第一部分,根據注解生成mappedstatemnet:

configuration會把工作委托給MapperRegistry去做,MapperRegistry會持有所有被解析的接口(運行時生成動態代理用),而最終解析的產物:mappedstatement依然會被configuration實例持有放在mappedStatements的map中:

  public void addMappers(String packageName, Class<?> superType) {
    ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
    resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
    Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
    for (Class<?> mapperClass : mapperSet) {
      addMapper(mapperClass);
    }
  }

這里同樣是掃描指定包路徑地下的所有類,並且根據filter(new ResolverUtil.IsA(superType)),挑選出滿足條件的類,這里的條件是Object.class,所以包底下的所有類都會被裝進來,接下來就是遍歷這些類然后解析了:

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

我們看到,這里只解析所有接口,MapperRegistry所持有的是一個knownMappers,這里會有一個工廠類的實例MapperProxyFactory,這個類會在下一章介紹,會在生成接口的動態代理時被調用,我們繼續往下看,接下來就是接口的解析工作了:

 public void parse() {
    String resource = type.toString();
    if (!configuration.isResourceLoaded(resource)) {
      loadXmlResource();
      configuration.addLoadedResource(resource);
      assistant.setCurrentNamespace(type.getName());
      parseCache();
      parseCacheRef();
      Method[] methods = type.getMethods();
      for (Method method : methods) {
        try {
          // issue #237
          if (!method.isBridge()) {
            parseStatement(method);
          }
        } catch (IncompleteElementException e) {
          configuration.addIncompleteMethod(new MethodResolver(this, method));
        }
      }
    }
    parsePendingMethods();
  }

在執行真正解析之前,mybatis又去load了一次xml文件,這是為了防止之前沒有裝在xml,保證一定是xml被解析完,再解析接口,

  void parseStatement(Method method) {
    Class<?> parameterTypeClass = getParameterType(method);
    LanguageDriver languageDriver = getLanguageDriver(method);
    SqlSource sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver);
    if (sqlSource != null) {
      Options options = method.getAnnotation(Options.class);
      final String mappedStatementId = type.getName() + "." + method.getName();
      Integer fetchSize = null;
      Integer timeout = null;
      StatementType statementType = StatementType.PREPARED;
      ResultSetType resultSetType = ResultSetType.FORWARD_ONLY;
      SqlCommandType sqlCommandType = getSqlCommandType(method);
      boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
      boolean flushCache = !isSelect;
      boolean useCache = isSelect;

      KeyGenerator keyGenerator;
      String keyProperty = null;
      String keyColumn = null;
      if (SqlCommandType.INSERT.equals(sqlCommandType) || SqlCommandType.UPDATE.equals(sqlCommandType)) {
        // first check for SelectKey annotation - that overrides everything else
        SelectKey selectKey = method.getAnnotation(SelectKey.class);
        if (selectKey != null) {
          keyGenerator = handleSelectKeyAnnotation(selectKey, mappedStatementId, getParameterType(method), languageDriver);
          keyProperty = selectKey.keyProperty();
        } else if (options == null) {
          keyGenerator = configuration.isUseGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
        } else {
          keyGenerator = options.useGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
          keyProperty = options.keyProperty();
          keyColumn = options.keyColumn();
        }
      } else {
        keyGenerator = NoKeyGenerator.INSTANCE;
      }

      if (options != null) {
        if (FlushCachePolicy.TRUE.equals(options.flushCache())) {
          flushCache = true;
        } else if (FlushCachePolicy.FALSE.equals(options.flushCache())) {
          flushCache = false;
        }
        useCache = options.useCache();
        fetchSize = options.fetchSize() > -1 || options.fetchSize() == Integer.MIN_VALUE ? options.fetchSize() : null; //issue #348
        timeout = options.timeout() > -1 ? options.timeout() : null;
        statementType = options.statementType();
        resultSetType = options.resultSetType();
      }

      String resultMapId = null;
      ResultMap resultMapAnnotation = method.getAnnotation(ResultMap.class);
      if (resultMapAnnotation != null) {
        String[] resultMaps = resultMapAnnotation.value();
        StringBuilder sb = new StringBuilder();
        for (String resultMap : resultMaps) {
          if (sb.length() > 0) {
            sb.append(",");
          }
          sb.append(resultMap);
        }
        resultMapId = sb.toString();
      } else if (isSelect) {
        resultMapId = parseResultMap(method);
      }

      assistant.addMappedStatement(
          mappedStatementId,
          sqlSource,
          statementType,
          sqlCommandType,
          fetchSize,
          timeout,
          // ParameterMapID
          null,
          parameterTypeClass,
          resultMapId,
          getReturnType(method),
          resultSetType,
          flushCache,
          useCache,
          // TODO gcode issue #577
          false,
          keyGenerator,
          keyProperty,
          keyColumn,
          // DatabaseID
          null,
          languageDriver,
          // ResultSets
          options != null ? nullOrEmpty(options.resultSets()) : null);
    }
  }

這洋洋灑灑一堆,目的就是為了解析注解的配置,然后構建一個mappedstatement,我們只看核心邏輯:

首先,mybatis會根據注解生成一個sqlSource,這個接口是承載sql的實例,接口只有一個方法:getBoundSql,這個方法會根據傳入的參數,將原來的sql解析,替換為能被數據庫識別執行的sql,然后放入boundsql中,同樣,這個方法是在運行時才被調用的。這里我們只看生成sqlsource的邏輯:

  private SqlSource getSqlSourceFromAnnotations(Method method, Class<?> parameterType, LanguageDriver languageDriver) {
    try {
      Class<? extends Annotation> sqlAnnotationType = getSqlAnnotationType(method);
      Class<? extends Annotation> sqlProviderAnnotationType = getSqlProviderAnnotationType(method);
      if (sqlAnnotationType != null) {
        if (sqlProviderAnnotationType != null) {
          throw new BindingException("You cannot supply both a static SQL and SqlProvider to method named " + method.getName());
        }
        Annotation sqlAnnotation = method.getAnnotation(sqlAnnotationType);
        final String[] strings = (String[]) sqlAnnotation.getClass().getMethod("value").invoke(sqlAnnotation);
        return buildSqlSourceFromStrings(strings, parameterType, languageDriver);
      } else if (sqlProviderAnnotationType != null) {
        Annotation sqlProviderAnnotation = method.getAnnotation(sqlProviderAnnotationType);
        return new ProviderSqlSource(assistant.getConfiguration(), sqlProviderAnnotation, type, method);
      }
      return null;
    } catch (Exception e) {
      throw new BuilderException("Could not find value method on SQL annotation.  Cause: " + e, e);
    }
  }

這里只有兩種情況,普通的sql,和sqlprovider,我們來看核心方法:

@Override
  public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
    // issue #3
    if (script.startsWith("<script>")) {
      XPathParser parser = new XPathParser(script, false, configuration.getVariables(), new XMLMapperEntityResolver());
      return createSqlSource(configuration, parser.evalNode("/script"), parameterType);
    } else {
      // issue #127
      script = PropertyParser.parse(script, configuration.getVariables());
      TextSqlNode textSqlNode = new TextSqlNode(script);
      if (textSqlNode.isDynamic()) {
        return new DynamicSqlSource(configuration, textSqlNode);
      } else {
        return new RawSqlSource(configuration, script, parameterType);
      }
    }
  }

首先判斷是不是腳本,如果是腳本則走解析腳本的邏輯,如果不是,則判斷是否是動態sql,判斷的邏輯就是sql中是否含有 ”${}" 這樣的關鍵字,如果有則是動態sql,如果不是,則是靜態的。靜態的話在構造方法中還有一段邏輯:

  public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class<?> clazz = parameterType == null ? Object.class : parameterType;
    sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<String, Object>());
  }

這段的作用,是繼續解析sql中的 “#{}” ,將其替換為 ? ,然后返回一個StaticSqlSource的實例。其實DynamicSqlSource最中也是轉化為StaticSqlSource的,只不過它是在getBoundSql被調用的時候才做的,而且這里還會把"${}"替換為相應的參數:

  public BoundSql getBoundSql(Object parameterObject) {
    DynamicContext context = new DynamicContext(configuration, parameterObject);
    rootSqlNode.apply(context);
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
    SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
      boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
    }
    return boundSql;
  }

所以我們拿到的BoudSql的實例實際上已經經過了解析。

解析完sqlsource后,mybatis會生成相應的mappedstatement,為了區分不同的mappedstatement,mysql為其創建了一個Id:

final String mappedStatementId = type.getName() + "." + method.getName();

這里可以看到,Id是類名加上方法名,這里就有一個問題,當類中的方法被重載時,mybatis會認為有問題的,可以看到,雖然方法被重載,mappedStatementId依然是同一個,所以mybatis中sql的接口是不能重載的。

下面就是根據注解的配置,創建相應的對象,然后一起組裝成mappedstatement對象,然后放入configuration實例中的mappedstatements中。

 

至此,mybaits的sql解析篇就到此結束了,當然,mybatis的功能還遠遠不止如此,我們將在下一篇,mybatis的執行中,看到mybatis在運行時是如何代理接口,mybatis的各種插件有事如何介入的。

 

                                                                                                                                                                   轉載請注明出處,謝謝~

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM