Mybatis Mapper 接口源码解析

本人花费半年的时间总结的《Java面试指南》已拿腾讯等大厂offer,已开源在github ,欢迎star!

本文GitHub https://github.com/OUYANGSIHAI/JavaInterview 已收录,这是我花了6个月总结的一线大厂Java面试总结,本人已拿大厂offer,欢迎star

原文链接:blog.ouyangsihai.cn >> Mybatis Mapper 接口源码解析

本文首发地址为个人博客 https://my.oschina.net/mengyuankan/blog/2873220

相关文章

前言

在使用 Mybatis 的时候,我们只需要写对应的接口,即 dao层的 Mapper接口,不用写实现类, Mybatis 就能根据接口中对应的方法名称找到 xml 文件中配置的对应 SQL,方法的参数和 SQL 的参数一一对应,在 xml 里面的 SQL 中,我们可以通过 #{0} #{1},来绑定参数,也可以通过 #{arg0} #{arg1}来绑定参数,还可以通过方法中真正的参数名称如 name age之类的进行绑定,此外还可通过 #{param1} #{param2}等来绑定,接下来看下 Mybatis的源码是如何实现的。

源码分析

Mybatis 中,解析 Mapper 接口的源码主要是在 binding 包下,该包下就 4 个类,再加上一个方法参数名称解析的工具类 ParamNameResolver ,一共 5 个类,代码量不多,下面就来一次分析这几个类。

Mybatis Mapper 接口源码解析

