本文源代码来源于mybatis-spring-boot-starter的2.1.2版本
1.XML还是Annotation
  前面我们介绍了MappedStatement的创建流程,在SqlSessionFacotrybulid的时候,SqlSessionFacotryBean会加载配置文件中mapper-locations,对该路径下的 *mapper.xml文件进行解析,并最终生成MappedStatement放在Configuration中供后面执行sql方法时使用。
  不过不少同学可能会觉得创建xml文件写sql过于繁琐,更偏爱使用@select等注解,直接把sql写在Mapper接口里,岂不美哉?虽然笔者更偏向xml,易于维护。仁者见仁,我们还是来探究下通过Annotation是怎么玩的?
2.Annotation的使用
设计初期的 MyBatis 是一个 XML 驱动的框架。配置信息是基于 XML 的,映射语句也是定义在 XML 中的。而在 MyBatis 3 中,我们提供了其它的配置方式。MyBatis 3 构建在全面且强大的基于 Java 语言的配置 API 之上。它是 XML 和注解配置的基础。注解提供了一种简单且低成本的方式来实现简单的映射语句。
提示不幸的是,Java 注解的表达能力和灵活性十分有限。尽管我们花了很多时间在调查、设计和试验上,但最强大的 MyBatis 映射并不能用注解来构建——我们真没开玩笑。而 C# 属性就没有这些限制,因此 MyBatis.NET 的配置会比 XML 有更大的选择余地。虽说如此,基于 Java 注解的配置还是有它的好处的。
public interface RoleMapper {
  @Select("select * from role")
    @Results({
            @Result(property = "roleId",column = "role_id"),
            @Result(property = "roleName",column = "role_name")
    })
    List<Role> selectALl();
    @Select("select * from role where id = #{id}")
    @Results({
            @Result(property = "roleId",column = "role_id"),
            @Result(property = "roleName",column = "role_name")
    })
    Role selectById (Long id);
}
像这样通过@Select等注解标记SQL,@Result对应<result> 更多用法参考这里 本文不做过多讲解。
3.Annotation的扫描
前面我们聊bindMapperForNamespace()的时候会调用MapperRegistry.addMapper(),addmapper()主要做了两件事情:
- 构建MapperProxyFactory和Mapper接口映射关系的map
- 处理Mapper接口的注解
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<>(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);
        }
      }
    }
  }
3.1 parse
还是来看parse方法:
public void parse() {
    String resource = type.toString();
    if (!configuration.isResourceLoaded(resource)) {
      loadXmlResource();
      configuration.addLoadedResource(resource);
      assistant.setCurrentNamespace(type.getName());
      parseCache();
      parseCacheRef();
      //获得mapper接口的所有方法不y
      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();
  }
3.2 parseStatement
继续来看parseStatement()
void parseStatement(Method method) {
   Class<?> parameterTypeClass = getParameterType(method);
   LanguageDriver languageDriver = getLanguageDriver(method);
   //判断方法上是否包含那几种注解
   //如果有,就根据注解的内容创建SqlSource对象。创建过程与XML创建过程一样
   SqlSource sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver);
   ...//省略
     String resultMapId = null;
     ResultMap resultMapAnnotation = method.getAnnotation(ResultMap.class);
     if (resultMapAnnotation != null) {
       resultMapId = String.join(",", resultMapAnnotation.value());
     } 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);
   }
 }
实际上parseStatement()显示创建sqlSource然后通过addMappedStatement创建MappedStatement,这里和xml的构造大同小异了。之前我们讲xml的时候也提到了了关注下sqlSource,因为实际上它就是要执行的sql。所以对于注解解析的玄关一定存在getSqlSourceFromAnnotations中。
4.构建SqlSource
4.1 getSqlSourceFromAnnotations
来看下getSqlSourceFromAnnotations:
private SqlSource getSqlSourceFromAnnotations(Method method, Class<?> parameterType, LanguageDriver languageDriver) {
   try {
   //注解就分为两大类,sqlAnnotation和sqlProviderAnnotation
   //循环注解列表,判断Method包含哪一种,就返回哪种类型注解的实例
     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);
       //获取注解中的sql语句主体
       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);
   }
 }
