MyBatis源码笔记(三) -- mapper解析流程

mapper配置有以下几种配置方式

<!--1.使用类路径-->
<mappers>
  <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
  ...
</mappers>
<!--2.使用绝对url路径-->
<mappers>
  <mapper url="file:///var/mappers/AuthorMapper.xml"/>
  ...
</mappers>
<!--3.使用java类名-->
<mappers>
  <mapper class="org.mybatis.builder.AuthorMapper"/>
  ...
</mappers>
<!--4.自动扫描包下所有映射器-->
<mappers>
  <package name="org.mybatis.builder"/>
</mappers>

解析方法在XMLConfigBuilder类中:

private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        if ("package".equals(child.getName())) {
          //4.自动扫描包下所有映射器
          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) {
            //1.使用类路径
            ErrorContext.instance().resource(resource);
            InputStream inputStream = Resources.getResourceAsStream(resource);
            //调用XMLMapperBuilder
            //注意在for循环里每个mapper都重新new一个XMLMapperBuilder,来解析
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
            mapperParser.parse();
          } else if (resource == null && url != null && mapperClass == null) {
            //2.使用绝对url路径
            ErrorContext.instance().resource(url);
            InputStream inputStream = Resources.getUrlAsStream(url);
            //调用XMLMapperBuilder
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
            mapperParser.parse();
          } else if (resource == null && url == null && mapperClass != null) {
            //3.使用java类名
            Class<?> mapperInterface = Resources.classForName(mapperClass);
            //直接把这个映射加入配置
            configuration.addMapper(mapperInterface);
          } else {
            throw...
          }
        }
      }
    }
}

先分析第1、2种解析方式,构建一个XMLMapperBuilder实例进行解析

XMLMapperBuilder类

public XMLMapperBuilder(Reader reader, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
  this(new XPathParser(reader, true, configuration.getVariables(), new XMLMapperEntityResolver()),
      configuration, resource, sqlFragments);
 }

private XMLMapperBuilder(XPathParser parser, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
  super(configuration);
//解析助手器
  this.builderAssistant = new MapperBuilderAssistant(configuration, resource);
  this.parser = parser;
  this.sqlFragments = sqlFragments;
  this.resource = resource;
 }

//解析XML文件的mapper
public void parse() {
  //如果没有加载过再加载,防止重复加载
  if (!configuration.isResourceLoaded(resource)) {
    //配置mapper
    configurationElement(parser.evalNode("/mapper"));
    //标记一下,已经加载过了
    configuration.addLoadedResource(resource);
    //绑定映射器到namespace
    bindMapperForNamespace();
  }

  //还有没解析完的东东这里接着解析  
  parsePendingResultMaps();
  parsePendingChacheRefs();
  parsePendingStatements();
}

上面逻辑很明显,先判断有没有被解析过,然后就进行XML内容解析,解析完,就对该XML文件进行标记,然后根据namespace进行映射器绑定,最后存在一些没解析完的的内容进行再一次解析(XML解析顺序的原因,可能导致在解析某些引用依赖的时候,找不到该引用依赖)。

private void configurationElement(XNode context) {
    try {
      //1.配置namespace
      String namespace = context.getStringAttribute("namespace");
      if (namespace.equals("")) {
        throw ...;
      }
      builderAssistant.setCurrentNamespace(namespace);
      //2.配置cache-ref,最终加入configuration的cacheRefMap中
      cacheRefElement(context.evalNode("cache-ref"));
      //3.配置cache,最终加入configuration的caches中
      cacheElement(context.evalNode("cache"));
      //4.配置parameterMap(已经废弃,老式风格的参数映射)
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      //5.配置resultMap(高级功能),最终加入configuration的resultMaps
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      //6.配置sql(定义可重用的 SQL 代码段),最终放进configuration的sqlFragments
      sqlElement(context.evalNodes("/mapper/sql"));
      //7.配置select|insert|update|delete,最终放进configuration的mappedStatements
      buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
    }
}

绑定映射器到namespace,实际就是对接口类进行加载,创建代理类

private void bindMapperForNamespace() {
   String namespace = builderAssistant.getCurrentNamespace();
   if (namespace != null) {
     Class<?> boundType = null;
     try {
       boundType = Resources.classForName(namespace);
     } catch (ClassNotFoundException e) {
       //ignore, bound type is not required
     }
     if (boundType != null) {
	//如果mapper接口还没被解析过
       if (!configuration.hasMapper(boundType)) {
         // 标记一下已经加载过当前namespace的XML文件了
         configuration.addLoadedResource("namespace:" + namespace);
	  //这里实际是上面第3种解析mapper方式
         configuration.addMapper(boundType);
       }
     }
   }
}

bindMapperForNamespace方法实际是去加载接口类,即上面的第3种方式

