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

Mybatis源码系列文章

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

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

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

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

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

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

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

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

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

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

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



前言

  • 文章主要围绕着如下几个点,展开源码解析:
    • <package name=“com.xxx.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
<?xml version="1.0" encoding="UTF-8" ?>
<!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">
            <!-- 使用jdbc事务管理 -->
            <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>
        <!--使用相对路径注册映射文件-->
        <!--    <mapper resource="mapper/UserMapper.xml"/>-->
        <!--使用绝对路径注册映射文件-->
        <!-- <mapper url="file:///D:\javaCode\mybatis-3.5.7\src\test\resources\mapper\UserMapper.xml"/>-->
        <!--注册持久层接口-->
        <!-- <mapper class="com.xc.mapper.UserMapper"/>-->
        <!--注册一个包下的所有持久层接口-->
        <package name="com.xc.mapper"/>
    </mappers>
</configuration>
  • 实体映射配置文件UserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!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接口同目录下寻找

  • 方式一:<mapper resource=“mapper/UserMapper.xml”/> 指定xml,缺点需要每个映射xml都要手动添加
  • 方式二:<mapper class=“com.xc.mapper.UserMapper”/> 没有指定xml,会从同目录下寻找xml,缺点也是需要每个Mapper接口都要手动添加
  • 方式三:<package name=“com.xc.mapper”/> 没有指定xml,会从同目录下寻找xml,会遍历此包下所有Mappe接口

三、<package name=“com.xxx.mapper”/>标签的解析

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

进入解析<mappers>标签方法

private void mapperElement(XNode parent) throws Exception {
  if (parent != null) {
    // 获取<mappers>标签的子标签
    for (XNode child : parent.getChildren()) {
      // <package>子标签
      if ("package".equals(child.getName())) {
        // 获取mapper接口和mapper映射文件对应的package包名
        String mapperPackage = child.getStringAttribute("name");
        // 将包下所有的mapper接口以及它的代理工厂对象存储到一个Map集合中,key为mapper接口类型,value为代理对象工厂
        configuration.addMappers(mapperPackage);
      } else {// <mapper>子标签
        // 获取<mapper>子标签的resource属性
        String resource = child.getStringAttribute("resource");
        // 获取<mapper>子标签的url属性
        String url = child.getStringAttribute("url");
        // 获取<mapper>子标签的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:获取资源路径下的资源,如下
    在这里插入图片描述
  • 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解析<select>标签内的属性,注解解析@select注解里的属性
    • 相同点:最终目的都是解析成MappedStatement对象
public void parse() {
  // 获取mapper接口的全路径
  String resource = type.toString();
  // 是否解析过该mapper接口
  if (!configuration.isResourceLoaded(resource)) {
    // 先解析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 {
        // 每个mapper接口中的方法,都解析成MappedStatement对象
        parseStatement(method);
      } catch (IncompleteElementException e) {
        configuration.addIncompleteMethod(new MethodResolver(this, method));
      }
    }
  }
  parsePendingMethods();
}

3、xml和mapper接口需要同包同名的原因?

进入上步骤的xml解析方法loadXmlResource方法

  • type:Mapper接口的Class对象
  • 通过Mapper接口名字(com.xc.UserMapper),.替换/转换成资源路径,再添加后缀.xml获取mapper对应的xml
  • 通过资源路径加载为输入流
  • 然后创建xml解析Builder对象,再调用解析方法,就是Mybatis源码解析(三):映射配置文件的解析的内容了
private void loadXmlResource() {
  if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
    String xmlResource = type.getName().replace('.', '/') + ".xml";
    // #1347
    InputStream inputStream = type.getResourceAsStream("/" + xmlResource);
    if (inputStream == null) {
      // Search XML mapper that is not in the module but in the classpath.
      try {
        inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
      } catch (IOException e2) {
        // ignore, resource is not required
      }
    }
    if (inputStream != null) {
      XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
      xmlParser.parse();
    }
  }
}

四、Mapper接口代理对象的生成

sqlSession.getMapper(UserMapper.class)通过Mapper接口Class对象生成代理对象

  • 其实就是通过Mapper接口Class对象,获取上面说的接口代理类工厂
  • 代理类工厂调用.newInstance创建接口代理类
@Override
public <T> T getMapper(Class<T> type) {
  // 从Configuration对象中,根据Mapper接口,获取Mapper代理对象
  return configuration.getMapper(type, this);
}
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
  return mapperRegistry.getMapper(type, sqlSession);
}
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
  // 根据Mapper接口的类型,从Map集合中获取Mapper代理对象工厂
  final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
  if (mapperProxyFactory == null) {
    throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
  }
  try {
    // 通过MapperProxyFactory生产MapperProxy,通过MapperProxy产生Mapper代理对象
    return mapperProxyFactory.newInstance(sqlSession);
  } catch (Exception e) {
    throw new BindingException("Error getting mapper instance. Cause: " + e, e);
  }
}

