1. 程式人生 > >MyBatis原始碼分析之@SelectProvider註解使用詳解

MyBatis原始碼分析之@SelectProvider註解使用詳解

MyBatis原始碼分析之@SelectProvider註解使用詳解

之前講了MyBatis的配置、plugin、Select查詢,還有@MapKey註解的使用與原理,還有返回@ResultMap等等,我原想直接從MyBatis的快取開始說起,但是想想還是得說一下MyBatis中的@selectProvider,這個註解我也是在用了好久的MyBatis才用到,功能就是用來單獨寫一個class類與方法,用來提供一些xml或者註解中不好寫的sql,今天就來說下這個註解的具體用法與原始碼。

@SelectProvider註解用法


寫一個簡單的@SelectProvider的用法,新建class類,新增一個根據userId查詢user的方法。

SelectSqlProvider:

public class SelectSqlProvider {

    public String selectByUserId(Long id) {
        StringBuffer buffer = new StringBuffer();
        buffer.append("SELECT * FROM user where ");
        buffer.append("id = ").append(id).append(";");
        return buffer.toString();
    }
}

SelectSqlProvider中提供了一個很簡單的查詢方法,根據userId返回user物件,裡面就是用了一個StringBuffer物件來拼接一個SQL語句,我想更多的是想用MyBatis中的SQL Builder的寫法,SQL Builder寫法在官方網站地址為http://www.mybatis.org/mybatis-3/zh/statement-builders.html,不得不說SQL Builder的寫法確實比較漂亮,很工整,不過也是看自己運用的熟練程度吧。

UserMapper:

@ResultMap("BaseResultMap")
@SelectProvider
(type = SelectSqlProvider.class, method = "selectByUserId") User getUserByUserId(long id);

mapper中的其他方法就不貼出來了,需要說的就是這一個,這一個方法在xml中沒有對應的sql,在該方法上也沒有@Select註解修飾,只有@SelectProvider註解,@SelectProvider中兩個屬性,type為提供sql的class類,method為指定方法。

對應Mapper的呼叫與結果在這就不再分析了,就是簡單的返回user物件,下文將是對@SelectProvider註解作用的詳解。

2. @SelectProvider原始碼分析


說起Select查詢,基本就又是回到我們先前那幾篇文章說的了,@SelectProvider註解載入問題,之前的文章中說了如何在解析xml之後解析註解中的SQL,這一種無非換了種樣式,從由註解提供改為了從class類中單獨寫方法提供SQL,我們來看下相關原始碼實現。
這裡就還要回到mapper的解析處,回到開始的parseConfiguration方法中mapperElement。

mapperElement(root.evalNode("mappers"));

這一行在解析xml檔案之後,最後進行了addMapper操作。

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

但是從前文中我們知addMapper操作不僅將mapper儲存進knownMappers中,並且還進行了註解Mapper的解析,從而實現了對註解sql的載入,同時**@SelectProvider**也是在這裡進行載入的。

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;

進入到parse方法中,parse方法最終轉到parseStatement方法,在parseStatement方法中,在獲取SqlSource物件時,對method方法進行了進一步的解析。

SqlSource sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver);

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

這裡可以加上斷點,對我們上面寫的程式碼除錯一下,如下圖。

![image-20190108071518992](/Users/xiaxuan/Library/Application Support/typora-user-images/image-20190108071518992.png)

到這一步就是對@SelectProvider註解的解析,可以看到此時的method方法為getUserByUserId。type型別為UserMapper等等。我們繼續進入到ProviderSqlSource中,看看是如何組裝sql的。

public ProviderSqlSource(Configuration configuration, Object provider, Class<?> mapperType, Method mapperMethod) {
    String providerMethodName;
    try {
      this.configuration = configuration;
      this.sqlSourceParser = new SqlSourceBuilder(configuration);
      this.providerType = (Class<?>) provider.getClass().getMethod("type").invoke(provider);
      providerMethodName = (String) provider.getClass().getMethod("method").invoke(provider);

      for (Method m : this.providerType.getMethods()) {
        if (providerMethodName.equals(m.getName()) && CharSequence.class.isAssignableFrom(m.getReturnType())) {
          if (providerMethod != null){
            throw new BuilderException("Error creating SqlSource for SqlProvider. Method '"
                    + providerMethodName + "' is found multiple in SqlProvider '" + this.providerType.getName()
                    + "'. Sql provider method can not overload.");
          }
          this.providerMethod = m;
          this.providerMethodArgumentNames = new ParamNameResolver(configuration, m).getNames();
          this.providerMethodParameterTypes = m.getParameterTypes();
        }
      }
    } catch (BuilderException e) {
      throw e;
    } catch (Exception e) {
      throw new BuilderException("Error creating SqlSource for SqlProvider.  Cause: " + e, e);
    }
    if (this.providerMethod == null) {
      throw new BuilderException("Error creating SqlSource for SqlProvider. Method '"
          + providerMethodName + "' not found in SqlProvider '" + this.providerType.getName() + "'.");
    }
    for (int i = 0; i< this.providerMethodParameterTypes.length; i++) {
      Class<?> parameterType = this.providerMethodParameterTypes[i];
      if (parameterType == ProviderContext.class) {
        if (this.providerContext != null){
          throw new BuilderException("Error creating SqlSource for SqlProvider. ProviderContext found multiple in SqlProvider method ("
              + this.providerType.getName() + "." + providerMethod.getName()
              + "). ProviderContext can not define multiple in SqlProvider method argument.");
        }
        this.providerContext = new ProviderContext(mapperType, mapperMethod);
        this.providerContextIndex = i;
      }
    }
  }

