Loading

Mybatis如何添加映射接口和映射文件?

Mybatis是一种半ORM框架,需要我们手动编写SQL语句。

在启动时,它会将SQL语句等信息读取到内存中,便于操作数据库时进行参数解析、执行SQL和结果封装。

使用过Mybatis的都知道,它有两种方式编写SQL语句:

  1. xml映射文件
  2. 映射接口方法上的注解

在启动Mybatis时,可以通过Configuration的addMappers(basePackage)方法添加映射接口和映射文件,读取上述两种方式编写的SQL语句等信息。

实际上,该方法对MapperRegistry#addMappers(basePackage, superType)进行了代理,执行逻辑如下:

  1. 读取指定包路径下的.class文件
  2. 加载类对象
  3. 过滤出superType的子类
  4. 解析所有子类:子类对应的xml文件和子类方法上的注解

MapperRegistry#addMappers(basePackage, superType)源码如下,包括上述所有执行逻辑:

public void addMappers(String packageName, Class<?> superType) {
  ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
  // 读取指定包路径下的.class文件,过滤出superType的子类
  resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
  Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
  // 解析所有子类
  for (Class<?> mapperClass : mapperSet) {
    addMapper(mapperClass);
  }
}

具体解析逻辑位于MapperRegistry#addMapper,它会将解析完的映射接口添加到knownMappers中,值是MapperProxyFactory,可以用来创建映射接口的代理对象(需要注意的是,SQL语句等信息并不保存在这里):

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);
      // 解析xml和注解
      parser.parse();
      loadCompleted = true;
    } finally {
      if (!loadCompleted) {
        knownMappers.remove(type);
      }
    }
  }
}

MapperAnnotationBuilder#parse会解析该映射接口对应的xml映射文件,以及映射接口方法上的注解:

public void parse() {
  String resource = type.toString();
  if (!configuration.isResourceLoaded(resource)) {
    // 解析xml映射文件
    loadXmlResource();
    configuration.addLoadedResource(resource);

    // 解析注解
    assistant.setCurrentNamespace(type.getName());
    parseCache();
    parseCacheRef();
    for (Method method : type.getMethods()) {
      if (!canHaveStatement(method)) {
        continue;
      }
      if (getAnnotationWrapper(method, false, Select.class, SelectProvider.class).isPresent()
          && method.getAnnotation(ResultMap.class) == null) {
        parseResultMap(method);
      }
      try {
        parseStatement(method);
      } catch (IncompleteElementException e) {
        configuration.addIncompleteMethod(new MethodResolver(this, method));
      }
    }
  }
  parsePendingMethods();
}

解析xml映射文件时,会读取全限定类名对应的.xml文件,将SQL语句等信息分别添加到Configuration的ResultMap和MappedStatement等成员变量缓存中:

public void parse() {
  if (!configuration.isResourceLoaded(resource)) {
    // 解析XML节点
    configurationElement(parser.evalNode("/mapper"));
    configuration.addLoadedResource(resource);
    bindMapperForNamespace();
  }
  parsePendingResultMaps();
  parsePendingCacheRefs();
  parsePendingStatements();
}

private void configurationElement(XNode context) {
  try {
    // 获取namespace
    String namespace = context.getStringAttribute("namespace");
    if (namespace == null || namespace.isEmpty()) {
      throw new BuilderException("Mapper's namespace cannot be empty");
    }
    builderAssistant.setCurrentNamespace(namespace);
    // 解析cache-ref到Configuration的cacheRefMap
    cacheRefElement(context.evalNode("cache-ref"));
    // 解析cache到Configuration的caches
    cacheElement(context.evalNode("cache"));
    // 解析parameterMap到Configuration的parameterMaps
    parameterMapElement(context.evalNodes("/mapper/parameterMap"));
    // 解析resultMap到Configuration的resultMaps
    resultMapElements(context.evalNodes("/mapper/resultMap"));
    // 解析sql到XMLMapperBuilder的sqlFragments
    sqlElement(context.evalNodes("/mapper/sql"));
    // 解析select|insert|update|delete到Configuration的mappedStatements
    buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
  } catch (Exception e) {
    throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
  }
}

解析映射接口方法上的注解时,会读取每个方法上的注解信息,将SQL语句等信息分别添加到Configuration的ResultMap和MappedStatement等成员变量缓存中:

for (Method method : type.getMethods()) {
  if (!canHaveStatement(method)) {
    continue;
  }
  if (getAnnotationWrapper(method, false, Select.class, SelectProvider.class).isPresent()
      && method.getAnnotation(ResultMap.class) == null) {
    // 解析Arg、Result和TypeDiscriminator等注解到Configuration的resultMaps
    parseResultMap(method);
  }
  try {
    // 解析Select、Update、Insert、Delete、SelectKey和ResultMap等注解到Configuration的mappedStatements
    parseStatement(method);
  } catch (IncompleteElementException e) {
    configuration.addIncompleteMethod(new MethodResolver(this, method));
  }
}

综上我们可以发现,Mybatis添加映射接口和映射文件主要做了两件事:

  1. 缓存SQL语句等基本信息,包括SQL语句、参数映射和结果集映射等。
  2. 缓存映射接口代理工厂,用于创建映射接口代理对象,便于通过调用对象方法直接操作数据库。
posted @ 2023-08-12 18:13  Xianuii  阅读(107)  评论(0编辑  收藏  举报