专注于 JetBrains IDEA 全家桶,永久激活,教程
持续更新 PyCharm,IDEA,WebStorm,PhpStorm,DataGrip,RubyMine,CLion,AppCode 永久激活教程

Mybatis源码分析(三)Annotation的支持

本文源代码来源于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一上来就分别用 getSqlAnnotationTypegetSqlProviderAnnotationType分别去扫描了方法上的注解,很明显他们是两种类型不同的注解,所以处理的方式不一样。

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()完成注册

文章永久链接:https://tech.souyunku.com/36088

未经允许不得转载:搜云库技术团队 » Mybatis源码分析(三)Annotation的支持

JetBrains 全家桶,激活、破解、教程

提供 JetBrains 全家桶激活码、注册码、破解补丁下载及详细激活教程,支持 IntelliJ IDEA、PyCharm、WebStorm 等工具的永久激活。无论是破解教程,还是最新激活码,均可免费获得,帮助开发者解决常见激活问题,确保轻松破解并快速使用 JetBrains 软件。获取免费的破解补丁和激活码,快速解决激活难题,全面覆盖 2024/2025 版本!

联系我们联系我们