mybatis源码分析5 - mapper读写数据库完全解析

1 引言和主要类

上一节讲解了sqlSession读写数据库的整个流程和四大组件的执行过程,相信大家对mybatis操作数据库有了一定的了解。上一节还提到过,其实我们还可以通过mapper方式读写数据库,并且mybatis建议使用mapper方式,而不是直接通过sqlSession的selectList update等方法。使用mapper方式的例子如下

// 读取XML配置文件
String resource = "main/resources/SqlMapConfig.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
// 创建sqlSessionFactory单例,初始化mybatis容器
SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 创建sqlSession实例,用它来进行数据库操作,mybatis运行时的门面
SqlSession session = sessionFactory.openSession();
// 获取mapper接口动态代理对象
UserMapper mapper = session.getMapper(UserMapper.class);
// 利用动态代理调用mapper的相关方法
User user = mapper.findUserById(1);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

mapper使用起来十分方便,它的优点如下

  1. 只用定义接口,无需实现类。也不需要继承任何类,十分灵活
  2. 通过接口的方法进行调用,可以有效避免String拼写错误
  3. 返回值不用局限于sqlSession有限的那几个方法,实现了完全的自定义。

下面我们来分析mapper方式是如何读写数据库的。

2 流程

mapper操作数据库分为两个步骤,先getMapper()获取动态代理对象,然后利用mapper对象操作数据库。我们一步步分析。

2.1 getMapper()获取动态代理对象流程

先从DefaultSqlSession的getMapper()分析。

// mapper动态代理方式执行SQL命令的入口
public <T> T getMapper(Class<T> type) {
  return configuration.<T>getMapper(type, this);
}
  • 1
  • 2
  • 3
  • 4

这儿是总入口,没啥好说的,再看Configuration类的getMapper()

// mapper方式执行SQL命令,先获取mapper对象
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
  return mapperRegistry.getMapper(type, sqlSession);
}
  • 1
  • 2
  • 3
  • 4

这也没啥好说的,看MapperRegistry类

