『手撕 Mybatis 源码』06 - Mapper 代理方式初始化

Mapper 代理方式初始化

  1. 首先修改一下 SqlSession 获取代理对象方式,即通过 getMapper() 来拿到动态代理对象
public class MybatisTest {
  /**
   * 问题1:<package name="com.itheima.mapper"/> 是如何进行解析的?
   * 解答:解析得到 name 属性的值(包名)-->根据包名加载该包下所有 的mapper 接口--->将 mapper 接口及代理工厂对象存到 knownMappers map 集合中
   *      根据 mapper 接口的路径替换. / ---根据替换后路径定位到对应的映射配置文件--->XMLMapperBuilder.parse() 注解方式解析
  **/
  @Test
  public void test2() throws IOException {

    // 1. 通过类加载器对配置文件进行加载,加载成了字节输入流,存到内存中 注意:配置文件并没有被解析
    InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");

    // 2. (1) 解析了配置文件,封装 configuration 对象 (2)创建了 DefaultSqlSessionFactory 工厂对象
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);

    // 3.问题:openSession()执行逻辑是什么?
    // 3. (1) 创建事务对象 (2)创建了执行器对象cachingExecutor (3) 创建了DefaultSqlSession对象
    SqlSession sqlSession = sqlSessionFactory.openSession();

    // 4. JDK动态代理生成代理对象,就是这里
    UserMapper mapperProxy = sqlSession.getMapper(UserMapper.class);
    ...
  }
}
  • 修改 sqlMapConfig.xml 引入配置文件的方式
<configuration>

  ...
  <!--第二部分:引入映射配置文件-->
  <mappers>
    <package name="com.itheima.mapper"/>
  </mappers>
</configuration>
  • UserMapper.xml 放到和 com.itheima.mapper.UserMapper 同一个目录,同时修改一下命名空间,然后就可以学习 MyBatis 的代理方式
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- 即修改这里 -->
<mapper namespace="com.itheima.mapper.UserMapper">

  <cache></cache>
  <select id="findByCondition" resultType="com.itheima.pojo.User" useCache="true">
    SELECT id, name FROM  user WHERE id = #{id}
  </select>
</mapper>
  1. 问题
  • <package name=“com.itheima.mapper”/> 是如何进行解析的?
  1. 首先解析配置文件还是从 SqlSessionFactoryBuilderbuild() 开始,方法内会先创建 XMLConfigBuilder,然后开始解析 sqlMapConfig.xml 配置文件
public class SqlSessionFactoryBuilder {
  
  public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
      // XMLConfigBuilder:用来解析XML配置文件
      // 使用构建者模式(至少 4 个以上成员变量):好处:降低耦合、分离复杂对象的创建
      // 1. 创建 XPathParser 解析器对象,根据 inputStream 解析成了 document 对象 2. 创建全局配置对象 Configuration 对象
      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);

      // 2. parser.parse():使用 XPATH 解析XML配置文件,将配置文件封装到 Configuration 对象
      // 返回 DefaultSqlSessionFactory 对象,该对象拥有 Configuration 对象(封装配置文件信息)
      // 3. parse():配置文件就解析完成了
      return build(parser.parse());

    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
      ErrorContext.instance().reset();
      try {
        inputStream.close();
      } catch (IOException e) {
        // Intentionally ignore. Prefer previous error.
      }
    }
  }
  // 4. 返回对应封装的 DefaultSqlSessionFactory`
  public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
  }
}
  1. parse() 会继续先解析 /configuration 节点,然后从根节点开始找到每个节点进行解析
public class XMLConfigBuilder extends BaseBuilder {

  private boolean parsed;
  private final XPathParser parser;
  ...
  public Configuration parse() {
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;

    // parser.evalNode("/configuration"):通过 XPATH 解析器,解析 configuration 根节点
    // 1. 从 configuration 根节点开始解析,最终将解析出的内容封装到 Configuration 对象中
    parseConfiguration(parser.evalNode("/configuration"));

    return configuration;
  }
}
  1. 这次我们更关注解析 /mappers 这个基点
