《从Java面试题来看源码》,Dao 接口的工作原理

2022-12-02 16:33:08 浏览数 (1)

通常一个 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 接口的名称与对应的方法名称组成的 这里也就不贴代码了

以上就是对该面试题的源码分析。

关注我,给你看更多面试分析 

0 人点赞