Mybatis源码解析(八):Mapper代理原理

Mybatis源码系列文章

手写源码(了解源码整体流程及重要组件)

Mybatis源码解析(一):环境搭建

Mybatis源码解析(二):全局配置文件的解析

Mybatis源码解析(三):映射配置文件的解析

Mybatis源码解析(四):sql语句及#{}、${}的解析

Mybatis源码解析(五):SqlSession会话的创建

Mybatis源码解析(六):缓存执行器操作流程

Mybatis源码解析(七):查询数据库主流程

Mybatis源码解析(八):Mapper代理原理

Mybatis源码解析(九):插件机制

Mybatis源码解析(十):一级缓存和二级缓存


目录

  • 前言
  • 一、环境准备
  • 二、引入映射配置文件方式
  • 三、\标签的解析
    • 1、通过包路径获取Mapper接口
    • 2、注解方式mapper接口的解析
    • 3、xml和mapper接口需要同包同名的原因?
  • 四、Mapper接口代理对象的生成
  • 五、代理对象执行接口方法的流程
  • 总结


前言

  • 文章主要围绕着如下几个点,展开源码解析:
    • ;是如何进行解析的?
    • sqlSession.getMapper(UserMapper.class);是如何生成的代理对象?
    • mapperProxy.findById(1);是怎么完成的增删改查操作?

一、环境准备

  • java代码
@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. (1)创建事务对象 (2)创建了执行器对象cachingExecutor (3)创建了DefaultSqlSession对象
  SqlSession sqlSession = sqlSessionFactory.openSession();

  // 4. JDK动态代理生成代理对象
  UserMapper mapperProxy = sqlSession.getMapper(UserMapper.class);

  // 5.代理对象调用方法
  User user = mapperProxy.findUserById(100);

  System.out.println("MyBatis源码环境搭建成功....");

  sqlSession.close();
}
  • 核心配置文件sqlMapConfig.xml

DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    
    <environments default="development">
        <environment id="development">
            
            <transactionManager type="JDBC"/>
            
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql:///mybatis"/>
                <property name="username" value="root"/>
                <property name="password" value="123456789"/>
            dataSource>
        environment>
    environments>

    
    <mappers>
        
        
        
        
        
        
        
        <package name="com.xc.mapper"/>
    mappers>
configuration>
  • 实体映射配置文件UserMapper.xml

DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xc.mapper.UserMapper">
    <select id="findUserById" parameterType="int" resultType="com.xc.pojo.User"  >
        SELECT id,username FROM  user WHERE id = #{id}
    select>
mapper>

二、引入映射配置文件方式

先说个结论,后续源码验证:如果不指定xml,则会在Mapper接口同目录下寻找

  • 方式一: 指定xml,缺点需要每个映射xml都要手动添加
  • 方式二: 没有指定xml,会从同目录下寻找xml,缺点也是需要每个Mapper接口都要手动添加
  • 方式三: 没有指定xml,会从同目录下寻找xml,会遍历此包下所有Mappe接口

三、标签的解析

  • 为什么单独讲这个标签?
    1. 这个标签是多种引入映射文件的最佳选,也是工作中必用的
    2. 创建代理类工厂,为以后通过Mapper接口类生成代理实现类做准备
  • 标签在核心配置文件的标签下
  • 方式一也会创建代理类工厂,不过是在解析xml文件后,方式三是先创建代理类工厂,再解析xml
  • Mybatis源码解析(三):映射配置文件的解析:这篇单独讲了指定配置文件的解析

进入解析标签方法

  • 子标签的解析在Mybatis源码解析(三):映射配置文件的解析里详细讲了,这里讲下子标签的解析
  • 获取包名,调用addMappers方法