public class XMLConfigBuilder extends BaseBuilder {
  ...
  private void parseConfiguration(XNode root) {
    try {
      ...
      // 1. 解析 </mappers> 标签 加载映射文件流程主入口
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
  }
}
  1. 获取 /mappers 标签,然后遍历 /mappers 标签的子标签,也就是现在我们配置的 /package 标签,从标签中拿到 name 属性,即 com.itheima.mapper,然后开始将包下所有的 mapper 接口以及它的代理工厂对象存储到 Configuration 的一个 Map 集合中,key 为 mapper 接口类型,value 为代理对象工厂
public abstract class BaseBuilder {
  protected final Configuration configuration;
  ...
  private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      // 1. 获取 <mappers> 标签的子标签
      for (XNode child : parent.getChildren()) {
        // 2. <package> 子标签
        if ("package".equals(child.getName())) {
          // 3. 获取 mapper 接口和 mapper 映射文件对应的 package 包名。即 com.itheima.mapper
          String mapperPackage = child.getStringAttribute("name");
          // 4. 将包下所有的 mapper 接口以及它的代理工厂对象存储到一个 Map 集合中,key 为 mapper 接口类型,value 为代理对象工厂
          configuration.addMappers(mapperPackage);
        } else {// <mapper>子标签
        ...
      }
    }
  }
}
  1. Configuration 对象会交由 MapperRegistry 来添加到 Map 集合
public class Configuration {
  ...
  protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
  ...
  public void addMappers(String packageName) {
    // 1. 交由 MapperRegistry 来添加到 Map 集合。其中 packageName 为 com.itheima.mapper
    mapperRegistry.addMappers(packageName);
  }
}
  1. MapperRegistry 先创建解析工具类 ResolverUtil,根据 packageName 找到该包下所有的 Mapper 接口文件,然后将 Mapper 接口添加到 MapperRegistry
public class MapperRegistry {
  ...
  public void addMappers(String packageName, Class<?> superType) {
    // 其中 packageName 为 com.itheima.mapper,而 superType 为 Object.class
    ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
    // 1. 根据 package 名称,加载该包下 Mapper 接口文件(不是映射文件)
    resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
    // 2. 获取加载的 Mapper 接口,其实就是从 matches 拿到类集合
    Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
    for (Class<?> mapperClass : mapperSet) {
      // 3. 将 Mapper 接口添加到 MapperRegistry 中
      addMapper(mapperClass);
    }
  }
}
  • ResolverUtil 找接口时,先把 packageName 改成 com/itheima/mapper,然后查找所有的 .class 文件,然后加载这个文件存入 matches
public class ResolverUtil<T> {
  
  private Set<Class<? extends T>> matches = new HashSet<>();
  ...
  protected String getPackagePath(String packageName) {
    // 1. 修改报名
    return packageName == null ? null : packageName.replace('.', '/');
  }
 
  public ResolverUtil<T> find(Test test, String packageName) {
    // 2. packageName 为 com.itheima.mapper,改成 com/itheima/mapper
    String path = getPackagePath(packageName);

    try {
      List<String> children = VFS.getInstance().list(path);
      for (String child : children) {
        if (child.endsWith(".class")) {
          // 3. 添加到 matches 属性中
          addIfMatching(test, child);
        }
      }
    } catch (IOException ioe) {
      log.error("Could not read package: " + packageName, ioe);
    }
    return this;
  }
  
  protected void addIfMatching(Test test, String fqn) {
    try {
      // 4. 把 fqn 的 com/itheima/mapper/UserMapper.class 改全类名,即 com.itheima.mapper.UserMapper
      String externalName = fqn.substring(0, fqn.indexOf('.')).replace('/', '.');
      ClassLoader loader = getClassLoader();
      if (log.isDebugEnabled()) {
        log.debug("Checking to see if class " + externalName + " matches criteria [" + test + "]");
      }
      // 5. 加载类
      Class<?> type = loader.loadClass(externalName);
      if (test.matches(type)) {
        // 6. 将类装入 matches 中
        matches.add((Class<T>) type);
      }
    } catch (Throwable t) {
      log.warn("Could not examine class '" + fqn + "'" + " due to a "
          + t.getClass().getName() + " with message: " + t.getMessage());
    }
  }
}
  • 得到类集合之后,先判断对应的类是否已经加入过 knownMappers,如果是,则抛出异常,否则就加入 knownMappers 中,其中 key 为 type,value 为 MapperProxyFactory 代理工厂,如果接口中有使用 @Select 或者配置了 Mapper.xml,就需要使用 MapperAnnotationBuilder 进一步解析
