Dao接口,就是人们常说的Mapper接口,接口的全限名,就是映射文件中的namespace的值,接口的方法名,就是映射文件中MappedStatement的id值,接口方法内的参数,就是传递给sql的参数。Mapper接口是没有实现类的,当调用接口方法时,接口全限名+方法名拼接字符串作为key值,可唯一定位一个MappedStatement,举例:com.mybatis3.mappers.StudentDao.findStudentById,可以唯一找到namespace为com.mybatis3.mappers.StudentDao下面id = findStudentById的MappedStatement。在Mybatis中,每一个、、、标签,都会被解析为一个MappedStatement对象。
下面来看这个问题: 如何通过全限名+方法名来定位一个mapper的?
初始化首先mybatis会把各种mapper映射进行初始化 对于mybatis与spring结合的项目,最开始是从SqlSessionFactoryBean开始 **SqlSessionFactoryBean类**
```java 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类**
```java /** * 解析mapper映射配置文件 */ public void parse() { //判断是否已经加载过该映射文件 if (!configuration.isResourceLoaded(resource)) { configurationElement(parser.evalNode("/mapper")); configuration.addLoadedResource(resource); //注册 Mapper 接 口 bindMapperForNamespace(); } //处理 configurationElement ()方法中解析失败的节点 parsePendingResultMaps(); //处理 configurationElement ()方法中 解析失败的< cache-ref>节点 parsePendingCacheRefs(); //处理 configurationElement ()方法中 解析失败的 SQL 语句节点 parsePendingStatements(); } ```
**bindMapperForNamespace()方法**
```java //绑定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类**
```java
public void addMapper(Class type) { //.... try { //放入knownMappers中 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); parser.parse(); loadCompleted = true; } finally { if (!loadCompleted) { knownMappers.remove(type); } } } } ```
另外还有一个地方也会初始化,在初始化mybatis config配置文件的时候,可以看**XMLConfigBuilder.java**中mapperElement方法
### 定位
测试用例
```java @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类**
```java @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)方法
```java 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 语句的信息
```java 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 接口的名称与对应的方法名称组成的