进入newInstance方法

  • Proxy.newProxyInstance:jdk动态代理,Mapper接口的代理类,从这里创建
  • 代理方法第三个参数是InvocationHandler的实现类,invoke方法就是代理类实现接口类方法的内容
public T newInstance(SqlSession sqlSession) {
  // InvocationHandler接口的实现类
  final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
  return newInstance(mapperProxy);
}

protected T newInstance(MapperProxy<T> mapperProxy) {
  // 使用JDK动态代理方式,生成代理对象
  return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}

五、代理对象执行接口方法的流程

根据jdk动态代理可知,调用接口方法则会进入invoke方法,里面会有接口方法的实现内容

  • 如果是Object定义方法,则MapperProxy类直接调用方法
  • mapperProxy.findUserById(100):代理类调用接口方法,debug则会进入invoke方法
@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 {
      // 代理逻辑在这
      return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
    }
  } catch (Throwable t) {
    throw ExceptionUtil.unwrapThrowable(t);
  }
}

进入cachedInvoker(method).invoke方法(代理逻辑)

  • command.getType():增删改查类型标记
  • command.getName():statementId(namespace:id)
  • sqlSession.selectOne(command.getName(), param):Mybatis源码解析(六):查询数据库主流程
  • sqlSession.insert、sqlSession.update、sqlSession.delete调用方法相同,都是executor.update,所有xml中<insert><update><delete>三个标签的作用一样,只是为了看上去区分一下
@Override
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
  return mapperMethod.execute(sqlSession, args);
}
public Object execute(SqlSession sqlSession, Object[] args) {
  Object result;
  // 判断mapper中的方法类型
  switch (command.getType()) {
    // 添加
    case INSERT: {
      // 转换参数
      Object param = method.convertArgsToSqlCommandParam(args);
      // 最终调用的还是sqlSession中的方法
      result = rowCountResult(sqlSession.insert(command.getName(), param));
      break;
    }
    // 更新
    case UPDATE: {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.update(command.getName(), param));
      break;
    }
    // 删除
    case DELETE: {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.delete(command.getName(), param));
      break;
    }
    // 查询
    case SELECT:
       // 无返回结果,并且有ResultHandler方法参数,将查询结果交给ResultHandler进行处理
      if (method.returnsVoid() && method.hasResultHandler()) {
        executeWithResultHandler(sqlSession, args);
        result = null;
        // 执行查询、返回列表
      } else if (method.returnsMany()) {
        result = executeForMany(sqlSession, args);
        // 执行查询、返回Map
      } else if (method.returnsMap()) {
        result = executeForMap(sqlSession, args);
        // 执行查询、返回Cursor
      } else if (method.returnsCursor()) {
        result = executeForCursor(sqlSession, args);
      } else {
        // 转换参数
        Object param = method.convertArgsToSqlCommandParam(args);
        // 查询单条
        result = sqlSession.selectOne(command.getName(), param);
        if (method.returnsOptional()
            && (result == null || !method.getReturnType().equals(result.getClass()))) {
          result = Optional.ofNullable(result);
        }
      }
      break;
    case FLUSH:
      result = sqlSession.flushStatements();
      break;
    default:
      throw new BindingException("Unknown execution method for: " + command.getName());
  }
  if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
    throw new BindingException("Mapper method '" + command.getName()
        + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
  }
  return result;
}

补充说明下SqlCommand: command对象

  • 通过接口方法名获取对应xml的MappedStatement对象(每一个赠送改查标签对应一个),这里也说明了为啥接口名要与<insert><update><delete><select>标签内的id一致,就是通过方法名匹配标签id获取MappedStatement
  • command.getType():是<insert><update><delete><select>标签解析出增删改查类型
  • command.getName():MappedStatement的id,statementId
public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
  // 当前调用的方法名称
  final String methodName = method.getName();
  // 当前执行的方法对应的Class
  final Class<?> declaringClass = method.getDeclaringClass();
  // 获取对应的MappedStatement
  MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass,
      configuration);
  if (ms == null) {
    if (method.getAnnotation(Flush.class) != null) {
      name = null;
      type = SqlCommandType.FLUSH;
    } else {
      throw new BindingException("Invalid bound statement (not found): "
          + mapperInterface.getName() + "." + methodName);
    }
  } else {
    name = ms.getId();
    type = ms.getSqlCommandType();
    if (type == SqlCommandType.UNKNOWN) {
      throw new BindingException("Unknown execution method for: " + name);
    }
  }
}

总结

  • <package>标签配置的包名下的Mapper接口文件都会被加载成对应的代理类工厂
  • 通过Mapper接口获取同包同名的xml文件,并解析
  • Mapper接口通过jdk代理创建代理类,接口方法匹配xml中标签的id值,执行增删改查
  • 2
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

冬天vs不冷

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值