在上篇文章中分析了mybatis解析mapper标签中的resource、url属性的过程,《mybatis源码配置文件解析之五:解析mappers标签(解析XML映射文件)》。通过分析可以知道在解析这两个属性的时候首先解析的是对应的XML映射文件,然后解析XML映射文件中的namespace属性配置的接口,在上篇中说到该解析过程和mapper标签中的class属性的解析过程是一样的,因为class属性配置的即是一个接口的全限类名。

一、概述

在mybatis的核心配置文件中配置mappers标签有以下方式,

<mappers>
<mapper class="cn.com.mybatis.dao.UserMapper"/>
</mappers>

上面这种方式便是mapper标签的class属性配置方式,其解析部分过程如下,

else if (resource == null && url == null && mapperClass != null) {
Class<?> mapperInterface = Resources.classForName(mapperClass);
configuration.addMapper(mapperInterface);
} else {
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}

可以看到主要是调用了configuration.addMapper方法,和上篇文章中解析namespace调用的方法是一致的。看其具体实现

public <T> void addMapper(Class<T> type) {
mapperRegistry.addMapper(type);
}

下方分析mapperRegistry.addMapper方法。

二、详述

mapperRegistry.addMapper方法的定义如下,

 public <T> void addMapper(Class<T> type) {
if (type.isInterface()) {//判断是否为接口
if (hasMapper(type)) {//如果knownMappers中已经存在该type,则抛出异常
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
//1、把type放入knownMappers中,其value为一个MapperProxyFactory对象
knownMappers.put(type, new MapperProxyFactory<T>(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.
//2、对mapper文件及注解进行解析,初始化了sqlAnnotationTypessqlProviderAnnotationTypes两个变量
      //具体的解析过程,1、先解析对应的XML映射文件,2、再解析接口方法中的注解信息
        /**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 parser = new MapperAnnotationBuilder(config, type); parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {//3、如果解析失败,则删除knowMapper中的信息
knownMappers.remove(type);
}
}
}
}

该方法主要分为下面几个步骤。

1、检查是否解析过接口

首先会判断knowMappers中是否已经存在该接口,如果存在则会抛出异常

if (hasMapper(type)) {//如果knownMappers中已经存在该type,则抛出异常
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}

如果不存在则放入knownMappers中,

 //1、把type放入knownMappers中,其value为一个MapperProxyFactory对象
knownMappers.put(type, new MapperProxyFactory<T>(type));

继续解析对应的映射文件及接口方法注解

2、解析接口对应的映射文件及接口方法注解

上面把mapper接口放入了knownMappers中,接着需要解析映射文件及注解,

//2、对mapper文件及注解进行解析,初始化了sqlAnnotationTypessqlProviderAnnotationTypes两个变量
//具体的解析过程,1、先解析对应的XML映射文件,2、再解析接口方法中的注解信息
/**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 parser = new MapperAnnotationBuilder(config, type); parser.parse();
loadCompleted = true;

上面的代码,生成了一个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);
}

给sqlAnnotationTypes和sqlProviderAnnotationTypes进行了赋值。

下面看具体的解析过程,

parser.parse();

MapperAnnotationBuilder的parse方法如下,

public void parse() {
String resource = type.toString();
if (!configuration.isResourceLoaded(resource)) {//判断是否加载过该Mapper接口
//解析和接口同名的xml文件,前提是存在该文件,如果不存在该文件要怎么解析那?答案是解析接口中方法上的注解
/**
* 1、解析和接口同名的xml配置文件,最终要做的是把xml文件中的标签,转化为mapperStatement,
* 并放入mappedStatements中
*
*/
loadXmlResource();
configuration.addLoadedResource(resource);
assistant.setCurrentNamespace(type.getName());
//解析接口上的@CacheNamespace注解
parseCache();
parseCacheRef();
//2、获得接口中的所有方法,并解析方法上的注解
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();
}

首先判断是否加载过该资源,

if (!configuration.isResourceLoaded(resource)) {

}

只有未加载过,才会执行该方法的逻辑,否则该方法执行完毕。

public boolean isResourceLoaded(String resource) {
return loadedResources.contains(resource);
}

从loadResources中进行判断,判断是否解析过该Mapper接口,答案是没有解析过,则会继续解析。

1.1、解析对应的XML文件

首先会解析XML文件,调用下面的方法,

//解析和接口同名的xml文件,前提是存在该文件,如果不存在该文件要怎么解析那?答案是解析接口中方法上的注解
/**
* 1、解析和接口同名的xml配置文件,最终要做的是把xml文件中的标签,转化为mapperStatement,
* 并放入mappedStatements中
*
*/
loadXmlResource();

看loadXmlResource方法

/**
* 解析mapper配置文件
*/
private void loadXmlResource() {
// Spring may not know the real resource name so we check a flag
// to prevent loading again a resource twice
// this flag is set at XMLMapperBuilder#bindMapperForNamespace
if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
//解析对应的XML映射文件,其名称为接口类+"."+xml,即和接口类同名且在同一个包下。
String xmlResource = type.getName().replace('.', '/') + ".xml";
InputStream inputStream = null;
try {
inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
} catch (IOException e) {
// ignore, resource is not required
}
if (inputStream != null) {
XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
//解析xml映射文件
xmlParser.parse();
}
}
}

首先进行了判断,进入if判断,看判断上的注解

// Spring may not know the real resource name so we check a flag
// to prevent loading again a resource twice
// this flag is set at XMLMapperBuilder#bindMapperForNamespace

第一句注解没理解什么意思,第二句的意思是方式两次加载资源,第三句是说明了该标识是在XMLMapperBuilder类中的bindMapperForNamespace中进行的设置,如下

为什么这样设置,后面会总结mapper的加载流程详细说明该问题。

判断之后寻找相应的XML映射文件,映射文件的文件路径如下,

//解析对应的XML映射文件,其名称为接口类+"."+xml,即和接口类同名且在同一个包下。
String xmlResource = type.getName().replace('.', '/') + ".xml";

从上面可以看出Mapper接口文件和XML映射文件在同一个包下,且文件名称相同(扩展名不同)。接着便是解析XML映射文件的逻辑。

if (inputStream != null) {
XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
//解析xml映射文件
xmlParser.parse();
}

该逻辑和《mybatis源码配置文件解析之五:解析mappers标签(解析XML映射文件)》的过程是一样的,调用XMLMapperBuilder的parse方法进行解析,解析的结果为MapperStatement对象。

1.2、解析接口方法上的注解

上面是解析接口对应的XML映射文件,解析完成之后,还要解析接口方法上的注解,因为mybatis的sql配置有两种方式,一种是通过XML映射文件,另一种便是注解(当SQL比较复杂建议使用映射文件的方式),下面看解析注解的过程,

//2、获得接口中的所有方法,并解析方法上的注解
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));
}
}