先来简单看下这几个类:

  • `BindingException` :自定义异常,忽略
  • `MapperMethod` :在该类中封装了 Mapper 接口对应方法的信息,以及对应的 SQL 语句的信息,`MapperMethod` 类可以看做是 Mapper 接口和配置文件中的 SQL 语句之间的连接桥梁,是这几个类中最重要的一个类,代码也较多,其他几个类的代码就很少。
  • `MapperProxy` :它是 Mapper 接口的代理对象,在使用 Mybatis 的时候,不需要我们实现 Mapper 接口,是使用 JDK 的动态代理来实现。
  • `MapperProxyFactory` :MapperProxy类的工厂类,用来创建 MapperProxy
  • `MapperRegistry` :它是 Mybatis 接口及其代理对象工厂的注册中心,在 Mybatis 初始化的时候,会加载配置文件和 Mapper 接口信息注册到该类中来。
  • `ParamNameResolver`  该类不是 binding 包下的类,它是 reflection 包下的一个工具类,主要用来解析接口方法参数的。
  • MapperMethod :在该类中封装了 Mapper 接口对应方法的信息,以及对应的 SQL 语句的信息, MapperMethod 类可以看做是 Mapper 接口和配置文件中的 SQL 语句之间的连接桥梁,是这几个类中最重要的一个类,代码也较多,其他几个类的代码就很少。

    MapperProxyFactory :MapperProxy类的工厂类,用来创建 MapperProxy

    ParamNameResolver  该类不是 binding 包下的类,它是 reflection 包下的一个工具类,主要用来解析接口方法参数的。

    接下来看下每个类的实现过程:

    MapperProxy

    首先来看下 MapperProxy 类,它是 Mapper 接口的代理对象,实现了 InvocationHandler 接口,即使用了 JDK 的动态代理为 Mapper 接口生成代理对象,

    
    public class MapperProxyT implements InvocationHandler, Serializable {
    
      // 关联的 sqlSession 对象
      private final SqlSession sqlSession;
      // 目标接口,即 Mapper 接口对应的 class 对象
      private final Class mapperInterface;
      // 方法缓存,用于缓存 MapperMethod对象,key 为 Mapper 接口中对应方法的 Method 对象,value 则是对应的 MapperMethod,MapperMethod 会完成参数的转换和 SQL 的执行功能
      private final MapMethod, MapperMethod methodCache;
    
      public MapperProxy(SqlSession sqlSession, Class mapperInterface, MapMethod, MapperMethod methodCache) {
        this.sqlSession = sqlSession;
        this.mapperInterface = mapperInterface;
        this.methodCache = methodCache;
      }
    
      // 代理对象执行的方法,代理以后,所有 Mapper 的方法调用时,都会调用这个invoke方法
      @Override
      public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
         // 并不是每个方法都需要调用代理对象进行执行,如果这个方法是Object中通用的方法,则无需执行
         if (Object.class.equals(method.getDeclaringClass())) {
            return method.invoke(this, args);
          // 如果是默认方法,则执行默认方法,Java 8 提供了默认方法
          } else if (isDefaultMethod(method)) {
            return invokeDefaultMethod(proxy, method, args);
          }
         // 从缓存中获取 MapperMethod 对象,如果缓存中没有,则创建一个,并添加到缓存中
         final MapperMethod mapperMethod = cachedMapperMethod(method);
         // 执行方法对应的 SQL 语句
         return mapperMethod.execute(sqlSession, args);
      }
      // 缓存 MapperMethod 
      private MapperMethod cachedMapperMethod(Method method) {
        MapperMethod mapperMethod = methodCache.get(method);
        if (mapperMethod == null) {
          mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
          methodCache.put(method, mapperMethod);
        }
        return mapperMethod;
      }
    }
    

    以上就是 MapperProxy 代理类的主要代码,需要注意的是调用了 MapperMethod 中的相关方法, MapperMethod 在后面进行分析,这里先知道它是完成方法参数的转换和执行方法对应的SQL即可,还有一点,在执行目标方法的时候,如果是 Object 中的方法,则直接执行目标方法,如果是默认方法,则会执行默认方法的相关逻辑,否则在使用代理对象执行目标方法

    MapperProxyFactory

    在看了上述的 MapperProxy 代理类之后, MapperProxyFactory 就是用来创建该代理类的,是一个工厂类。

    
    public class MapperProxyFactoryT {
      // 当前的 MapperProxyFactory 对象可以创建的 mapperInterface 接口的代理对象
      private final Class mapperInterface;
    
      // MapperMetho缓存
      private final MapMethod, MapperMethod methodCache = new ConcurrentHashMapMethod, MapperMethod();
    
      // 创建 mapperInterface 的代理对象
      @SuppressWarnings("unchecked")
      protected T newInstance(MapperProxy mapperProxy) {
        return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
      }
    
      public T newInstance(SqlSession sqlSession) {
        final MapperProxy mapperProxy = new MapperProxy(sqlSession, mapperInterface, methodCache);
        return newInstance(mapperProxy);
      }
    }
    

    一个工厂类,主要用于创建 Mapper 接口的代理对象,代码简单,没什么好说的。

    MapperRegistry

    MapperRegistry 它是 Mapper 接口及其对应代理工厂对象的注册中心,在 Mybatis 初始化的时候,会加载配置文件及Mapper接口信息,注册到 MapperRegistry  类中,在需要执行某 SQL 的时候,会先从注册中心获取 Mapper 接口的代理对象。

    
    public class MapperRegistry {
      // 配置对象,包含所有的配置信息
      private final Configuration config;
    
      // 接口和代理对象工厂的对应关系,会用工厂去创建接口的代理对象
      private final MapClass?, MapperProxyFactory? knownMappers = new HashMapClass?, MapperProxyFactory?();
    
      // 注册 Mapper 接口
      public  void addMapper(Class type) {
        // 是接口,才进行注册
        if (type.isInterface()) {
          // 如果已经注册过了,则抛出异常,不能重复注册
          if (hasMapper(type)) {
            throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
          }
          // 是否加载完成的标记
          boolean loadCompleted = false;
          try {
            // 会为每个 Mapper 接口创建一个代理对象工厂
            knownMappers.put(type, new MapperProxyFactory(type));
    
            // 下面这个先不看,主要是xml的解析和注解的处理
            MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
            parser.parse();
            loadCompleted = true;
          } finally {
            // 如果注册失败,则移除掉该Mapper接口
            if (!loadCompleted) {
              knownMappers.remove(type);
            }
          }
        }
      }
    
      // 获取 Mapper 接口的代理对象,
      public  T getMapper(Class type, SqlSession sqlSession) {
        // 从缓存中获取该 Mapper 接口的代理工厂对象
        final MapperProxyFactory mapperProxyFactory = (MapperProxyFactory) knownMappers.get(type);
         // 如果该 Mapper 接口没有注册过,则抛异常
        if (mapperProxyFactory == null) {
          throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
        }
        //使用代理工厂创建 Mapper 接口的代理对象
        return mapperProxyFactory.newInstance(sqlSession);
      }  
    }
    

    以上就是 MapperRegistry 注册的代码,主要注册 Mapper 接口,和获取 Mapper 接口的代理对象,很好理解。

    ParamNameResolver

    ParamNameResolver  它不是 binding 包下的类,它是 reflection 包下的一个工具类,主要用来解析接口方法参数的。
    也就是方法参数中,该参数是第几个,即解析出参数的名称和索引的对应关系;该类中的代码相比于以上几个类要复杂些,有点绕,我是通过写 main 方法来辅助理解的,不过代码量也不多。

    该类中的主要方法主要是构造方法和 getNamedParams 方法,先来看下其他的方法:

    
    public class ParamNameResolver {
      // 参数前缀,在 SQL 中可以通过 #{param1}之类的来获取
      private static final String GENERIC_NAME_PREFIX = "param";
    
      // 参数的索引和参数名称的对应关系,有序的,最重要的一个属性
      private final SortedMapInteger, String names;
      // 参数中是否有 @Param 注解
      private boolean hasParamAnnotation;
    
      // 获取对应参数索引实际的名称,如 arg0, arg1,arg2......
      private String getActualParamName(Method method, int paramIndex) {
          Object[] params = (Object[]) GET_PARAMS.invoke(method);
          return (String) GET_NAME.invoke(params[paramIndex]);
      }
      // 是否是特殊参数,如果方法参数中有 RowBounds 和 ResultHandler 则会特殊处理,不会存入到 names 集合中
      private static boolean isSpecialParameter(Class? clazz) {
        return RowBounds.class.isAssignableFrom(clazz) || ResultHandler.class.isAssignableFrom(clazz);
      }
    
      // 返回所有的参数名称 (toArray(new String[0])又学习了一种新技能)
      public String[] getNames() {
        return names.values().toArray(new String[0]);
      }
    

    上述代码是 ParamNameResolver  的一些辅助方法,最重要的是 names 属性,它用来存放参数索引于参数名称的对应关系,是一个 map,但是有例外,如果参数中含有 RowBounds ResultHandler 这两种类型,则不会把它们的索引和对应关系放入到 names 集合中,如下:

    
    aMethod(@Param("M") int a, @Param("N") int b) -- names = {{0, "M"}, {1, "N"}}
    aMethod(int a, int b) -- names = {{0, "0"}, {1, "1"}}
    aMethod(int a, RowBounds rb, int b) -- names = {{0, "0"}, {2, "1"}}
    

    构造方法,现在来看下 ParamNameResolver 的构造方法,在调用构造方法创建该类对象的时候会对方法的参数进行解析,解析结果放到 names 数据中去,代码如下:

    重点

    
      public ParamNameResolver(Configuration config, Method method) {
        // 方法所有参数的类型
        final Class?[] paramTypes = method.getParameterTypes();
        // 所有的方法参数,包括 @Param 注解的参数
        final Annotation[][] paramAnnotations = method.getParameterAnnotations();
        // 参数索引和参数名称的对应关系
        final SortedMapInteger, String map = new TreeMapInteger, String();
        int paramCount = paramAnnotations.length;
        // get names from @Param annotations
        for (int paramIndex = 0; paramIndex  paramCount; paramIndex++) {
          // 不处理 RowBounds 和 ResultHandler 这两种特殊的参数
          if (isSpecialParameter(paramTypes[paramIndex])) {
            continue;
          }
          String name = null;
          for (Annotation annotation : paramAnnotations[paramIndex]) {
            // 如果参数被 @Param 修饰
            if (annotation instanceof Param) {
              hasParamAnnotation = true;
              // 则参数名称取其值
              name = ((Param) annotation).value();
              break;
            }
          }
          // 如果是一般的参数
          if (name == null) {
            // 是否使用真实的参数名称,true 使用,false 跳过
            if (config.isUseActualParamName()) {
              // 如果为 true ,则name = arg0, arg1 之类的
              name = getActualParamName(method, paramIndex);
            }
            // 如果上述为false,
            if (name == null) {
              // name为参数索引,0,1,2 之类的
              name = String.valueOf(map.size());
            }
          }
          // 存入参数索引和参数名称的对应关系
          map.put(paramIndex, name);
        }
        // 赋值给 names 属性
        names = Collections.unmodifiableSortedMap(map);
      }
    

    看了上述的构造以后,main 方法测试一下,有如下方法:

    
    Person queryPerson(@Param("age") int age, String name, String address, @Param("money") double money);
    

    如上方法,如果使用真实名称,即 config.isUseActualParamName() 为 true 的时候,解析之后, names属性的为打印出来如下:

    
    {0=age, 1=arg1, 2=money, 3=arg3}
    

    如果 config.isUseActualParamName() 为 false的时候,解析之后, names 属性的为打印出来如下:

    
    {0=age, 1=1, 2=money, 3=3}
    

    现在解析了方法的参数后,如果传入方法的参数值进来,怎么获取呢?就是该类中的   getNamedParams 方法:

    
      public Object getNamedParams(Object[] args) {
        // 参数的个数
        final int paramCount = names.size();
        if (args == null || paramCount == 0) {
          return null;
         // 如果参数没有被 @Param 修饰,且只有一个,则直接返回
        } else if (!hasParamAnnotation && paramCount == 1) {
          return args[names.firstKey()];
        } else {
          // 参数名称和参数值的对应关系
          final MapString, Object param = new ParamMapObject();
          int i = 0;
          for (Map.EntryInteger, String entry : names.entrySet()) {
            // key = 参数名称,value = 参数值
            param.put(entry.getValue(), args[entry.getKey()]);
    
            final String genericParamName = "param"+ String.valueOf(i + 1);
            // 默认情况下它们将会以它们在参数列表中的位置来命名,比如:#{param1},#{param2}等
            if (!names.containsValue(genericParamName)) {
              param.put(genericParamName, args[entry.getKey()]);
            }
            i++;
          }
         // 返回参数名称和参数值的对应关系,是一个 map 
          return param;
        }
      }
    

    现在在了 main 方法测试一下:

    如果 config.isUseActualParamName() 为 true,且方法经过构造方法解析后,参数索引和名称的对应关系为:

    
    {0=age, 1=arg1, 2=money, 3=arg3}
    

    现在参数为:

    
    Object[] argsArr = {24, "zhangsan", 1000.0, "chengdou"};
    

    现在调用 getNamedParams 方法来绑定方法名和方法值,获取的结果如下,注意该方法返回的是 Object,其实它是一个 map

    
    {age=24, param1=24, arg1=zhangsan, param2=zhangsan, money=1000.0,  param3=1000.0, arg3=chengdou,  param4=chengdou}
    

    所以在 xml 中的 SQL 中,可以通过 对应的 #{name} 来获取值,也可以通过 #{param1} 等来获取值。

    还有一种情况,就是   config.isUseActualParamName() 为 false,解析后,参数索引和名称的对应关系为:

    
    {0=age, 1=1, 2=money, 3=3}
    

    之后,参数和参数值的绑定又是什么样子的呢?还是 main 方法测试如下,参数还是上面的 argsArr 参数:

    
    {age=24, param1=24, 1=zhangsan, param2=zhangsan, money=1000.0, param3=1000.0, 3=chengdou param4=chengdou}
    

    在 SQL 中也可以通过 #{0} #{1} 之类的来获取参数值。

    所以,综上 ParamNameResolver 类解析 Mapper 接口后,我们在 SQL 中可以通过 #{name} #{param1} #{0} 之类的方式来获取对应的参数值的原因。

    MapperMethod

    在理解了上述的   ParamNameResolver 工具类之后,来看 MapperMethod 就很好理解了。

    在该类中封装了 Mapper 接口对应方法的信息,以及对应的 SQL 语句的信息, MapperMethod 类可以看做是 Mapper 接口和配置文件中的 SQL 语句之间的连接桥梁,

    该类中只有两个属性,分别对应两个内部类, SqlCommand MethodSignature ,其中 SqlCommand  记录了 SQL 语句的名称和类型, MethodSignature 就是 Mapper 接口中对应的方法信息:

    
    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);
      }
    }
    

    先来看看这两个内部类。

    SqlCommand

    
    public static class SqlCommand {
    // SQL 的名称,是接口的全限定名+方法名组成
    private final String name;
    // SQL 的类型,取值:UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH;
    private final SqlCommandType type;
    
    public SqlCommand(Configuration configuration, Class? mapperInterface, Method method) {
      // SQL 名称
      String statementName = mapperInterface.getName() + "." + method.getName();
      // MappedStatement  封装了 SQL 语句的相关信息
      MappedStatement ms = null;
      // 在配置文件中检测是否有该 SQL 语句
      if (configuration.hasStatement(statementName)) {
        ms = configuration.getMappedStatement(statementName);
      } else if (!mapperInterface.equals(method.getDeclaringClass())) { 
        // 是否父类中有该 SQL 的语句
        String parentStatementName = method.getDeclaringClass().getName() + "." + method.getName();
        if (configuration.hasStatement(parentStatementName)) {
          ms = configuration.getMappedStatement(parentStatementName);
        }
      }
      // 处理 @Flush 注解
      if (ms == null) {
        if(method.getAnnotation(Flush.class) != null){
          name = null;
          type = SqlCommandType.FLUSH;
        }
      } else {
        // 获取 SQL 名称和类型
        name = ms.getId();
        type = ms.getSqlCommandType();
      }
    }
    

    MethodSignature

    
      public static class MethodSignature {
    
        private final boolean returnsMany; // 方法的返回值为 集合或数组
        private final boolean returnsMap;  // 返回值为 map
        private final boolean returnsVoid;  // void
        private final boolean returnsCursor; // Cursor
        private final Class? returnType;  // 方法的返回类型
        private final String mapKey; // 如果返回值为 map,则该字段记录了作为 key 的列名
        private final Integer resultHandlerIndex;  // ResultHandler 参数在参数列表中的位置
        private final Integer rowBoundsIndex;  // RowBounds参数在参数列表中的位置
    
        // 用来解析接口参数,上面已介绍过
        private final ParamNameResolver paramNameResolver;
    
        public MethodSignature(Configuration configuration, Class? mapperInterface, Method method) {
          // 方法的返回值类型
          Type resolvedReturnType = TypeParameterResolver.resolveReturnType(method, mapperInterface);
          if (resolvedReturnType instanceof Class?) {
            this.returnType = (Class?) resolvedReturnType;
          } else if (resolvedReturnType instanceof ParameterizedType) {
            this.returnType = (Class?) ((ParameterizedType) resolvedReturnType).getRawType();
          } else {
            this.returnType = method.getReturnType();
          }
          this.returnsVoid = void.class.equals(this.returnType);
          this.returnsMany = (configuration.getObjectFactory().isCollection(this.returnType) || this.returnType.isArray());
          this.returnsCursor = Cursor.class.equals(this.returnType);
          this.mapKey = getMapKey(method);
          this.returnsMap = (this.mapKey != null);
          this.rowBoundsIndex = getUniqueParamIndex(method, RowBounds.class);
          this.resultHandlerIndex = getUniqueParamIndex(method, ResultHandler.class);
          this.paramNameResolver = new ParamNameResolver(configuration, method);
        }
    
        // 根据参数值来获取参数名称和参数值的对应关系 是一个 map,看上面的main方法测试
        public Object convertArgsToSqlCommandParam(Object[] args) {
          return paramNameResolver.getNamedParams(args);
        }
    }
    

    MapperMethod

    接下来看下 MapperMethod,核心的方法为 execute 方法,用于执行方法对应的 SQL :

    
      public Object execute(SqlSession sqlSession, Object[] args) {
        Object result;
        switch (command.getType()) {
          case INSERT: {
            // insert 语句,param 为 参数名和参数值的对应关系
            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()) {
              // void 类型且方法有 ResultHandler 参数,调用 sqlSession.select 执行
              executeWithResultHandler(sqlSession, args);
              result = null;
            } else if (method.returnsMany()) {
              // 返回集合或数组,调用 sqlSession.EselectList 执行
              result = executeForMany(sqlSession, args);
            } else if (method.returnsMap()) {
              // 返回 map ,调用 sqlSession.K, VselectMap
              result = executeForMap(sqlSession, args);
            } else if (method.returnsCursor()) {
              result = executeForCursor(sqlSession, args);
            } else {
              Object param = method.convertArgsToSqlCommandParam(args);
              result = sqlSession.selectOne(command.getName(), param);
            }
            break;
          case FLUSH:
            result = sqlSession.flushStatements();
            break;
          default:
            throw new BindingException("Unknown execution method for: " + command.getName());
        }
        return result;
      }
    

    以上就是 MapperMethod 类的主要实现,就是获取对应接口的名称和参数,调用 sqlSession 的对应方法值执行对应的 SQL 来获取结果,该类中还有一些辅助方法,可以忽略。

    总结

    以上就是 Mapper接口底层的解析,即 binding 模块, Mybatis 会 使用 JDK 的动态代理来为每个 Mapper 接口创建一个代理对象,通过 ParamNameResolver 工具类来解析 Mapper 接口的参数,使得在 XML 中的 SQL 可以使用三种方式来获取参数的值, #{name} #{0} #{param1} ,当接口参数解析完成后,会有 MapperMethod execute 方法来把 接口的名称 即 SQL 对应的名称和参数通过调用 sqlSession的相关方法去执行 SQL 获取结果。

    原文始发于微信公众号(Java技术大杂烩):

    本人花费半年的时间总结的《Java面试指南》已拿腾讯等大厂offer,已开源在github ,欢迎star!

    本文GitHub https://github.com/OUYANGSIHAI/JavaInterview 已收录,这是我花了6个月总结的一线大厂Java面试总结,本人已拿大厂offer,欢迎star

    原文链接:blog.ouyangsihai.cn >> Mybatis Mapper 接口源码解析


     上一篇
    Mybatis 数据库连接池源码解析 Mybatis 数据库连接池源码解析
    本文首发地址为个人博客地址: https://my.oschina.net/mengyuankan/blog/2664784 本文将从以下几个方面介绍 相关文章 前言 类图 工厂类实现 数据库连接实现 连接池的
    2021-04-05
    下一篇 
    Mybatis 解析 SQL 源码分析一 Mybatis 解析 SQL 源码分析一
    相关文章 前言在使用 Mybatis 的时候,我们在 Mapper.xml 配置文件中书写 SQL;文件中还配置了对应的 dao, SQL 中还可以使用一些诸如 for循环, if判断之类的高级特性,当数据库列和 JavaBean属性
    2021-04-05