private void mapperElement(XNode parent) throws Exception {
  if (parent != null) {
    // 获取标签的子标签
    for (XNode child : parent.getChildren()) {
      // 子标签
      if ("package".equals(child.getName())) {
        // 获取mapper接口和mapper映射文件对应的package包名
        String mapperPackage = child.getStringAttribute("name");
        // 将包下所有的mapper接口以及它的代理工厂对象存储到一个Map集合中,key为mapper接口类型,value为代理对象工厂
        configuration.addMappers(mapperPackage);
      } else {// 子标签
        // 获取子标签的resource属性
        String resource = child.getStringAttribute("resource");
        // 获取子标签的url属性
        String url = child.getStringAttribute("url");
        // 获取子标签的class属性
        String mapperClass = child.getStringAttribute("class");
        // 它们是互斥的
        if (resource != null && url == null && mapperClass == null) {
          ErrorContext.instance().resource(resource);
          try(InputStream inputStream = Resources.getResourceAsStream(resource)) {
            // 专门用来解析mapper映射文件
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
            // 通过XMLMapperBuilder解析mapper映射文件
            mapperParser.parse();
          }
        } else if (resource == null && url != null && mapperClass == null) {
          ErrorContext.instance().resource(url);
          try(InputStream inputStream = Resources.getUrlAsStream(url)){
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
            // 通过XMLMapperBuilder解析mapper映射文件
            mapperParser.parse();
          }
        } else if (resource == null && url == null && mapperClass != null) {
          Class<?> mapperInterface = Resources.classForName(mapperClass);
          // 将指定mapper接口以及它的代理对象存储到一个Map集合中,key为mapper接口类型,value为代理对象工厂
          configuration.addMapper(mapperInterface);
        } else {
          throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
        }
      }
    }
  }
}

进入configuration的addMappers方法

public void addMappers(String packageName) {
  mapperRegistry.addMappers(packageName);
}
  • mapperRegistry对象中核心属性就是knownMappers
    • key:Mapper接口的Class对象
    • value:Mapper接口代理类工厂
public class MapperRegistry {

  private final Configuration config;
  private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
  ...
}

进入mapperRegistry的addMappers方法

public void addMappers(String packageName) {
  addMappers(packageName, Object.class);
}

1、通过包路径获取Mapper接口

  • resolverUtil.find方法:加载包路径下Mapper接口
  • mapperSet:mapper接口Class对象集合
  • addMapper方法:将Mapper接口添加到上面所说的Map集合knownMappers中
public void addMappers(String packageName, Class<?> superType) {
  ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
  // 根据package名称,加载该包下Mapper接口文件(不是映射文件)
  resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
  // 获取加载的Mapper接口
  Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
  for (Class<?> mapperClass : mapperSet) {
    // 将Mapper接口添加到MapperRegistry中
    addMapper(mapperClass);
  }
}

resolverUtil.find方法

  • getPackagePath方法:将包名-com.xc.mapper转换为资源路径-com/xc/mapper(.替换成/)
  • children:获取资源路径下的资源,如下
    Mybatis源码解析(八):Mapper代理原理_第1张图片
  • addIfMatching方法:将Mapper接口的Class对象添加到matches集合中
public ResolverUtil<T> find(Test test, String packageName) {
  String path = getPackagePath(packageName);

  try {
    List<String> children = VFS.getInstance().list(path);
    for (String child : children) {
      if (child.endsWith(".class")) {
        addIfMatching(test, child);
      }
    }
  } catch (IOException ioe) {
    log.error("Could not read package: " + packageName, ioe);
  }

  return this;
}

resolverUtil.getClasses()

private Set<Class<? extends T>> matches = new HashSet<>();
...  
public Set<Class<? extends T>> getClasses() {
  return matches;
}

addMapper方法

  • 循环遍历matches集合,将所有Mapper接口Class对象添加到knownMappers
    • key:Mapper接口的Class对象
    • value:Mapper接口代理类工厂
  • 创建注解解析Builder,调用parse解析方法(xml的解析也包含在内)
public <T> void addMapper(Class<T> type) {
  if (type.isInterface()) {
    // 如果Map集合中已经有该mapper接口的映射,就不需要再存储了
    if (hasMapper(type)) {
      throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
    }
    boolean loadCompleted = false;
    try {
      // 将mapper接口以及它的代理对象存储到一个Map集合中,key为mapper接口类型,value为代理对象工厂
      knownMappers.put(type, new MapperProxyFactory<>(type));
      // 用来解析注解方式的mapper接口
      MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
      // 解析注解方式的mapper接口
      parser.parse();
      loadCompleted = true;
    } finally {
      if (!loadCompleted) {
        knownMappers.remove(type);
      }
    }
  }
}

2、注解方式mapper接口的解析

  • loadXmlResource方法:xml文件的解析
  • parseStatement方法:原理其实和Mybatis源码解析(三):映射配置文件的解析差不多
    • 不同点:xml解析标签内的id一致,就是通过方法名匹配标签id获取MappedStatement
    • command.getType():是