// 动态代理方式创建mapper对象
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
  // 先从mybatis初始化,也就是创建SqlSessionFactory时,创建的MapperProxyFactory的map中取出当前mapper接口对应的MapperProxyFactory
  // MapperProxyFactory为mapper动态代理的工厂类
  final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
  if (mapperProxyFactory == null) {
    throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
  }
  try {
    // 利用工厂类创建mapper动态代理对象
    return mapperProxyFactory.newInstance(sqlSession);
  } catch (Exception e) {
    throw new BindingException("Error getting mapper instance. Cause: " + e, e);
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

先从mybatis初始化时创建的MapperProxyFactory的Map中,取出当前mapper接口类的MapperProxyFactory。利用这个工厂类构造MapperProxy。下面详细看这个过程

// 创建mapper动态代理对象
public T newInstance(SqlSession sqlSession) {
  // 构造MapperProxy对象,然后创建我们定义的mapper接口对应的对象
  final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
  return newInstance(mapperProxy);
}

// 动态代理的方式,反射生成mapper接口对象
  protected T newInstance(MapperProxy<T> mapperProxy) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

先创建MapperProxy类,然后创建Java动态代理对象,将mapperProxy作为它的InvocationHandler。执行动态代理方法时,会回调InvocationHandler的invoke()方法。对Java动态代理不清楚的同学,建议复习一下这个内容。

Java的动态代理类Proxy的newProxyInstance()生成mapper接口的动态代理对象后,getMapper()方法就执行完毕了。接下来就是动态代理执行方法调用的过程了。下面详细分析。

2.2 mapper 接口方法调用流程

mapper接口方法调用,是通过动态代理的方式执行的。Proxy动态代理执行方法调用时,调用到invocationHandler的invoke方法。我们传入的invocationHandler是创建的MapperProxy对象,故方法调用的入口为MapperProxy的invoke()方法。下面详细分析。

  // 调用mapper接口中方法时的入口,Proxy会回调InvocationHandler的invoke方法
  // @Param proxy: Java反射的动态代理对象,getMapper()中通过Java反射 Proxy.newProxyInstance()生成的动态代理
  // @Param method: 要调用的接口方法
  // @Param args: 方法入参
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      // mapper接口类中的抽象方法不会执行这儿的if和else
      if (Object.class.equals(method.getDeclaringClass())) {
        // mapper是一个接口,而非实现类,不会走到这儿
        return method.invoke(this, args);
      } else if (isDefaultMethod(method)) {
        // mapper中方法为抽象方法,也不会走到这儿
        return invokeDefaultMethod(proxy, method, args);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }

    // 生成mapperMethod,先从cache中取,没有则创建一个MapperMethod。
    final MapperMethod mapperMethod = cachedMapperMethod(method);

    // 执行execute,通过mapperMethod的method方法名,从XML中找到匹配的SQL语句,最终利用sqlSession执行数据库操作
    return mapperMethod.execute(sqlSession, args);
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

先创建MapperMethod对象,然后利用它的execute来执行。我们分两小节分别分析。

2.2.1 MapperMethod的创建

先看MapperMethod的创建过程cachedMapperMethod方法。

// 生成mapperMethod,先从cache中取,没有则创建一个MapperMethod
private MapperMethod cachedMapperMethod(Method method) {
  // 先从cache中取
  MapperMethod mapperMethod = methodCache.get(method);

  // 未命中,则创建一个MapperMethod
  if (mapperMethod == null) {
    // 创建MapperMethod用来执行mapper接口的方法。
    // mapperInterface: 用户定义的mapper接口类
    // method:要执行的mapper接口中的方法
    // Configuration: XML配置信息
    mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
    methodCache.put(method, mapperMethod);
  }
  return mapperMethod;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

MapperMethod对象是mapper方法调用的核心对象,它先尝试从cache中取,没有则创建。下面看MapperMethod的构造方法。

// MapperMethod的构造方法,创建SqlCommand和MethodSignature两个主要成员变量。二者也是动态代理的关键
// @Param mapperInterface: 用户定义的mapper接口类
// @Param method:要执行的mapper接口中的方法
// @Param Configuration: XML配置信息
public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
  // 创建SqlCommand,包含SQL语句的id和type两个关键字段,type为insert update delete等类型。
  this.command = new SqlCommand(config, mapperInterface, method);
  // 创建MethodSignature,包含方法的返回值returnType等关键字段
  this.method = new MethodSignature(config, mapperInterface, method);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

MapperMethod包含两个主要成员变量,SqlCommand和MethodSignature。SqlCommand是mapper.xml中SQL语句的描述,包含了SQL语句的id和type。MethodSignature是mapper接口中方法的描述,包含了方法返回值等关键信息。有了它们,mapper.xml配置文件和mapper接口方法基本就描述清楚了。后面会使用到这两个变量来执行方法调用。下面我们分别看这两个关键对象是如何创建的。

先看SqlCommand的构造过程。

public static class SqlCommand {

  private final String name; // sql语句的id,mapper接口类名.方法名,拼接而成
  private final SqlCommandType type;  // sql语句的标签,insert select update delete等

  // SqlCommand构造方法,描述了mapper.xml配置信息中一条SQL语句
  public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
    // 先获取mapper接口方法的方法名,和方法声明所在的类,也就是mapper接口类
    final String methodName = method.getName();
    final Class<?> declaringClass = method.getDeclaringClass();

    // 拼接mapperInterface和methodName,构成id,从mybatis初始化阶段生成的mappedStatements这个map中获取当前方法对应的mappedStatement
    // mybatis初始化一节讲过mappedStatement,它是通过解析mapper.xml生成的,描述了其中的一条CRUD语句。
    // mappedStatement是mapper.xml中SQL操作语句的核心,包含sqlSource和BoundSql等重要对象
    MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass,
        configuration);

    if (ms == null) {
      // 找不到此sql语句时
      if (method.getAnnotation(Flush.class) != null) {
        name = null;
        type = SqlCommandType.FLUSH;
      } else {
        throw new BindingException("Invalid bound statement (not found): "
            + mapperInterface.getName() + "." + methodName);
      }
    } else {
      // 找到此SQL语句对应的MappedStatement时。
      // name为SQL语句的id,mapper接口类名.方法名,拼接而成。也就是上面分析的statementId。
      name = ms.getId();

      // sqlCommandType为mapper.xml中SQL语句的标签,如select insert delete update等
      type = ms.getSqlCommandType();
      if (type == SqlCommandType.UNKNOWN) {
        throw new BindingException("Unknown execution method for: " + name);
      }
    }
  }

  // 从mybatis初始化时创建的mappedStatements中找到本SQL语句对应的mappedStatement
  private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName,
        Class<?> declaringClass, Configuration configuration) {
      // 以mapper接口类名.方法名,作为id,这样可以唯一对应一条SQL操作。
      // mapper.xml中namespace对应接口类名,SQL语句id对应mapper接口中方法名。
      String statementId = mapperInterface.getName() + "." + methodName;

      // 本SQL语句在mybatis初始化阶段已经添加了时
      if (configuration.hasStatement(statementId)) {
        // 直接从初始化mybatis阶段,生成的mappedStatements总map表中,获取当前SQL语句的mappedStatement
        return configuration.getMappedStatement(statementId);
      } else if (mapperInterface.equals(declaringClass)) {
        return null;
      }

      for (Class<?> superInterface : mapperInterface.getInterfaces()) {
        if (declaringClass.isAssignableFrom(superInterface)) {
          MappedStatement ms = resolveMappedStatement(superInterface, methodName,
              declaringClass, configuration);
          if (ms != null) {
            return ms;
          }
        }
      }
      return null;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66

MethodSignature构造方法我们就不详细分析了,有兴趣的同学可以自行分析。

2.2.2 MapperMethod的执行

下面来看MapperMethod的方法执行,也就是execute方法。

// 执行mapper接口中的方法
public Object execute(SqlSession sqlSession, Object[] args) {
  Object result;
  // 按照SQL语句的不同type,如insert update等,调用sqlSession的insert update等对应方法,执行数据库操作
  // 下面重点分析Select
  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:
      // 根据select返回值的不同,调用对应的不同方法来查询,其实本质上大同小异,最后都是调用了sqlSession的select方法
      if (method.returnsVoid() && method.hasResultHandler()) {
        // 无需返回值时
        executeWithResultHandler(sqlSession, args);
        result = null;
      } else if (method.returnsMany()) {
        // 返回值为Collection集合类型或者数组类型时,下面重点分析
        result = executeForMany(sqlSession, args);
      } else if (method.returnsMap()) {
        // 返回值为Map类型时
        result = executeForMap(sqlSession, args);
      } else if (method.returnsCursor()) {
        // 返回值为Cursor类型时
        result = executeForCursor(sqlSession, args);
      } else {
        // 返回值为其他类型时,认为不是集合类型,故调用selectOne返回一个对象。
        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());
  }
  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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54

execute按照SQL语句的不同type,如insert update等,调用sqlSession的insert update等对应方法。由此可见,最终mapper方式还是通过sqlSession来完成数据库insert select等操作的。我们以select这个type为例分析。不同的select返回值类型,如集合Collection,Map等对应不同的执行方法,但其实本质都是调用selectList方法。我们以最常见的返回Collection为例分析。

返回值为Collection集合类型或者数组类型时,调用executeForMany()来执行方法调用。如下。

// select返回值为集合类型时,如List
private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
  List<E> result;
  // 调用paramNameResolver,处理入参
  Object param = method.convertArgsToSqlCommandParam(args);

  // 调用sqlSession的selectList()方法,查询数据库。是否定义了逻辑分页时,会有些细微差别
  // selectList方法前面一节已经讲解过了,这儿不详细讲了。
  // 从这儿可见,mapper方式最终还是通过sqlSession操作数据库。
  // mapper方式比直接操作方式更灵活,且不易出错。mybatis建议大家使用mapper方式。
  if (method.hasRowBounds()) {
    // 定义了逻辑分页rowBounds时
    RowBounds rowBounds = method.extractRowBounds(args);
    result = sqlSession.<E>selectList(command.getName(), param, rowBounds);
  } else {
    // 未定义rowBounds时
    result = sqlSession.<E>selectList(command.getName(), param);
  }

  // 由于selectList返回的是List类型的集合,而mapper接口中的方法返回值可能为Set Queue等其他集合类型,故此处需要做转换
  if (!method.getReturnType().isAssignableFrom(result.getClass())) {
    if (method.getReturnType().isArray()) {
      return convertToArray(result);
    } else {
      return convertToDeclaredCollection(sqlSession.getConfiguration(), result);
    }
  }
  return result;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

可以很清晰的看到,最终我们还是通过sqlSession的selectList方法完成查询操作的。selectList()方法的流程上一节已经讲解过了,这儿就不说了。这样,mapper动态代理就完成了它的方法调用了。

3 总结

sqlSession的mapper动态代理方法调用,比直接通过sqlSession的selectList,update等方法更优。mapper方式是sqlSession selectList等直接调用方式的一层封装。正因为有了这层封装,使得mybatis在增加灵活性之余,还能提高健壮性。这也充分体现了mybatis的设计精巧,值得我们学习。

阅读更多

没有更多推荐了,返回首页