Mybatis如何添加映射接口和映射文件?
Mybatis是一种半ORM框架,需要我们手动编写SQL语句。
在启动时,它会将SQL语句等信息读取到内存中,便于操作数据库时进行参数解析、执行SQL和结果封装。
使用过Mybatis的都知道,它有两种方式编写SQL语句:
- xml映射文件
- 映射接口方法上的注解
在启动Mybatis时,可以通过Configuration的addMappers(basePackage)
方法添加映射接口和映射文件,读取上述两种方式编写的SQL语句等信息。
实际上,该方法对MapperRegistry#addMappers(basePackage, superType)
进行了代理,执行逻辑如下:
- 读取指定包路径下的
.class
文件 - 加载类对象
- 过滤出superType的子类
- 解析所有子类:子类对应的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添加映射接口和映射文件主要做了两件事:
- 缓存SQL语句等基本信息,包括SQL语句、参数映射和结果集映射等。
- 缓存映射接口代理工厂,用于创建映射接口代理对象,便于通过调用对象方法直接操作数据库。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)