前言
在Mybatis中,SqlSession是一个重要的接口类,在SqlSession对象中定义了一系列的增删改查方法,对于数据库的操作,最后都会调用SqlSession中的方法,例如selectList、selectOne、update等等。

但是我们在项目中使用Mybatis时,并没有去操作过SqlSession对象,而是通过创建了一个个的Mapper接口类,以及对应的xml文件,就完成了对数据库的操作。
在底层,这其实是框架通过jdk动态代理,为我们提供了每个Mapper接口的代理类,来实现了增强功能。
下面就来解析一下整个流程的原理,例如Mapper接口类以及xml文件的初始化、动态代理的调用等。方便自己的学习,也希望能够帮助到大家,谢谢~
初始化-解析配置文件
<package>标签配置方式
在项目中,一般不会只有一个Mapper接口,一般是在一个包里面,存放着一系列的各种模块的Mapper接口以及xml文件,例如 mapper包 或者 dao包。
如果使用xml的方式进行全局配置的话,一般引入Mapper接口会使用<package>标签,如下
            
            
              xml
              
              
            
          
          <configuration>
    <mappers>
        <package name="com.xxx.mapper"/>
    </mappers>
</configuration>   使用这种方式,就可以扫描到所配置的包下,所有的Mapper接口类。下面来看一下他的原理。
Mybatis中对于配置文件的解析,是通过SqlSessionFactoryBuilder类的build方法来完成的,过程中解析了配置文件,创建了SqlSessionFactory工厂对象。解析过程中,经历了XMLConfigBuilder解析核心配置文件、XMLMapperBuilder解析映射配置文件等等。
在XMLConfigBuilder解析核心配置文件的过程中,自然会对核心配置文件中配置的<mappers>标签进行解析,主要是通过mapperElement()方法来完成的,下面看一下源码:
因为这里主要是讲
<package>配置方式的解析过程,所以<mapper>标签的解析过程就省略了,感兴趣的朋友可以参考这篇文章(Mybatis源码 - 初始化流程 加载解析配置文件)。
            
            
              java
              
              
            
          
            private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        if ("package".equals(child.getName())) {
          String mapperPackage = child.getStringAttribute("name");
          configuration.addMappers(mapperPackage);
        } else {
            //解析其它配置方式,例如<mapper resource=""> <mapper class="">等标签
        }
      }
    }
  }第5行:获取到了标签的name属性的值,其实就是com.xxx.mapper。
第6行:调用了全局配置对象Configuration的addMappers()方法,传入了上面获取到的包路径,完成了解析。
            
            
              java
              
              
            
          
          //Mybatis全局配置类