getSqlSourceFromAnnotations一上来就分别用 getSqlAnnotationType和getSqlProviderAnnotationType分别去扫描了方法上的注解,很明显他们是两种类型不同的注解,所以处理的方式不一样。
4.2 sqlAnnotation与sqlProviderAnnotation
我们在MapperAnnotationBuilder这个类下面可以看到:
public class MapperAnnotationBuilder {
 private static final Set<Class<? extends Annotation>> SQL_ANNOTATION_TYPES = new HashSet<>();
 private static final Set<Class<? extends Annotation>> SQL_PROVIDER_ANNOTATION_TYPES = new HashSet<>();
//...
 static {
   SQL_ANNOTATION_TYPES.add(Select.class);
   SQL_ANNOTATION_TYPES.add(Insert.class);
   SQL_ANNOTATION_TYPES.add(Update.class);
   SQL_ANNOTATION_TYPES.add(Delete.class);
   SQL_PROVIDER_ANNOTATION_TYPES.add(SelectProvider.class);
   SQL_PROVIDER_ANNOTATION_TYPES.add(InsertProvider.class);
   SQL_PROVIDER_ANNOTATION_TYPES.add(UpdateProvider.class);
   SQL_PROVIDER_ANNOTATION_TYPES.add(DeleteProvider.class);
 }
- @Select、@Insert、@Update、@Delete属于sqlAnnotation
- @SelectProvider、@InsertProvider、@UpdateProvider、@DeleteProvider属于sqlProviderAnnotation
事实上从名字上我们也很好去分辨。
4.3 创建SqlSource对象
- sqlAnnotation
  buildSqlSourceFromStrings会拼接注解里面的value,然后调用createSqlSource进行对parameter和value进行进一步拼接。这里和xml解析一样。
@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);
      }
    }
  }
- sqlProviderAnnotatio
调用到ProviderSqlSource类的构造器,过程比较简单,就是拿到SqlProvider类上的方法,将方法名、方法参数和参数类型设置一下。
public ProviderSqlSource(Configuration configuration, Object provider, Class<?> mapperType, Method mapperMethod) {
    String providerMethodName;
    try {
        // 初始化Mybatis全局配置
        this.configuration = configuration;
        // SqlSource对象的构建器
        this.sqlSourceParser = new SqlSourceBuilder(configuration);
        // 通过注解获取提供Sql内容的对象类型
        this.providerType = (Class<?>) provider.getClass().getMethod("type").invoke(provider);
        // 通过注解获取提供Sql内容的方法名称
        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.");
                }
                // 配置提供Sql的方法
                this.providerMethod = m;
                // 配置提供Sql方法的入参名称集合
                this.providerMethodArgumentNames = new ParamNameResolver(configuration, m).getNames();
                // 配置提供Sql方法的入参类型集合
                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) {
            // 查找ProviderContext类型的参数
            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.");
            }
            // 构建用于Provider方法的上下文对象
            this.providerContext = new ProviderContext(mapperType, mapperMethod);
            // 配置使用Provider方法对应的上下文对象对应的参数位置索引
            this.providerContextIndex = i;
        }
    }
}
代码虽然比较长,但是没什么难点,小伙伴可以自行debug一下。
总结
- Annotation的扫描发生在bindMapperForNamespace()的时候调用MapperRegistry.addMapper()
- MapperAnnotationBuilder.parse()会对注解解析
- sqlAnnotation(@Selcet等)和- sqlProviderAnnotation(@SelectProvider等)是两类不同的注解他们是分开处理的
- 根据解析结果创建SqlSource对象
- 调用addMappedStatement()完成注册