通常一个 Xml 映射文件,都会写一个 Dao 接口与之对应,请问,这个 Dao 接口的工作原理是什么?
答:Dao 接口,就是人们常说的 Mapper 接口,接口的全限名,就是映射文件中的 namespace 的值,接口的方法名,就是映射文件中 MappedStatement 的 id 值,接口方法内的参数,就是传递给 sql 的参数。Mapper 接口是没有实现类的,当调用接口方法时,接口全限名 方法名拼接字符串作为 key 值,可唯一定位一个 MappedStatement,举例:com.mybatis3.mappers.StudentDao.findStudentById,可以唯一找到 namespace 为 com.mybatis3.mappers.StudentDao 下面 id = findStudentById 的 MappedStatement。在 Mybatis 中,每一个 <select>、<insert>、<update>、<delete > 标签,都会被解析为一个 MappedStatement 对象。
下面来看这个问题: 如何通过全限名 方法名来定位一个 mapper 的?
初始化
首先 mybatis 会把各种 mapper 映射进行初始化 对于 mybatis 与 spring 结合的项目,最开始是从 SqlSessionFactoryBean 开始 SqlSessionFactoryBean 类
代码语言:javascript复制 protected SqlSessionFactory buildSqlSessionFactory() throws IOException {
//其他省略...
if (xmlConfigBuilder != null) {
try {
//初始化mybatis config配置文件
xmlConfigBuilder.parse();
LOGGER.debug(() -> "Parsed configuration file: '" this.configLocation "'");
} catch (Exception ex) {
throw new NestedIOException("Failed to parse config resource: " this.configLocation, ex);
} finally {
ErrorContext.instance().reset();
}
}
//其他省略...
try {
XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
targetConfiguration, mapperLocation.toString(), targetConfiguration.getSqlFragments());
//格式化mapper xml文件
xmlMapperBuilder.parse();
} catch (Exception e) {
throw new NestedIOException("Failed to parse mapping resource: '" mapperLocation "'", e);
} finally {
ErrorContext.instance().reset();
}
LOGGER.debug(() -> "Parsed mapper file: '" mapperLocation "'");
//其他省略...
return this.sqlSessionFactoryBuilder.build(targetConfiguration);
}
XMLMapperBuilder 类
代码语言:javascript复制 /**
* 解析mapper映射配置文件
*/
public void parse() {
//判断是否已经加载过该映射文件
if (!configuration.isResourceLoaded(resource)) {
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
//注册 Mapper 接 口
bindMapperForNamespace();
}
//处理 configurationElement ()方法中解析失败的<resultMap>节点
parsePendingResultMaps();
//处理 configurationElement ()方法中 解析失败的< cache-ref>节点
parsePendingCacheRefs();
//处理 configurationElement ()方法中 解析失败的 SQL 语句节点
parsePendingStatements();
}
bindMapperForNamespace () 方法
代码语言:javascript复制//绑定Mapper接口
private void bindMapperForNamespace() {
//获取当前namespace名称
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) {
//如果还没有加载
if (!configuration.hasMapper(boundType)) {
// Spring may not know the real resource name so we set a flag
// to prevent loading again this resource from the mapper interface
// look at MapperAnnotationBuilder#loadXmlResource
configuration.addLoadedResource("namespace:" namespace);
configuration.addMapper(boundType);
}
}
}
}
你从 configuration.addMapper (boundType) 进入,到最后你会发现,会以类全限定名为 key,mapper 代理作为 value 放入 knownMappers 中
MapperRegistry 类
代码语言:javascript复制public <T> void addMapper(Class<T> type) {
//....
try {
//放入knownMappers中
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.
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
另外还有一个地方也会初始化,在初始化 mybatis config 配置文件的时候,可以看 XMLConfigBuilder.java 中 mapperElement 方法
定位
测试用例
代码语言:javascript复制 @Test
public void shouldSelectBlogWithPostsUsingSubSelect() throws Exception {
SqlSession session = sqlSessionFactory.openSession();
try {
//getMapper返回一个MapperProxy对象
BoundBlogMapper mapper = session.getMapper(BoundBlogMapper.class);
Blog b = mapper.selectBlogWithPostsUsingSubSelect(1);
assertEquals(1, b.getId());
session.close();
assertNotNull(b.getAuthor());
assertEquals(101, b.getAuthor().getId());
assertEquals("jim", b.getAuthor().getUsername());
assertEquals("********", b.getAuthor().getPassword());
assertEquals(2, b.getPosts().size());
} finally {
session.close();
}
}
MapperProxy 类实现了 InvocationHandler 接口,代理类调用的时候会执行 invoke 方法 MapperProxy 类
代码语言:javascript复制@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
//如果目标方法继承自Object,则直接调用目标方法
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else if (isDefaultMethod(method)) {
//对jdk7以上版本,动态语言的支持
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
//从缓存中获取 MapperMethod对象,如果缓存中没有,则创建新的 MapperMethod对象并添加到缓存中
final MapperMethod mapperMethod = cachedMapperMethod(method);
//调用 MapperMethod.execute ()方法执行 SQL 语 句
return mapperMethod.execute(sqlSession, args);
}
看 cachedMapperMethod (method) 方法
代码语言:javascript复制 private MapperMethod cachedMapperMethod(Method method) {
MapperMethod mapperMethod = methodCache.get(method);
if (mapperMethod == null) {
创建 MapperMethod 对象 , 并添加到 methodCache 集合 中缓存
mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
methodCache.put(method, mapperMethod);
}
return mapperMethod;
}
MapperMethod 中封装了 Mapper 接口中对应方法的信息,以及对应 SQL 语句的信息
代码语言:javascript复制 public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
//记录了 SQL语句的名称和类型
this.command = new SqlCommand(config, mapperInterface, method);
//Mapper 接 口中对应方法的相关信息
this.method = new MethodSignature(config, mapperInterface, method);
}
看 SqlCommand -->resolveMappedStatement 你会发现,sql 语句的名称是由:Mapper 接口的名称与对应的方法名称组成的 这里也就不贴代码了
以上就是对该面试题的源码分析。
关注我,给你看更多面试分析