通过反射的方式获得接口中的所有方法,遍历方法执行parseStatement方法

void parseStatement(Method method) {
Class<?> parameterTypeClass = getParameterType(method);
LanguageDriver languageDriver = getLanguageDriver(method);
//获得方法上的注解,并生成SqlSource
SqlSource sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver);
if (sqlSource != null) {
Options options = method.getAnnotation(Options.class);
//生成mappedStatementId,为接口的权限类名+方法名。从这里可以得出同一个接口或namespace中不允许有同名的方法名或id
final String mappedStatementId = type.getName() + "." + method.getName();
Integer fetchSize = null;
Integer timeout = null;
StatementType statementType = StatementType.PREPARED;
ResultSetType resultSetType = ResultSetType.FORWARD_ONLY;
SqlCommandType sqlCommandType = getSqlCommandType(method);
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
boolean flushCache = !isSelect;
boolean useCache = isSelect; KeyGenerator keyGenerator;
String keyProperty = "id";
String keyColumn = null;
if (SqlCommandType.INSERT.equals(sqlCommandType) || SqlCommandType.UPDATE.equals(sqlCommandType)) {
// first check for SelectKey annotation - that overrides everything else
SelectKey selectKey = method.getAnnotation(SelectKey.class);
if (selectKey != null) {
keyGenerator = handleSelectKeyAnnotation(selectKey, mappedStatementId, getParameterType(method), languageDriver);
keyProperty = selectKey.keyProperty();
} else if (options == null) {
keyGenerator = configuration.isUseGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
} else {
keyGenerator = options.useGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
keyProperty = options.keyProperty();
keyColumn = options.keyColumn();
}
} else {
keyGenerator = NoKeyGenerator.INSTANCE;
} if (options != null) {
if (FlushCachePolicy.TRUE.equals(options.flushCache())) {
flushCache = true;
} else if (FlushCachePolicy.FALSE.equals(options.flushCache())) {
flushCache = false;
}
useCache = options.useCache();
fetchSize = options.fetchSize() > -1 || options.fetchSize() == Integer.MIN_VALUE ? options.fetchSize() : null; //issue #348
timeout = options.timeout() > -1 ? options.timeout() : null;
statementType = options.statementType();
resultSetType = options.resultSetType();
} String resultMapId = null;
ResultMap resultMapAnnotation = method.getAnnotation(ResultMap.class);
if (resultMapAnnotation != null) {
String[] resultMaps = resultMapAnnotation.value();
StringBuilder sb = new StringBuilder();
for (String resultMap : resultMaps) {
if (sb.length() > 0) {
sb.append(",");
}
sb.append(resultMap);
}
resultMapId = sb.toString();
} 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);
}
}

