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)执行流程解析 一二级缓存 参数设置 结果集封装),这里就不再赘述。

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

相关推荐
customer0817 分钟前
【开源免费】基于SpringBoot+Vue.JS周边产品销售网站(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·java-ee·开源
Yaml41 小时前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理
小码编匠2 小时前
一款 C# 编写的神经网络计算图框架
后端·神经网络·c#
AskHarries2 小时前
Java字节码增强库ByteBuddy
java·后端
佳佳_3 小时前
Spring Boot 应用启动时打印配置类信息
spring boot·后端
许野平4 小时前
Rust: 利用 chrono 库实现日期和字符串互相转换
开发语言·后端·rust·字符串·转换·日期·chrono
BiteCode_咬一口代码5 小时前
信息泄露!默认密码的危害,记一次网络安全研究
后端
齐 飞6 小时前
MongoDB笔记01-概念与安装
前端·数据库·笔记·后端·mongodb