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 load() {
Map 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 parseClassModel(Resource resource)
throws IOException, NoSuchMethodException {
Map 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 nodes = element.childNodes();
// 便历子节点,解析获取 MethodModel
Map 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 中 <mapper> 下的子标签。即方法标签。
* @return 返回当前XXXDao类的所有方法信息Map,k: 方法名,v:方法模型(即 xml 里一个方法标签的全部信息)
*/
private Map parseMethodModel(ClassModel cm, List nodes)
throws NoSuchMethodException {
Class namespace = cm.getNamespace();
Map methods = new HashMap<>();
List 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 的用法,不再赘述。
/**
* 解析 <mapper> 下的一个子标签,形成方法模型。
*
* @param node <mapper> 子标签
* @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 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 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 parse(Resource resource) throws IOException {
Document doc = Jsoup.parse(resource.getInputStream(), "UTF-8", "http://example.com/");
Map 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 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 处理使用。
__EOF__