注解的解析和解析XML映射文件的方式是一样的,解析的属性是一致的。需要注意下面的注解

@SelectProvider(type=BaseUserProvider.class,method="getUser")

该注解的意思是定义select语句的提供者,需要配置type和method,即提供类的Class对象和相应的方法(返回一个字符串)

3、解析失败回退

如果在继续过程中失败或抛出异常,则进行回退,回退的意思是从knownMappers中删除该类型。

finally {
if (!loadCompleted) {//3、如果解析失败,则删除knowMapper中的信息
knownMappers.remove(type);
}
}

因为Mapper解析的过程有两个结果一个是放入到configuration.knownMappers中的MapperProxyFactory对象,一个是放入到configuration.mappedStatements中MappedStatement对象,由于生产MappedStatement对象失败,所以要回退生成MapperProxyFactory对象过程。

三、总结

本文分析了mybatis解析<mapper class=""/>的过程,依旧是包含MapperProxyFactory和MappedStatement两个过程。

有不当之处,欢迎指正,感谢!

mybatis源码配置文件解析之五:解析mappers标签(解析class属性)的更多相关文章

  1. mybatis源码配置文件解析之五:解析mappers标签(解析XML映射文件)

    在上篇文章中分析了mybatis解析<mappers>标签,<mybatis源码配置文件解析之五:解析mappers标签>重点分析了如何解析<mappers>标签中 ...

  2. mybatis源码配置文件解析之五:解析mappers标签

    在上篇博客中分析了plugins标签,<mybatis源码配置文件解析之四:解析plugins标签>,了解了其使用方式及背后的原理.现在来分析<mappers>标签. 一.概述 ...

  3. mybatis源码配置文件解析之二:解析settings标签

    在前边的博客中分析了mybatis解析properties标签,<mybatis源码配置文件解析之一:解析properties标签>.下面来看解析settings标签的过程. 一.概述 在 ...

  4. mybatis源码配置文件解析之三:解析typeAliases标签

    在前边的博客在分析了mybatis解析settings标签,<mybatis源码配置文件解析之二:解析settings标签>.下面来看解析typeAliases标签的过程. 一.概述 在m ...

  5. mybatis源码配置文件解析之四:解析plugins标签

    在前边的博客在分析了mybatis解析typeAliases标签,<mybatis源码配置文件解析之三:解析typeAliases标签>.下面来看解析plugins标签的过程. 一.概述 ...

  6. mybatis源码配置文件解析之五:解析mappers标签流程图

    前面几篇博客分析了mybatis解析mappers标签的过程,主要分为解析package和mapper子标签.补充一张解析的总体过程流程图,画的不好,多多谅解,感谢.

  7. mybatis源码配置文件解析之一:解析properties标签

    mybatis作为日常开发的常用ORM框架,在开发中起着很重要的作用,了解其源码对日常的开发有很大的帮助.源码版本为:3-3.4.x,可执行到github进行下载. 从这篇文章开始逐一分析mybati ...

  8. MyBatis 源码分析 - 映射文件解析过程

    1.简介 在上一篇文章中,我详细分析了 MyBatis 配置文件的解析过程.由于上一篇文章的篇幅比较大,加之映射文件解析过程也比较复杂的原因.所以我将映射文件解析过程的分析内容从上一篇文章中抽取出来, ...

  9. MyBatis 源码分析 - 配置文件解析过程

    * 本文速览 由于本篇文章篇幅比较大,所以这里拿出一节对本文进行快速概括.本篇文章对 MyBatis 配置文件中常用配置的解析过程进行了较为详细的介绍和分析,包括但不限于settings,typeAl ...

  10. mybatis源码-解析配置文件(四)之配置文件Mapper解析

    在 mybatis源码-解析配置文件(三)之配置文件Configuration解析 中, 讲解了 Configuration 是如何解析的. 其中, mappers作为configuration节点的 ...

