Mybatis源码系列文章
手写源码(了解源码整体流程及重要组件)
Mybatis源码解析(一):环境搭建
Mybatis源码解析(二):全局配置文件的解析
Mybatis源码解析(三):映射配置文件的解析
Mybatis源码解析(四):sql语句及#{}、${}的解析
Mybatis源码解析(五):SqlSession会话的创建
Mybatis源码解析(六):缓存执行器操作流程
Mybatis源码解析(六):查询数据库主流程
Mybatis源码解析(七):Mapper代理原理
目录
- 前言
 - 一、环境准备
 - 二、引入映射配置文件方式
 - 三、\<package name="com.xxx.mapper"/>标签的解析
 - 1、通过包路径获取Mapper接口
 - 2、注解方式mapper接口的解析
 - 3、xml和mapper接口需要同包同名的原因?
 
- 四、Mapper接口代理对象的生成
 - 五、代理对象执行接口方法的流程
 - 总结
 
前言
- 文章主要围绕着如下几个点,展开源码解析: 
  
- <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”/>标签的解析
- 为什么单独讲这个标签? 
  
- 这个标签是多种引入映射文件的最佳选,也是工作中必用的
 - 创建代理类工厂,为以后通过Mapper接口类生成代理实现类做准备
 
 - <package>标签在核心配置文件的<mappers>标签下
 - 方式一也会创建代理类工厂,不过是在解析xml文件后,方式三是先创建代理类工厂,再解析xml
 - Mybatis源码解析(三):映射配置文件的解析:这篇单独讲了<mapper resource=“mapper/UserMapper.xml”/>指定配置文件的解析
 
进入解析<mappers>标签方法
- <mapper>子标签的解析在Mybatis源码解析(三):映射配置文件的解析里详细讲了,这里讲下<package>子标签的解析
 - 获取包名,调用addMappers方法
 
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值,执行增删改查
 


