此處對sqlSourceParser與providerType、providerMethodName等引數進行了例項化與賦值,最後返回sqlSource物件。
此處得到的可以說還不是原有的sql,所以在Select查詢的時候,還要繼續追蹤看一下到底是如何執行sql的,這就要繼續回到Select查詢方法了,在前面很多文章中知最後查詢呼叫基本都是呼叫的selectList方法,此處還是要從這裡分析開始。

@Override
  public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      MappedStatement ms = configuration.getMappedStatement(statement);
      return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

進入到executor.query方法中,executor的實現有兩種,一種是BaseExecutor,一種是CacheingExecutor,而這種的初始化條件為openSession中的newExecutor方法。

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;
    if (ExecutorType.BATCH == executorType) {
      executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
      executor = new ReuseExecutor(this, transaction);
    } else {
      executor = new SimpleExecutor(this, transaction);
    }
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

這裡一般就是例項化為Simple型別,但是如果cacheEnable欄位為true的話,返回CachingExecutor物件。而cacheEnable欄位算得上是之前漏說了的一個屬性,這個是在loadSettings時進行初始化的,而如果沒有設定cacheEnable欄位時,預設設定為true,如下:

private void settingsElement(Properties props) throws Exception { configuration.setAutoMappingUnknownColumnBehavior(AutoMappingUnknownColumnBehavior.valueOf(props.getProperty("autoMappingUnknownColumnBehavior", "NONE")));
    configuration.setCacheEnabled(booleanValueOf(props.getProperty("cacheEnabled"), true));
    configuration.setProxyFactory((ProxyFactory) createInstance(props.getProperty("proxyFactory")))
  }

在說完BaseExecutor和CacheingExecutor之後,此處繼續回到query方法。

@Override
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameter);
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
 }

在query方法中獲取到boundSql物件,此處可以除錯一下程式碼,看看boundSql中有什麼引數。
![image-20190108071555453](/Users/xiaxuan/Library/Application Support/typora-user-images/image-20190108071555453.png)

此處已經完成了sql的組裝,繼續getBoundSql看看進行了什麼操作。

public BoundSql getBoundSql(Object parameterObject) {
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings == null || parameterMappings.isEmpty()) {
      boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject);
    }

    // check for nested result maps in parameter mappings (issue #30)
    for (ParameterMapping pm : boundSql.getParameterMappings()) {
      String rmId = pm.getResultMapId();
      if (rmId != null) {
        ResultMap rm = configuration.getResultMap(rmId);
        if (rm != null) {
          hasNestedResultMaps |= rm.hasNestedResultMaps();
        }
      }
    }

    return boundSql;
  }

還需要繼續追溯sqlSource.getBoundSql(parameterObject),此處SqlSource毫無疑問為ProviderSqlSource類。

@Override
  public BoundSql getBoundSql(Object parameterObject) {
    SqlSource sqlSource = createSqlSource(parameterObject);
    return sqlSource.getBoundSql(parameterObject);
  }
private SqlSource createSqlSource(Object parameterObject) {
    try {
      int bindParameterCount = providerMethodParameterTypes.length - (providerContext == null ? 0 : 1);
      String sql;
      if (providerMethodParameterTypes.length == 0) {
        sql = invokeProviderMethod();
      } else if (bindParameterCount == 0) {
        sql = invokeProviderMethod(providerContext);
      } else if (bindParameterCount == 1 &&
              (parameterObject == null || providerMethodParameterTypes[(providerContextIndex == null || providerContextIndex == 1) ? 0 : 1].isAssignableFrom(parameterObject.getClass()))) {
        sql = invokeProviderMethod(extractProviderMethodArguments(parameterObject));
      } else if (parameterObject instanceof Map) {
        @SuppressWarnings("unchecked")
        Map<String, Object> params = (Map<String, Object>) parameterObject;
        sql = invokeProviderMethod(extractProviderMethodArguments(params, providerMethodArgumentNames));
      } else {
        throw new BuilderException("Error invoking SqlProvider method ("
                + providerType.getName() + "." + providerMethod.getName()
                + "). Cannot invoke a method that holds "
                + (bindParameterCount == 1 ? "named argument(@Param)": "multiple arguments")
                + " using a specifying parameterObject. In this case, please specify a 'java.util.Map' object.");
      }
      Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
      return sqlSourceParser.parse(replacePlaceholder(sql), parameterType, new HashMap<String, Object>());
    } catch (BuilderException e) {
      throw e;
    } catch (Exception e) {
      throw new BuilderException("Error invoking SqlProvider method ("
          + providerType.getName() + "." + providerMethod.getName()
          + ").  Cause: " + e, e);