Mybatis-binding模块

Scroll Down

在看binding模块之前首先要想好下面三点:

1、binding模块存在的目的是什么?解决了什么问题?
2、binding模块有哪些必须要的步骤(关键点)?
3、binding模块与原ibatis(SqlSession)是怎么衔接的?

概述

我们先来看下使用ibatis也就是没有binding模块的情况:

// 开启一个SqlSession
SqlSession sqlSession = factory.openSession();
int arr[]={4,5,6};
// 通过SqlSession的selectList方法执行查询操作
List<User> users = sqlSession.selectList("UserMapper.findUserById", arr);
// 关闭SqlSession
sqlSession.close();

这样操作有几个缺点。
1、首先,就是SqlSession的执行方法需要用户手动传入一个statement参数,用来映射MapperStatement。这个statement极易写错。而且这个错误只有在运行时才会暴露出来。这一点是安全性。
2、用户需要去决定到底该使用哪个SqlSession的方法来实现具体的功能,其实,我们所定义的所有的statementId到最后都对应着SqlSession中不同的数据库操作inset|update|select|delete,而这个具体的操作其实是有规律的,比如通过标签来判断执行的方法类型,通过返回值来判断用selectOne或者selectMany。这一点是不透明性。
3、我觉得之前的statement只能是一个标识,用来查找statement的标识,并不具备可读性,这个可读性需要人为的标识出来。
4、我们在手动调用SqlSession的时候传入的parameter实际上是一个映射的参数,它可以是一个对象,也可以是一个map,当接口的参数是一个的时候可以是一个对象,当接口中的参数是多个的时候,就变成了一个Map,而在解析的时候会使用ognl表达式来解析。后面我会详细说明。
基于上面几点,Mybatis引入了binding模块。我们先来看下binding模块是如何使用的。

public void mybatis() { 
    // 1、加载资源
    Reader reader = Resources.getResourceAsReader("mybatis-config.xml");
    // 2、构建SqlSessionFactory
    SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader);
    // 3、通过SqlSessionFactory构建SqlSession
    SqlSession sqlSession = factory.openSession();
    // 4、通过SqlSession获取用户接口代理类
    UserDao userDao = sqlSession.getMapper(UserDao.class);
    // 5、执行方法
    userDao.addUser(user);
    // 6、提交
    sqlSession.commit();
    // 7、关闭
    sqlSession.close();
}

可以看到,这里需要我们定义一个UserDao的接口类,这个接口类中定义了我们需要执行的一些方法。这些类名+方法名决定了一个唯一的Statement。如果在实例化Dao对象的时候找不到对应的Statement那么会抛出异常。

所以总结下,其实mybatis的binding模块存在的最重要的意义是实现了通过Dao接口和xml映射文件的绑定,自动生成接口的具体实现,这样做的好处就是尽可能的屏蔽实现细节,同时可以将实现模板化。

binding模块的核心内容

所以,目标有了,想要实现接口和配置文件的绑定,自动实现接口,该怎么实现呢?

首先,我们先看自动实现接口和配置文件的绑定。

自动实现接口和配置文件绑定,必要的功能:
1、确定一个唯一的MappedStatement。
2、自动确定需要执行的SqlSession的方法,确定方法需要两点,一个是方法名,一个是返回类型。
3、需要确定SqlSession用户入参。

为了实现上面三点。Mybatis引入了一个叫MapperMethod的类,该类持有了两个类的引用,分别是SqlCommand和MethodSignature。
我们先来看SqlCommand的实现

public static class SqlCommand {
    // 名称空间+id
    private final String name;
    // SQL类型UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH;
    private final SqlCommandType type;

    public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
      // 获取方法名
      final String methodName = method.getName();
      // 获取类
      final Class<?> declaringClass = method.getDeclaringClass();
      // 通过类和方法名来获取MappedStatement,注意这里获取不到就会直接抛出异常,不用等到执行才抛。
      MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass,
          configuration);
      if (ms == null) {
        if (method.getAnnotation(Flush.class) != null) {
          name = null;
          type = SqlCommandType.FLUSH;
        } else {
          throw new BindingException("Invalid bound statement (not found): "
              + mapperInterface.getName() + "." + methodName);
        }
      } else {
        name = ms.getId();
        // 获取方法执行的类型。之前解析Mapper的时候会自动解析对应的TYPE。
        type = ms.getSqlCommandType();
        if (type == SqlCommandType.UNKNOWN) {
          throw new BindingException("Unknown execution method for: " + name);
        }
      }
    }

    private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName,
        Class<?> declaringClass, Configuration configuration) {
      String statementId = mapperInterface.getName() + "." + methodName;
      if (configuration.hasStatement(statementId)) {
        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;
    }
  }

