MyBatis源码分析之@SelectProvider注解使用详解

Posted 叶长风

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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

以上是关于MyBatis源码分析之@SelectProvider注解使用详解的主要内容,如果未能解决你的问题,请参考以下文章

mybatis源码分析之06二级缓存

mybatis源码分析之05一级缓存

MyBatis源码分析之核心流程介绍(上)

MyBatis源码分析之核心流程介绍(上)

MyBatis源码分析之@SelectProvider注解使用详解

mybatis之SqlSessionFactory源码分析