Mybatis源码 - Mapper代理查询过程解析 <package>标签解析 JDK动态代理原理 代理对象创建 invoke方法

前言

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

但是我们在项目中使用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行:调用了全局配置对象ConfigurationaddMappers()方法,传入了上面获取到的包路径,完成了解析。

java 复制代码
//Mybatis全局配置类
public class Configuration {
  protected final MapperRegistry mapperRegistry = new MapperRegistry(this);

  public void addMappers(String packageName) {
    mapperRegistry.addMappers(packageName);
  }
}

可以看到,Configuration类中,是调用了一个成员变量mapperRegistryaddMappers()方法,将包路径再次进行了传入。我们先来看看,mapperRegistry是什么。

MapperRegistry类结构

从下面的源码中可以看出,在MapperRegistry类中,维护着一个Map集合knownMappers,该集合的key为Class<?>类对象,value为MapperProxyFactory<?>,这里其实是Mapper接口的代理工厂对象,正是通过这个对象,来生产了对应Mapper接口的代理类。

该类中也提供了一系列针对knownMappers的操作方法,例如addMappergetMapperhasMapper等等,包括了对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集合中。可以看一下下面的源码:

    java 复制代码
      public 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);  
    }
}

这里调用了全局配置对象ConfigurationgetMapper(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)

这里对入参做一个说明:

  1. ClassLoader loader:类加载器,一般传入哪个类加载器都可以,比如当前类的类加载器。
  2. Class<?>[] interfaces:想要增强的类所实现的接口的类对象数组。比如Target类实现了T接口,那么这里传入的数组里面,有一个T.class
  3. 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方法进行增强,需要三步:

  1. 使用Proxy.newProxyInstance()创建代理对象。
  2. 在传入的InvocationHandlerinvoke()方法中,编写增强逻辑。
  3. 获取到代理类,通过代理类调用目标方法。
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()方法被执行了...
后置增强打印...

执行数据库操作

从上面的解析中,我们可以明白两件事:

  1. 在初始化流程中,创建了Mapper接口的代理对象。在创建过程中,传入的InvocationHandler,是一个封装好的对象MapperProxy,它实现了InvocationHandler接口。
  2. 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)执行流程解析 一二级缓存 参数设置 结果集封装),这里就不再赘述。

到这里,整个解析流程就结束了。感谢你的阅读,如果有不对的地方,欢迎在评论区指正!谢谢~

相关推荐
SomeB1oody1 小时前
【Rust自学】18.1. 能用到模式(匹配)的地方
开发语言·后端·rust
LiuYuHani2 小时前
Spring Boot面试题
java·spring boot·后端
萧月霖2 小时前
Scala语言的安全开发
开发语言·后端·golang
电脑玩家粉色男孩2 小时前
八、Spring Boot 日志详解
java·spring boot·后端
ChinaRainbowSea3 小时前
八. Spring Boot2 整合连接 Redis(超详细剖析)
java·数据库·spring boot·redis·后端·nosql
叫我DPT3 小时前
Go 中 defer 的机制
开发语言·后端·golang
我们的五年4 小时前
【Linux网络编程】:守护进程,前台进程,后台进程
linux·服务器·后端·ubuntu
谢大旭4 小时前
ASP.NET Core自定义 MIME 类型配置
后端·c#
简 洁 冬冬4 小时前
002 mapper代理开发方式-xml方式
mybatis
命运之手5 小时前
[ Spring ] Spring Boot Mybatis++ 2025
spring boot·spring·mybatis·mybatis-plus·mybatis++