接下来分析第3、4种解析方式,它们的区别在于一个直接提供类名,另一个提供包名进行扫描

Configuration类

//提供包名进行扫描
public void addMappers(String packageName) {
   	mapperRegistry.addMappers(packageName);
 	}
//直接提供类型
public <T> void addMapper(Class<T> type) {
    mapperRegistry.addMapper(type);
}

这里看到bindMapperForNamespace方法那调用的addMapper方法。

MapperRegistry类

//对包名进行扫描
public void addMappers(String packageName) {
 	addMappers(packageName, Object.class);
}
//对包名进行扫描
public void addMappers(String packageName, Class<?> superType) {
  //查找包下所有是superType的类
  ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
  resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
  Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
  for (Class<?> mapperClass : mapperSet) {
    addMapper(mapperClass);
  }
}
//真正缓存类型
public <T> void addMapper(Class<T> type) {
 	//mapper必须是接口!才会添加
  if (type.isInterface()) {
    if (hasMapper(type)) {
      //如果重复添加了,报错
      throw...
    }
    boolean loadCompleted = false;
    try {
//接口代理:把接口传入MapperProxyFactory缓存起来
      knownMappers.put(type, new MapperProxyFactory<T>(type));
      //进行注解、XML解析
      MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
      parser.parse();
      loadCompleted = true;
    } finally {
      //如果加载过程中出现异常需要再将这个mapper从mybatis中删除.
      if (!loadCompleted) {
        knownMappers.remove(type);
      }
    }
  }
 }

第3、4种方式,最终都会调用configuration中的MapperRegistry成员中的addMapper方法,把接口缓存到knownMappers(一个map)里,这时候接口已经被封装到MapperProxyFactory里,为动态代理做好了铺垫。

接下来就是进行对mapper接口类进行解析

MapperAnnotationBuilder类

public MapperAnnotationBuilder(Configuration configuration, Class<?> type) {
    String resource = type.getName().replace('.', '/') + ".java (best guess)";
	//解析助手器
    this.assistant = new MapperBuilderAssistant(configuration, resource);
    this.configuration = configuration;
    this.type = type;

    sqlAnnotationTypes.add(Select.class);
    sqlAnnotationTypes.add(Insert.class);
    sqlAnnotationTypes.add(Update.class);
    sqlAnnotationTypes.add(Delete.class);

    sqlProviderAnnotationTypes.add(SelectProvider.class);
    sqlProviderAnnotationTypes.add(InsertProvider.class);
    sqlProviderAnnotationTypes.add(UpdateProvider.class);
    sqlProviderAnnotationTypes.add(DeleteProvider.class);
 	}

MapperAnnotationBuilder类构造方法中实例化了一个MapperBuilderAssistant(解析助手器),还准备了一些需要解析的注解。

接下来看parse方法如何进行解析

public void parse() {
    String resource = type.toString();
    if (!configuration.isResourceLoaded(resource)) {
      //这里加载mapper的xml文件
      loadXmlResource();
      //已经解析过,记录下来做标记
      configuration.addLoadedResource(resource);
      //助手器设置当前解析的接口
      assistant.setCurrentNamespace(type.getName());
      //解析缓存:CacheNamespace注解
      parseCache();
      //解析CacheNamespaceRef注解
      parseCacheRef();
      //拿到接口方法
      Method[] methods = type.getMethods();
      for (Method method : methods) {
        try {
          // issue #237  解决泛型问题
          if (!method.isBridge()) {
			//对注解sql进行解析,如果不存在注解就不用解析
            parseStatement(method);
          }
        } catch (IncompleteElementException e) {
          configuration.addIncompleteMethod(new MethodResolver(this, method));
        }
      }
    }
    //上面未解析完的方法继续解析
    parsePendingMethods();
 	}
private void loadXmlResource() {
    //判断是否已加载过
    if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
		//类的完整名称+".xml"结尾
      String xmlResource = type.getName().replace('.', '/') + ".xml";
      InputStream inputStream = null;
      try {
        inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
      } catch...
      if (inputStream != null) {
		//这里进行XML文件解析,就是上面的第1种方式
        XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
        xmlParser.parse();
      }
    }
}

可以看到,解析接口类的时候,首先是先加载解析mapper的xml文件,然后解析接口类,无论是xml文件解析还是接口类的注解解析,最终都是把解析的内容封装成MappedStatement对象,存进configuration里,同时因为mybatis做了MappedStatement的唯一性处理,所以注解和xml方式只能选其中一种来写sql语句。

总结流程:

mapper的解析,有两个方向,第一方向是从xml文件进行解析,解析在加载解析接口类;第二个方向是从接口类进行解析,但还是先解析xml文件,然后再解析接口类方法上的注解(如果有的话)。

猜你喜欢

转载自blog.csdn.net/seasonLai/article/details/82853915