public class Configuration {
  protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
  public void addMappers(String packageName) {
    mapperRegistry.addMappers(packageName);
  }
}可以看到,Configuration类中,是调用了一个成员变量mapperRegistry的addMappers()方法,将包路径再次进行了传入。我们先来看看,mapperRegistry是什么。
MapperRegistry类结构
从下面的源码中可以看出,在MapperRegistry类中,维护着一个Map集合knownMappers,该集合的key为Class<?>类对象,value为MapperProxyFactory<?>,这里其实是Mapper接口的代理工厂对象,正是通过这个对象,来生产了对应Mapper接口的代理类。
该类中也提供了一系列针对knownMappers的操作方法,例如addMapper、getMapper、hasMapper等等,包括了对knownMappers的增删改查、以及判断操作等。
            
            
              java
              
              
            
          
          public class MapperRegistry {
  private final Configuration config;
  private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
  public MapperRegistry(Configuration config) {
    this.config = config;
  }
  
  //省略方法的具体实现
  public <T> T getMapper(Class<T> type, SqlSession sqlSession) {}
  public <T> boolean hasMapper(Class<T> type) {}
  public <T> void addMapper(Class<T> type) {}
  public Collection<Class<?>> getMappers() {}
  public void addMappers(String packageName, Class<?> superType) {}
  public void addMappers(String packageName) {}
}总结:MapperRegistry的主要作用,就是维护了一个Map集合,Map集合中存放着所有Mapper接口的类对象以及对应的代理工厂对象。
MapperRegistry.addMappers()
addMappers(String packageName)
            
            
              java
              
              
            
          
          public class MapperRegistry {
    public void addMappers(String packageName) {  
        addMappers(packageName, Object.class);  
    }
}通过上面的源码可以看出,调用了MapperRegistry.addMappers()之后,又调用了一个重载方法addMappers(String packageName, Class<?> superType)。该方法的源码如下:
addMappers(String packageName, Class<?> superType)
            
            
              java
              
              
            
          
          public class MapperRegistry {
    private final Configuration config;
    private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
  
    public void addMappers(String packageName, Class<?> superType) {
        ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
        resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
        Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
        for (Class<?> mapperClass : mapperSet) {
            addMapper(mapperClass);
        }
    }
}- 
第6-7行,创建了一个解析工具类 ResolverUtil,调用了它的find方法,传入了packageName参数。这一步其实是解析了传入的包路径下的所有资源,并且将.class文件,添加到了一个set集合中。可以看一下下面的源码:javapublic ResolverUtil<T> find(Test test, String packageName) { //getPackagePath方法逻辑:将.替换为了/ 并返回 //return packageName == null ? null : packageName.replace('.', '/'); String path = getPackagePath(packageName); try { List<String> children = VFS.getInstance().list(path); //遍历查找出来的资源,最后只返回以.class结尾的文件。 for (String child : children) { if (child.endsWith(".class")) { addIfMatching(test, child); } } } catch (IOException ioe) { log.error("Could not read package: " + packageName, ioe); } return this; }
- 
第8-11行,通过 resolverUtil获取到了刚封装好的set集合,进行遍历。将每一个Mapper接口的类对象都调用了另一个重载方法addMapper(Class<T> type)。下面看一下该方法的源码。
addMapper(Class<T> type)
            
            
              java
              
              
            
          
            public <T> void addMapper(Class<T> type) {
    if (type.isInterface()) {
      if (hasMapper(type)) {
        throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
      }
      boolean loadCompleted = false;
      try {
        knownMappers.put(type, new MapperProxyFactory<>(type));
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        parser.parse();
        loadCompleted = true;
      } finally {
        if (!loadCompleted) {
          knownMappers.remove(type);
        }
      }
    }
  }- 可以看出,这个方法实际上就是进行了一系列的判断,然后将Mapper接口的类对象以及对应的代理工厂对象,封装到了Map集合knownMappers中。
- 第9-10行,创建了MapperAnnotationBuilder对象,并且调用了它的parse()方法,这里实际上是对Mapper接口中方法上面标注的注解进行了解析,例如@Select等。- 在这个对象的parse()方法中,也对包路径进行了替换,从.替换成了/,同时拼接了.xml,使用这种方式定位到了xml文件,然后通过XMLMapperBuilder对xml文件进行了解析,保存到了全局配置对象Configuration中。
- 这也解释了为什么xml文件必须和Mapper接口处在同包位置下。
 
- 在这个对象的
总结:整个
MapperRegistry.addMappers()执行完毕之后,就算是完成了初始化过程,将配置的包路径下的所有Mapper接口及其代理工厂对象,封装到了Map集合knownMappers中。
代理对象创建
文章一开始提到,框架通过jdk动态代理,为Mapper接口生成了代理对象,实现了增强逻辑。代理对象其实是通过SqlSession对象的getMapper(Class<T> type)方法生成的。本小节来看一下这一块逻辑。
SqlSession.getMapper()
先来看一下SqlSession接口的getMapper方法源码,这里直接查看默认实现类 DefaultSqlSession 的源码
            
            
              java
              
              
            
          
          public class DefaultSqlSession implements SqlSession {
    private final Configuration configuration;
    @Override  
    public <T> T getMapper(Class<T> type) {  
        return configuration.getMapper(type, this);  
    }
}这里调用了全局配置对象Configuration的getMapper(Class<T> type, SqlSession sqlSession)方法
            
            
              java
              
              
            
          
          public class Configuration {
    protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {  
        return mapperRegistry.getMapper(type, sqlSession);  
    }
}可以看到,这里又用到了MapperRegistry对象,通过前面的分析,我们知道 该对象中封装了每个Mapper接口以及它的代理工厂对象。
那显然这里的逻辑就是通过传入的type(Mapper接口的类对象),来获取到它对应的代理工厂对象,然后通过代理工厂对象,创建出对应Mapper接口的代理对象。
MapperRegistry.getMapper()
            
            
              java
              
              
            
          
          public <T> T getMapper(Class<T> type, SqlSession sqlSession) {  
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);  
    if (mapperProxyFactory == null) {  
        throw new BindingException("Type " + type + " is not known to the MapperRegistry.");  
    }  
    try {  
        return mapperProxyFactory.newInstance(sqlSession);  
    } catch (Exception e) {  
        throw new BindingException("Error getting mapper instance. Cause: " + e, e);  
    }  
}- 第2-5行:从Map集合knownMappers中,根据type,获取到了Mapper接口对应的代理工厂对象。并对其进行了判空,如果不存在该代理工厂对象,抛出异常。
- 第7行:调用了代理工厂对象的newInstance方法,创建了代理对象。
MapperProxyFactory.newInstance()
            
            
              java
              
              
            
          
          public class MapperProxyFactory<T> {
  private final Class<T> mapperInterface;
  private final Map<Method, MapperMethodInvoker> methodCache = new ConcurrentHashMap<>();
  public MapperProxyFactory(Class<T> mapperInterface) {
    this.mapperInterface = mapperInterface;
  }
  @SuppressWarnings("unchecked")
  protected T newInstance(MapperProxy<T> mapperProxy) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }
  public T newInstance(SqlSession sqlSession) {
    final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
  }
}- 可以看出,newInstance方法中,首先创建了一个MapperProxy对象,并将相关对象都进行了传入,然后调用了重载的newInstance(MapperProxy<T> mapperProxy)方法。
- 在重载的newInstance方法中,使用了Proxy.newProxyInstance创建了代理对象。
- MapperProxy类中实现了InvocationHandler接口,所以最后直接将它传入了newProxyInstance方法,来创建代理对象。
代理对象执行
在继续讲解代理对象是如何去执行数据库操作之前,我觉得有必要先来简单介绍一下 jdk动态代理原理,了解这块的大佬可以直接跳过。
jdk动态代理原理
JDK动态代理,是在字节码的层面,不改变原类代码的前提下,去增强这个类的方法,添加一些增强功能。它的实现,必须依赖于接口。也就是说,需要被增强的类,必须继承了某个接口。
例如在Mybatis中,Mapper接口只是一个普通接口,本身并不能去执行和它关联的xml文件中的sql语句,这部分逻辑,其实是写在代理对象中的。
Proxy.newProxyInstance()
Java中使用Proxy类的newProxyInstance方法来创建指定类的代理对象,具体参数有三个: newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
这里对入参做一个说明:
- ClassLoader loader:类加载器,一般传入哪个类加载器都可以,比如当前类的类加载器。
- Class<?>[] interfaces:想要增强的类所实现的接口的类对象数组。比如Target类实现了T接口,那么这里传入的数组里面,有一个T.class。
- InvocationHandler h:这是一个函数式接口,提供一个invoke()方法,具体的增强逻辑就写在这里边。
当想去增强Target类的run()方法时,通过Proxy类的newProxyInstance方法来创建出代理对象,当使用代理对象调用run()方法时,就会调用到InvocationHandler里边的invoke()方法中,这样就可以执行到增强逻辑。
用一张图来梳理一下:

下面带来一个jdk动态代理小案例,方便大家理解。
jdk动态代理案例
现有一个接口T和一个实现类Target,接口中提供了一个无参抽象方法run,实现类中实现了这个接口,并执行了逻辑,这里就做一个简单打印。如下
            
            
              java
              
              
            
          
          //接口
public interface T {  
    void run();  
}
//实现类
public class Target implements T {  
    @Override  
    public void run() {  
        System.out.println("Target.run()方法被执行了...");  
    }  
}如果相对Target类中的run方法进行增强,需要三步:
- 使用Proxy.newProxyInstance()创建代理对象。
- 在传入的InvocationHandler的invoke()方法中,编写增强逻辑。
- 获取到代理类,通过代理类调用目标方法。
            
            
              java
              
              
            
          
          public static void main(String[] param) {
    //目标对象
    Target target = new Target();
    //创建代理对象
    T proxyInstance = (T) Proxy.newProxyInstance(
            ClassLoader.getSystemClassLoader(),
            new Class[]{T.class},
            (proxy, method, args) -> {
                System.out.println("前置增强打印...");
                //通过method.invoke()调用目标方法,需要传入目标对象以及调用参数
                Object result = method.invoke(target, args);
                System.out.println("后置增强打印...");
                //返回调用结果
                return result;
            }
    );
    //通过代理对象调用目标方法
    proxyInstance.run();
}
//打印结果
前置增强打印...
Target.run()方法被执行了...
后置增强打印...执行数据库操作
从上面的解析中,我们可以明白两件事:
- 在初始化流程中,创建了Mapper接口的代理对象。在创建过程中,传入的InvocationHandler,是一个封装好的对象MapperProxy,它实现了InvocationHandler接口。
- jdk动态代理中,通过代理对象调用目标方法,实际是调用的InvocationHandler中的invoke()方法。
依据上面两点,我们可以得出:
当通过Mapper接口的代理对象调用方法时,会执行到
MapperProxy对象的invoke()方法中。
下面看一下源码:
            
            
              java
              
              
            
          
            @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      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);
    }
  }- 第4-6行:对Object自带的方法进行过滤,例如equals、hahscode、toString等,不需要增强。
