Mybatis Mapper 接口源码解析

TSMYK Java技术编程

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

相关文章

Mybatis 解析配置文件的源码解析 Mybatis 类型转换源码分析 Mybatis 数据库连接池源码解析

前言

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

源码分析

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

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

MapperProxy

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


public class MapperProxy<T> implements InvocationHandler, Serializable {

  // 关联的 sqlSession 对象
  private final SqlSession sqlSession;
  // 目标接口,即 Mapper 接口对应的 class 对象
  private final Class<T> mapperInterface;
  // 方法缓存,用于缓存 MapperMethod对象,key 为 Mapper 接口中对应方法的 Method 对象,value 则是对应的 MapperMethod,MapperMethod 会完成参数的转换和 SQL 的执行功能
  private final Map<Method, MapperMethod> methodCache;

  public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, 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 MapperProxyFactory<T> {
  // 当前的 MapperProxyFactory 对象可以创建的 mapperInterface 接口的代理对象
  private final Class<T> mapperInterface;

  // MapperMetho缓存
  private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>();

  // 创建 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<T>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
  }
}

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

MapperRegistry

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


public class MapperRegistry {
  // 配置对象,包含所有的配置信息
  private final Configuration config;

  // 接口和代理对象工厂的对应关系,会用工厂去创建接口的代理对象
  private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>();

  // 注册 Mapper 接口
  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 {
        // 会为每个 Mapper 接口创建一个代理对象工厂
        knownMappers.put(type, new MapperProxyFactory<T>(type));

        // 下面这个先不看,主要是xml的解析和注解的处理
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        parser.parse();
        loadCompleted = true;
      } finally {
        // 如果注册失败,则移除掉该Mapper接口
        if (!loadCompleted) {
          knownMappers.remove(type);
        }
      }
    }
  }

  // 获取 Mapper 接口的代理对象,
  public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    // 从缓存中获取该 Mapper 接口的代理工厂对象
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) 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 SortedMap<Integer, 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 SortedMap<Integer, String> map = new TreeMap<Integer, 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 Map<String, Object> param = new ParamMap<Object>();
      int i = 0;
      for (Map.Entry<Integer, 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.<E>selectList 执行
          result = executeForMany(sqlSession, args);
        } else if (method.returnsMap()) {
          // 返回 map ,调用 sqlSession.<K, V>selectMap
          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 获取结果。