随机推荐

  1. Android 13 - Media框架 - 异步消息机制

    关注公众号免费阅读全文,进入音视频开发技术分享群! b7693967-317e-4c46-96d3-d40d9d87e382 由于网上已经有许多优秀的博文讲解了Android的异步消息机制(ALoop ...

  2. linux服务器下安装cbc和ipopt求解器【踩坑总结】

    安装CBC求解器 CBC求解器是一个C++库,我们可以通过以下命令在Linux系统中进行安装: sudo apt-get install coinor-cbc 对于Windows操作系统,可以从CBC ...

  3. nginx知识点汇总

  4. Swift全局变量的线程安全分析

    一.示例代码 import UIKit let obj = TestObj() class TestObj { init() { print("\(type(of: self)) init& ...

  5. 【论文笔记】R-CNN系列之代码实现

    代码源码 前情回顾:[论文笔记]R-CNN系列之论文理解 整体架构 由三部分组成 (1)提取特征的卷积网络extractor (2)输入特征获得建议框rois的rpn网络 (3)传入rois和特征图, ...

  6. windows 命令行调整分辨率

    windows 命令行调整分辨率 下载:qres_v1.1 https://abcker.lanzouq.com/i1uzA1a5uo8j 解压出来,如:D:\Soft\QRes,不要使用中文名目录. ...

  7. (六)基于Scrapy爬取网易新闻中的新闻数据

    需求:爬取这国内.国际.军事.航空.无人机模块下的新闻信息 1.找到这五个板块对应的url  2.进入每个模块请求新闻信息 我们可以明显发现''加载中'',因此我们判断新闻数据是动态加载出来的. 3. ...

  8. 338 warnings potentially fixable with the `--fix` option.

    将lint的值设置为eslint --fix --ext .js,.vue src,重启就可以了或者直接把lint这行删除掉

  9. uniapp ios 白屏

    这个问题找了两天,问题出现的很离谱代码编译没问题,安卓.ios真机运行也没问题,打包以后安卓没问题,iphone 12(ios16.6.1)安装也没问题,还有一个iphone 6s(ios14.7.1 ...

  10. js-文件读写和上传下载的简单例子01

    现下,网络越来越快,浏览器的功能和性能越来越好,所以很多时候,已经不需要一些复杂的框架来实现不是非常复杂的功能. 我们只有在以下情况才会考虑使用框架或者现成的第三方组件: 1.功能复杂,自己写没有必要 ...