这里我们看到其实SqlCommand做了两件事,1、获取唯一ID,2、获取方法执行类型。
我们知道唯一定位一个MappedStatement是通过一个名称空间(namespace)和一个SQL的id。其中namespace可以是用户自定义接口的全路径,那么SQL的id可以是一个方法名。
也就是说我们实现接口和配置文件的绑定是通过namespace和id来的。
我们先看Configuration中定义如下:

// 定义了名称空间和MappedStatement的映射
protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection");

我们再来看下MethodSignature

public static class MethodSignature {
    // 是否返回多个
    private final boolean returnsMany;
    // 是否返回是Map
    private final boolean returnsMap;
    // 是否返回是void
    private final boolean returnsVoid;
    // 是否返回Cursor
    private final boolean returnsCursor;
    // 返回类型
    private final Class<?> returnType;
    private final String mapKey;
    private final Integer resultHandlerIndex;
    private final Integer rowBoundsIndex;
    // 参数名解析器,确定SqlSession用户入参
    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);
    }

    public Object convertArgsToSqlCommandParam(Object[] args) {
      return paramNameResolver.getNamedParams(args);
    }

    private Integer getUniqueParamIndex(Method method, Class<?> paramType) {
      Integer index = null;
      final Class<?>[] argTypes = method.getParameterTypes();
      for (int i = 0; i < argTypes.length; i++) {
        if (paramType.isAssignableFrom(argTypes[i])) {
          if (index == null) {
            index = i;
          } else {
            throw new BindingException(method.getName() + " cannot have multiple " + paramType.getSimpleName() + " parameters");
          }
        }
      }
      return index;
    }

    private String getMapKey(Method method) {
      String mapKey = null;
      if (Map.class.isAssignableFrom(method.getReturnType())) {
        final MapKey mapKeyAnnotation = method.getAnnotation(MapKey.class);
        if (mapKeyAnnotation != null) {
          mapKey = mapKeyAnnotation.value();
        }
      }
      return mapKey;
    }
  }

这里我们看到MethodSignature也是做了两件事,1、确定返回类型,2、确定用户调用SqlSession的实际入参对象。
扩展内容:我们把ParamNameResolver代码贴出来,看一下参数是如何解析和获取的。

// 参数解析:主要的逻辑是按照参数的顺序进行排序组成一个Map,key是顺序,value是字段名,
// 如果没有就按照"0"、"1"补充。 然后解析的时候也是按照顺序从数组中挨个取出数据,
// 需要注意,获取参数需要按照顺序来
public ParamNameResolver(Configuration config, Method method) {
    // 解析参数类型
    final Class<?>[] paramTypes = method.getParameterTypes();
    // 解析参数的注解
    final Annotation[][] paramAnnotations = method.getParameterAnnotations();
    // 最后的结果,注意是排序的,从0开始依次有多少个参数,map里面就有多少的键值对。
    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++) {
      if (isSpecialParameter(paramTypes[paramIndex])) {
        // skip special parameters
        continue;
      }
      String name = null;
      // 开始解析参数
      for (Annotation annotation : paramAnnotations[paramIndex]) {
        // 如果存在Param注解,就解析Param的值最为value存起来。
        if (annotation instanceof Param) {
          hasParamAnnotation = true;
          name = ((Param) annotation).value();
          break;
        }
      }
      // 如果没有解析到,则尝试直接解析参数的字段名,也不一定解析到,看JDK配置
      if (name == null) {
        // @Param was not specified.
        if (config.isUseActualParamName()) {
          name = getActualParamName(method, paramIndex);
        }
        // 如果实在没有了,就补充0,1这种数字字符串
        if (name == null) {
          // use the parameter index as the name ("0", "1", ...)
          // gcode issue #71
          name = String.valueOf(map.size());
        }
      }
      // 放入集合
      map.put(paramIndex, name);
    }
    names = Collections.unmodifiableSortedMap(map);
  }
  // 根据入参获取需要传给SqlSession的真实的对象,这个对象有可能是Map,有可能是个单独对象
  public Object getNamedParams(Object[] args) {
    final int paramCount = names.size();
    // 如果没有解析到,那就返回空
    if (args == null || paramCount == 0) {
      return null;
      // 如果参数只有1个对象,并且没有Param标记,就直接把对象取出,返回,
      // 后续会使用ognl表达式解析这个对象
    } else if (!hasParamAnnotation && paramCount == 1) {
      return args[names.firstKey()];
    } else {
      // 接下来就是存在多个参数或者存在Param标记的参数的情况,这种情况需要组成一个map
      final Map<String, Object> param = new ParamMap<Object>();
      int i = 0;
      for (Map.Entry<Integer, String> entry : names.entrySet()) {
        // 这个map存放的有可能是"0","1",有可能是Param的参数值,有可能是参数的名字
        // value则存放的就是参数的值。
        param.put(entry.getValue(), args[entry.getKey()]);
        // add generic param names (param1, param2, ...)
        final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1);
        // ensure not to overwrite parameter named with @Param
        if (!names.containsValue(genericParamName)) {
          param.put(genericParamName, args[entry.getKey()]);
        }
        i++;
      }
      return param;
    }
  }

