Ngbatis 源码阅读之资源加载器 DaoResourceLoader

DaoResourceLoader

Ngbatis
的资源文件加载器,扩展自
MapperResourceLoader
。本篇文章主要分析这两个类。

1. 相关类

  • MapperResourceLoader
  • DaoResourceLoader

2. MapperResourceLoader

在介绍
DaoResourceLoader
之前有必要先介绍一下
MapperResourceLoader

DaoResourceLoader

MapperResourceLoader
的扩展。

MapperResourceLoader
继承了
PathMatchingResourcePatternResolver
类,关于
PathMatchingResourcePatternResolver
的有关内容,可以查看
《Ngbatis源码学习之 Spring 资源管理 ResourceLoader》
这篇文章。

2.1. load

MapperResourceLoader 的作用是加载解析开发人员自定义的 XML 文件资源
,核心是
load()
方法。具体方法如下:

  /**
   * 加载多个开发者自建的 XXXDao.xml 资源。
   *
   * @return 所有 XXXDao 的全限定名 与 当前接口所对应 XXXDao.xml 解析后的全部信息
   */
  @TimeLog(name = "xml-load", explain = "mappers xml load completed : {} ms")
  public Map<String, ClassModel> load() {
    Map<String, ClassModel> resultClassModel = new HashMap<>();
    try {
      // 加载 Resource 资源
      Resource[] resources = getResources(parseConfig.getMapperLocations());
      // 遍历资源并逐一解析
      for (Resource resource : resources) {
        resultClassModel.putAll(parseClassModel(resource));
      }
    } catch (IOException | NoSuchMethodException e) {
      throw new ResourceLoadException(e);
    }
    // 返回解析 xml 后的全部信息
    return resultClassModel;
  }

可以看到在 load() 方法中首先调用 PathMatchingResourcePatternResolver 类的 getResources 方法加载指定文件夹位置下的所有 xml 文件,再对加载的 Resource 资源数组进行遍历,逐一对内容进行解析映射为模型类返回。

重点在 parseClassModel 方法。

2.2. parseClassModel

parseClassModel
方法是解析 xml 文件,将 xml 内容映射到 ClassModel 模型类的具体实现,代码如下:

  /**
   * 解析 单个开发者自定义的 XXXDao.xml 文件
   *
   * @param resource 单个 XXXDao.xml 的资源文件
   * @return 单个 XXXDao 的全限定名 与 当前接口所对应 XXXDao.xml 解析后的全部信息
   * @throws IOException 读取xml时产生的io异常
   */
  public Map<String, ClassModel> parseClassModel(Resource resource)
      throws IOException, NoSuchMethodException {
    Map<String, ClassModel> result = new HashMap<>();
    // 从资源中获取文件信息,使用 Jsoup 进行 IO 读取
    Document doc = Jsoup.parse(resource.getInputStream(), "UTF-8", "http://example.com/");
    // 传入 xml 解析器,获取 xml 信息
    Elements elementsByTag = doc.getElementsByTag(parseConfig.getMapper());

    for (Element element : elementsByTag) {
      ClassModel cm = new ClassModel();
      cm.setResource(resource);
      // 解析标签,获取 namespace 的值
      match(cm, element, "namespace", parseConfig.getNamespace());
      // 解析标签,获取 space 的值
      match(cm, element, "space", parseConfig.getSpace());

      // 如果标签中未设置 space,则从注解获取 space
      if (null == cm.getSpace()) {
        setClassModelBySpaceAnnotation(cm);
      }
      // 将需要初始化的空间名添加到列表并在 sessionPool 中,初始化 session.
      addSpaceToSessionPool(cm.getSpace());

      // 获取子节点(方法配置)
      List<Node> nodes = element.childNodes();
      // 便历子节点,解析获取 MethodModel
      Map<String, MethodModel> methods = parseMethodModel(cm, nodes);
      cm.setMethods(methods);
      // 将结果和加入到映射缓存,key 值为代理类名称。
      result.put(cm.getNamespace().getName() + PROXY_SUFFIX, cm);
    }
    return result;
  }