- 第7行:通过cachedInvoker()方法,创建了一个MapperMethodInvoker接口对象,这里创建的是实现类PlainMethodInvoker的对象,接下来调用了该对象的invoke()方法,将相关参数进行了传入。
看一下源码
            
            
              java
              
              
            
          
            private static class PlainMethodInvoker implements MapperMethodInvoker {
    private final MapperMethod mapperMethod;
    public PlainMethodInvoker(MapperMethod mapperMethod) {
      super();
      this.mapperMethod = mapperMethod;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
      return mapperMethod.execute(sqlSession, args);
    }
  }- 第4-7行,在构造器中可以看出,是将上一步中的method对象封装后进行了传入,并赋值给了mapperMethod。- 源码:new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()
 
- 第10-12行:这里是调用了mapperMethod这个成员变量的execute方法,而mapperMethod这个成员变量的值,是在上一步创建MapperMethodInvoker接口对象时进行了赋值。
下面看一下MapperMethod的源码
            
            
              java
              
              
            
          
          public class MapperMethod {  
  
  private final SqlCommand command;  
  private final MethodSignature method;  
  
  public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {  
    this.command = new SqlCommand(config, mapperInterface, method);  
    this.method = new MethodSignature(config, mapperInterface, method);  
  }
  
  public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) {
      case INSERT: {
        Object param = method.convertArgsToSqlCommandParam(args);
        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:
        if (method.returnsVoid() && method.hasResultHandler()) {
          executeWithResultHandler(sqlSession, args);
          result = null;
        } else if (method.returnsMany()) {
          result = executeForMany(sqlSession, args);
        } else if (method.returnsMap()) {
          result = executeForMap(sqlSession, args);
        } 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;
  }
}这个方法就没什么好说的了。通过判断sql的commandType,决定是去执行增删改查哪种操作,然后调用对应方法,传入SqlSession和参数。
sql的commandType,也是在MapperMethod的构造器中,进行了SqlCommand对象的构建,感兴趣的朋友可以自己翻一下,这里就不贴源码了。
再跟进去会发现,实际还是调用的SqlSession中的方法,后面的逻辑,感兴趣的朋友可以参考这篇文章(Mybatis源码 - SqlSession.selectOne(selectList)执行流程解析 一二级缓存 参数设置 结果集封装),这里就不再赘述。
到这里,整个解析流程就结束了。感谢你的阅读,如果有不对的地方,欢迎在评论区指正!谢谢~