public class MapperRegistry {

  private final Configuration config;
  private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
  ...
  public <T> boolean hasMapper(Class<T> type) {
    return knownMappers.containsKey(type);
  }

  public <T> void addMapper(Class<T> type) {
    if (type.isInterface()) {
      // 1. 如果 Map 集合中已经有该 mapper 接口的映射,就不需要再存储了
      if (hasMapper(type)) {
        throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
      }
      boolean loadCompleted = false;
      try {
        // 2. 将 mapper 接口以及它的代理对象存储到一个 Map 集合中,key 为 mapper 接口类型,value 为代理对象工厂
        knownMappers.put(type, new MapperProxyFactory<>(type));
        // 3. 用来解析注解方式的 mapper 接口
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        // 4. 解析注解方式的 mapper 接口
        parser.parse();
        loadCompleted = true;
      } finally {
        if (!loadCompleted) {
          knownMappers.remove(type);
        }
      }
    }
  }
}
  • 解析 Mapper.xml 时,先获取 mapper 接口的全路径(即 interface com.itheima.mapper.UserMapper)。如果 Configuration 对象从来没加载过,就创建 XMLMapperBuilder 来解析。然后遍历 type 的每个方法,都解析构成 MappedStatement 对象,存入 Configuration 对象中
public class MapperAnnotationBuilder {
  
  private final Configuration configuration;
  private final MapperBuilderAssistant assistant;
  private final Class<?> type;
  ...
  public void parse() {
    // 1. 获取 mapper 接口的全路径,即 interface com.itheima.mapper.UserMapper
    String resource = type.toString();
    // 2. 是否解析过该 mapper 接口
    if (!configuration.isResourceLoaded(resource)) {
      // 3. 先解析 mapper 映射文件
      loadXmlResource();
      // 设置解析标识
      configuration.addLoadedResource(resource);
      assistant.setCurrentNamespace(type.getName());
      // 解析CacheNamespace注解
      parseCache();
      // 解析CacheNamespaceRef注解
      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 {
          // 4. 每个 mapper 接口中的方法,都解析成 MappedStatement 对象
          parseStatement(method);
        } catch (IncompleteElementException e) {
          configuration.addIncompleteMethod(new MethodResolver(this, method));
        }
      }
    }
    parsePendingMethods();
  }
  
  void parseStatement(Method method) {
    // 1. 获取 Mapper 接口的形参类型
    final Class<?> parameterTypeClass = getParameterType(method);
    ...
    // 2. 添加到 Configuration 对象
    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,
          statementAnnotation.getDatabaseId(),
          languageDriver,
          // ResultSets
          options != null ? nullOrEmpty(options.resultSets()) : null);
    });
  }
}
  1. 总结
    在这里插入图片描述
当我们在使用MyBatis进行开发时,有时候会遇到"java mybatis-3-mapper.dtd找不到文件"的报错。这个错误的原因是MyBatis在解析mapper.xml文件时,会通过DTD(Document Type Definition)来验证XML的结构和语法是否正确。而这个DTD文件通常会从MyBatis的jar包中加载。 出现找不到文件的情况可能是由以下几个原因导致的: 1. 项目配置问题:检查项目的配置文件,例如mybatis-config.xml是否正确配置了DTD路径。通常在mybatis-config.xml文件中会有类似以下的配置: <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> 确保该配置正确并指向了DTD文件的正确位置。 2. Maven依赖问题:如果你是通过Maven来管理依赖的话,检查是否正确引入了MyBatis的相关依赖。通常需要引入以下两个依赖: <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.x.x</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>1.x.x</version> </dependency> 请确保版本号正确,并且Maven能够正确下载这些依赖。 3. 服务端网络问题:如果你的项目是部署在远程服务器上的,有可能是服务器无法访问MyBatis的DTD文件所在的URL导致的。可以尝试在本地动访问DTD文件的URL,看是否能够正常下载。 以上是解决"java mybatis-3-mapper.dtd找不到文件"报错的一些常见方法,希望能够帮助到你。如果以上方法不能解决问题,可能需要进一步检查你的项目配置和环境设置。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值