可以看到这个方法中解析 xml 主要分为以下几个步骤:

  • 使用 Jsoup 的方式加载 Resource 并传入 xml 解析器,从中获取 xml 信息
  • 遍历 Elements,获取到 namespace(全限定类名)和 space(图空间名称)的值,加入 ClassModel 模型类。若 space 的值未在 xml 中设置,则直接从对应 Dap 中设置的实体类中的注解里获取 space。当然,也可能为空。
  • 判断在配置文件中是否开启了 sessionPool 会话池,如果有则加入 space 列表,用于初始化 session。
  • 继续使用 Jsoup 的方法获取 xml 子节点的数据,这边的子节点就是对应的方法配置了。
  • 遍历子节点,在
    parseMethodModel
    方法来中解析 xml,并映射到 MethodModel 模型类中。
  • 将解析好的 ClassModel 加入到 Map 中,key 值为之后要创建的代理类名称。

所以总结下说这个方法就是加载 Resource,解析 xml,并映射为模型类,与代理类名称一一对应并返回供之后使用。

这个方法又涉及到了很多的具体的解析方法,重点查看
match
方法和
parseMethodModel
方法。

2.3. match

match
方法其实就是获取 xml 标签属性的值,与模型类中的属性进行一个匹配并且赋值的过程。具体代码查看如下:

  /**
   * 将 xml 中的标签属性及文本,与模型进行匹配并设值。(模型包含 类模型与方法模型)
   *
   * @param model  ClassModel 实例或 MethodModel 实例
   * @param node   当前 xml 单个 gql 的xml节点
   * @param javaAttr 欲填入 model 的属性名
   * @param attr   node 标签中的属性名
   */
  private void match(Object model, Node node, String javaAttr, String attr) {
    String attrTemp = null;
    try {
      String attrText = node.attr(attr);
      if (isBlank(attrText)) {
        return;
      }
      attrTemp = attrText;
      Field field = model.getClass().getDeclaredField(javaAttr);
      Class<?> type = field.getType();
      Object value = castValue(attrText, type);
      ReflectUtil.setValue(model, field, value);
    } catch (ClassNotFoundException e) {
      throw new ParseException("类型 " + attrTemp + " 未找到");
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

代码其实很简单,传入模型类、node 标签、需要设置的模型类属性名、node 标签需要获取值的属性名这四个参数,获取 node 标签属性值之后,使用反射将属性值赋值给模型类对应的属性里去。

2.4. parseMethodModel

parseMethodModel 方法就是解析 xml 文件中方法标签,并映射到方法模型类中的具体实现了。具体代码如下:

  /**
   * 解析 一个 XXXDao 的多个方法。
   *
   * @param nodes   XXXDao.xml 中 &lt;mapper&gt; 下的子标签。即方法标签。
   * @return 返回当前XXXDao类的所有方法信息Map,k: 方法名,v:方法模型(即 xml 里一个方法标签的全部信息)
   */
  private Map<String, MethodModel> parseMethodModel(ClassModel cm, List<Node> nodes)
      throws NoSuchMethodException {
    Class namespace = cm.getNamespace();
    Map<String, MethodModel> methods = new HashMap<>();
    List<String> methodNames = getMethodNames(nodes);
    for (Node methodNode : nodes) {
      if (methodNode instanceof Element) {
        // nGQL 为自定义查询语句,若存在 nGQL 标签,则执行 parseNgqlModel 方法对标签进行解析
        if (((Element) methodNode).tagName().equalsIgnoreCase("nGQL")) {
          if (Objects.isNull(cm.getNgqls())) {
            cm.setNgqls(new HashMap<>());
          }
          // 解析 nGQL 语句,并映射到对应模型类
          NgqlModel ngqlModel = parseNgqlModel((Element) methodNode);
          cm.getNgqls().put(ngqlModel.getId(),ngqlModel);
        } else {
          // 解析 node 标签内容,并映射为 MethodModel 方法
          MethodModel methodModel = parseMethodModel(methodNode);
          // 将需要初始化的空间名添加到列表并在 sessionPool 中,初始化 session.
          addSpaceToSessionPool(methodModel.getSpace());
          // 根据方法名,利用反射获取唯一的方法
          Method method = getNameUniqueMethod(namespace, methodModel.getId());
          methodModel.setMethod(method);
          Assert.notNull(method,
            "接口 " + namespace.getName() + " 中,未声明 xml 中的出现的方法:" + methodModel.getId());
          // 返回类型检查
          checkReturnType(method, namespace);
          // 对接口进行分页支持
          pageSupport(method, methodModel, methodNames, methods, namespace);
          // 将解析结果加入到 Map 中
          methods.put(methodModel.getId(), methodModel);
        }
      }
    }
    return methods;
  }

可以看到在这个方法中,首先会判断 node 节点元素是否含有
nGQL
标签,如果有则解析 nGQL 语句并映射到
NgqlModel
自定义 nGQL 语句的模型类。解析 nGQL 标签节点的方法很简单,就是获取标签中的文本内容返回:

  protected NgqlModel parseNgqlModel(Element ngqlEl) {
    // 获取元素中的 id 和文本内容
    return  new NgqlModel(ngqlEl.id(),ngqlEl.text());
  }

如果没有 nGQL 标签,则调用 parseMethodModel 方法解析 node 节点元素,并映射为
MethodModel
方法模型。这个方法也很简单,在方法内部同样是调用了 match 来进行解析,前面已经描述过 match 的用法,不再赘述。

  /**
   * 解析 &lt;mapper&gt; 下的一个子标签,形成方法模型。
   * <p/>
   * @param node &lt;mapper&gt;  子标签
   * @return 方法模型
   */
  protected MethodModel parseMethodModel(Node node) {
    MethodModel model = new MethodModel();
    match(model, node, "id", parseConfig.getId());
    match(model, node, "parameterType", parseConfig.getParameterType());
    match(model, node, "resultType", parseConfig.getResultType());
    match(model, node, "space", parseConfig.getSpace());
    match(model, node, "spaceFromParam", parseConfig.getSpaceFromParam());

    List<Node> nodes = node.childNodes();
    model.setText(nodesToString(nodes));
    return model;
  }

映射处理完成之后,会再进行一些后置处理工作,包括返回类型的检查、对方法的分页支持等操作,加入 Map 后返回。

所以将 MapperResourceLoader 类的代码梳理下来能知道,它的作用就是解析 xml 的文件内容,并将其映射为模型类。

3. DaoResourceLoader

在 Ngbatis 内部包含了一个基础操作和内置预定义操作的 xml,会在启动时就被加载解析,作用是为开发人员提供不需要再次编写可直接使用的图库操作。而在 DaoResourceLoader 中就做了这件事情。

DaoResourceLoader 继承了 MapperResourceLoader,所以在了解了 MapperResourceLoader 的作用之后,DaoResourceLoader 类的内容就很好理解了,就是在 MapperResourceLoader 的基础上又扩展了一个加载基类接口所需要的 xml 文件的模板方法。

做法与 MapperResourceLoader 类中的加载方式类似,同样是通过调用 getResource 方法加载指定的 xml,并对 xml 内容进行解析返回。重点方法是
loadTpl()

  /**
   * 加载基类接口所需 nGQL 模板
   *
   * @return 基类接口方法名 与 nGQL 模板的 Map
   */
  public Map<String, String> loadTpl() {
    try {
      Resource resource = getResource(parseConfig.getMapperTplLocation());
      return parse(resource);
    } catch (IOException e) {
      throw new ResourceLoadException(e);
    }
  }

  /**
   * 资源文件解析方法。用于获取 基类方法与nGQL模板
   *
   * @param resource 资源文件
   * @return 基类接口方法名 与 nGQL 模板的 Map
   * @throws IOException 可能找不到 xml 文件的 io 异常
   */
  private Map<String, String> parse(Resource resource) throws IOException {
    Document doc = Jsoup.parse(resource.getInputStream(), "UTF-8", "http://example.com/");
    Map<String, String> result = new HashMap<>();
    // 获取基类 NebulaDaoBasic 的所有方法
    Method[] methods = NebulaDaoBasic.class.getMethods();
    // 遍历方法,并与 xml 文件中的方法名一一对应,解析返回
    for (Method method : methods) {
      String name = method.getName();
      Element elementById = doc.getElementById(name);
      if (elementById != null) {
        List<TextNode> textNodes = elementById.textNodes();
        // 获取 xml 文件中的文本内容
        String tpl = nodesToString(textNodes);
        // key 为方法名,value 为 xml 文件中标签内的文本内容
        result.put(name, tpl);
      }
    }
    return result;
  }
}

可以看到在 loadTpl 中,获取了
NebulaDaoBasic
基类的所有方法,并通过方法名找到 xml 与之对应的 node 标签,获取到文本内容并加入到 Map 返回。

4. 总结

总结一下,DaoResourceLoader 就是加载解析 xml 文件的资源加载器,包括加载解析自定义的 xml 文件和 NebulaDaoBasic 基类所需的基础 xml,将 xml 文件映射为模型类供之后的 Bean 处理使用。

标签: none

添加新评论