这时候我们可以确定一个SqlSession中唯一的方法了。MapperMethod的代码如下

public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) {
      case INSERT: {
        // 实际上是调用了ParamNameResolver
        Object param = method.convertArgsToSqlCommandParam(args);
        // 执行sqlSession.insert
        result = rowCountResult(sqlSession.insert(command.getName(), param));
        break;
      }
      case UPDATE: {
        // 实际上是调用了ParamNameResolver
        Object param = method.convertArgsToSqlCommandParam(args);
        // 执行sqlSession.update
        result = rowCountResult(sqlSession.update(command.getName(), param));
        break;
      }
      case DELETE: {
        // 实际上是调用了ParamNameResolver
        Object param = method.convertArgsToSqlCommandParam(args);
        // 执行sqlSession.delete
        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 {
          // 实际上是调用了ParamNameResolver
          Object param = method.convertArgsToSqlCommandParam(args);
          // 执行sqlSession.selectOne
          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、Mybatis使用SqlCommand和MethodSignature实现了接口的定位。
2、Mybatis使用了MapperMethod实现了到底层SqlSession操作数据库方法的调用。

最后我们再来看一下,Mybatis是如何实现接口自动代理的。这部分就简单了。

首先,Mybatis先定义了一个代理类MapperProxy。
代理类一般只增强方法功能。而获取代理对象是通过ProxyFactory来实现的。
现在我们来看Mybatis的实现。

public class MapperProxy<T> implements InvocationHandler, Serializable {
  // 持有一个SqlSession的对象,代理模式
  private final SqlSession sqlSession;
  // 需要代理的类
  private final Class<T> mapperInterface;
  // 缓存下MapperMethod,不用再重复解析SqlCommand和MethodSignature
  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;
  }

  @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 if (isDefaultMethod(method)) {
        return invokeDefaultMethod(proxy, method, args);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
    // 获取MapperMethod,并调用执行方法,上面已经分析过了。
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);
  }
  // 缓存
  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;
  }
}

我们再来看下ProxyFactory,Mybatis中的ProxyFactory是MapperProxyFactory

public class MapperProxyFactory<T> {
  // 代理类
  private final Class<T> mapperInterface;
  // 缓存,这里只是定义了一个对象
  private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>();

  public MapperProxyFactory(Class<T> mapperInterface) {
    this.mapperInterface = mapperInterface;
  }

  public Class<T> getMapperInterface() {
    return mapperInterface;
  }

  public Map<Method, MapperMethod> getMethodCache() {
    return methodCache;
  }

  @SuppressWarnings("unchecked")
  protected T newInstance(MapperProxy<T> mapperProxy) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }

  public T newInstance(SqlSession sqlSession) {
    // 手动创建一个Proxy,并且创建代理
    final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
  }

}

最后,Mybatis定义了一个MapperRegistry用来存放MapperProxy。

private final Configuration config;
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>();
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);
    }
  }
  
  public <T> boolean hasMapper(Class<T> type) {
    return knownMappers.containsKey(type);
  }

  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<T>(type));
        // It's important that the type is added before the parser is run
        // otherwise the binding may automatically be attempted by the
        // mapper parser. If the type is already known, it won't try.
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        parser.parse();
        loadCompleted = true;
      } finally {
        if (!loadCompleted) {
          knownMappers.remove(type);
        }
      }
    }
  }

然后,将getMapper暴露给SqlSession供用户调用。