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


    Mybatis源码系列文章

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

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

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

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

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

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

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

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

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

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

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



    前言

    • 文章主要围绕着如下几个点,展开源码解析:
      • ;是如何进行解析的?
      • 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();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 核心配置文件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>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 实体映射配置文件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>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

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

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

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

    三、标签的解析

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

    进入解析标签方法

    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.");
            }
          }
        }
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    进入configuration的addMappers方法

    public void addMappers(String packageName) {
      mapperRegistry.addMappers(packageName);
    }
    
    • 1
    • 2
    • 3
    • mapperRegistry对象中核心属性就是knownMappers
      • key:Mapper接口的Class对象
      • value:Mapper接口代理类工厂
    public class MapperRegistry {
    
      private final Configuration config;
      private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
      ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    进入mapperRegistry的addMappers方法

    public void addMappers(String packageName) {
      addMappers(packageName, Object.class);
    }
    
    • 1
    • 2
    • 3

    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);
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    resolverUtil.find方法

    • getPackagePath方法:将包名-com.xc.mapper转换为资源路径-com/xc/mapper(.替换成/)
    • children:获取资源路径下的资源,如下
      在这里插入图片描述
    • 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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    resolverUtil.getClasses()

    private Set<Class<? extends T>> matches = new HashSet<>();
    ...  
    public Set<Class<? extends T>> getClasses() {
      return matches;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    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);
          }
        }
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

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

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