mybatis源码学习|Mybatis 源码学习(十二) —— binding 包

Mybatis 系列文章导航

文章目录
    • 类关系概览
    • 类详解
      • MapperRegistry
      • MapperProxyFactory
      • MapperProxy
      • MapperMethod
      • MapperMethod 的静态内部类 MethodSignature、SqlCommand、ParamMap
        • MethodSignature
        • SqlCommand
        • ParamMap

可能在刚学 Java 和 Mybatis 的小伙伴,会很好奇为什么明明是一个接口,但是居然可以调用,但是明明并没有声明它的实现类,并且它是怎么找到我们要执行的 SQL 呢。那么这一个包就能解开你的疑惑。
binding 包的主要作用是处理 Java 方法和映射文件中 SQL 语句之间绑定关系。也就是我们常常用到的 userMapper.getById() 方法,是怎么绑定到一条具体 SQL 语句的。
虽然 binding 包下的类不多,但是每个都很经典,只要能理解透的话,你就会发现其实真想要手写一个简单的 ORM 框架也没有那么难。就让我们奔着这个目标继续学习下去吧。
类关系概览 mybatis源码学习|Mybatis 源码学习(十二) —— binding 包
文章图片

图片来自 《通用源码指导书:Mybatis源码详解》—— 易哥
首先观察一下上图,看到这样的一个关系,MapperProxyFactory 用来创建 MapperProxy 而 MapperProxy 用来执行 MapperMethod。现在我们将这三个类对应到我们使用 Mybatis 时所写的。
// 这是一个简单的 Mapper 文件,除了没有那么多方法以外,基本上都差不多 @Mapper public interface UserMapper {User selectById(@Param("userId") Long userId); List selectListByName(@Param("name") String name); }

我们把 MapperProxyFactory 看作 UserMapper,而 MapperProxy 看作 UserMapper 的实现类,MapperMethod 则是 UserMapper 中的某一个方法。
MapperProxyFactory 通过 JDK 动态代理来实现 new 一个 UserMapper 的实现类。
mybatis源码学习|Mybatis 源码学习(十二) —— binding 包
文章图片

看了这个图,我觉得你们心里应该有个底了,但是细心的读者可能会发现一个地方特别奇怪,为啥和上一张图有点不太一样,MapperProxy 下面不是不是 MapperMethod 吗,从哪里冒出来了一个 MapperMethodInvoker。图中的两个 MapperMehtodInvoker 中又分别包含了 MapperMethod 和 MethodHandle。
/* 由于在 JDK8 中又引入了默认方法,所以实际上 Java 映射文件中已经不是所有都是接口了。 默认方法是没有映射到具体的 SQL 的,所以也不能去找到它所指向的 SQL,而是直接通过反射去调用。 所以 Mybatis 抽象出了一个通用的接口 MapperMehtodInvoker,有两个实现类 PlainMethodInvoker、DefaultMethodInvoker 分别对应普通的接口方法 和默认方法。根据不同的方法类型来选用对应的实现。 */ if (m.isDefault()) { // 如果是一个默认方法 /* 通过判断是否存在特定的方法来区分版本,如果有 privateLookupInMethod 方法的话,说明是 JDK9 及以上版本,如果没有则是 JDK8。 可能有的小伙伴会疑惑,为什么不都选用同一种方法呢,是因为 JDK9 中就把对应的实现删除了吗? 其实并不是的,而是由于每个版本可能反射都做了一些优化,比如在 JDK8 中可能某一种反射的方式可能更加安全更加快,但是在 JDK9 中另一种方法更好,所以才会出现这种情况。 */ if (privateLookupInMethod == null) { // 如果当前 JDK 版本为 8 return new DefaultMethodInvoker(getMethodHandleJava8(method)); } else { // 如果当前 JDK 版本为 9+ return new DefaultMethodInvoker(getMethodHandleJava9(method)); } } else { // 如果是一个接口的话,说明是一个映射方法,即我们常用的 getById 这种。 return new PlainMethodInvoker( new MapperMethod(mapperInterface, method, sqlSession.getConfiguration())); }

类详解 终于到了最令人激动的环节了(不知道你们激不激动,反正我是挺激动的)。我们还是跟着之前的类关系图,从最外层的也就是 MapperRegistry 到内层的 MapperMethod 类,一个一个去分析。
MapperRegistry
在分析类关系的时候,我们已经提到了这个类,它保存了 Mybatis 中所有的 Mapper,从注册表中拿到了对应的 Mapper 我们就可以直接调用 Mapper 中的方法了。
核心成员变量
private final Map, MapperProxyFactory> knownMappers = new HashMap<>();

关键的成员变量就是 knowMappers 这x 是一个 Map,可以通过 Class 获取对应的 MapperProxyFactory
核心方法
作为一个保存 MapperProxyFactory 容器,最关键的两个功能就是两个:addMappergetMapper
public T getMapper(Class type, SqlSession sqlSession) { final MapperProxyFactory mapperProxyFactory = (MapperProxyFactory) knownMappers.get(type); // 如果找不到直接抛出异常 if (mapperProxyFactory == null) { throw new BindingException("Type " + type + " is not known to the MapperRegistry."); } // 实例化对象 return mapperProxyFactory.newInstance(sqlSession); }public void addMapper(Class type) { if (type.isInterface()) { // 只添加 interface 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 { // 如果由于报错没有加载完成,那么就要将预先生成好的 MapperProxyFactory 删掉 if (!loadCompleted) { knownMappers.remove(type); } } } }

Mybatis 为什么会有循环加载示意图。
mybatis源码学习|Mybatis 源码学习(十二) —— binding 包
文章图片

这里没有讲解析的细节是因为 MapperAnnotationBuilderbuilder 包的内容,下一篇文章应该就会分析 builder 包,大家可以好好期待一下。
MapperProxyFactory
MapperProxyFactory 作为一个 Mapper 的’原型’,包含了 Mapper 中的方法。
核心成员变量
// 当前 MapperProxyFactory 代表的 Mapper 的类型 private final Class mapperInterface; // Java 映射文件中的方法及其对应的调用器 private final Map methodCache = new ConcurrentHashMap<>();

核心方法
protected T newInstance(MapperProxy mapperProxy) { // JDK 动态代理生成代理对象 return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); }public T newInstance(SqlSession sqlSession) { // MapperProxy 实现了 InvocationHandler 所以只要调用被代理对象的任何方法,实际上都会进入到 invoke 方法中 final MapperProxy mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache); return newInstance(mapperProxy); }

其实最关键的就是 Proxy.newProxyInstance,可以说这句话就是 Mybatis 实现 Java 方法到执行 SQL 执行映射的底层。JDK 动态代理我就不在这里讲述了,相信聪明的小伙伴能找到不少相关的资料。
MapperProxy
MapperProxy 可以理解为 Java 映射接口的实现类。
核心成员变量
// 只允许以下作用域的方法能执行反射,利用二进制的每一位代表一种修饰符,可以节省空间 private static final int ALLOWED_MODES = MethodHandles.Lookup.PRIVATE | MethodHandles.Lookup.PROTECTED | MethodHandles.Lookup.PACKAGE | MethodHandles.Lookup.PUBLIC; // JDK8 中只有 Lookup(Class, int) private static final Constructor lookupConstructor; // 在 JDK9 中才有 privateLookupIn(Class, Lookup) private static final Method privateLookupInMethod; // 会话 private final SqlSession sqlSession; // 被代理对象 private final Class mapperInterface; // Java 映射文件中的方法及其对应的调用器,对象是由 MapperProxyFactory 创建的,生成的每个代理对象都是其的引用 // 所以,所有的代理对象都共享这些内容 private final Map methodCache;

核心方法
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (Object.class.equals(method.getDeclaringClass())) { // 如果调用的时 Object 类中声明的方法,直接反射调用 return method.invoke(this, args); } else { return cachedInvoker(method).invoke(proxy, method, args, sqlSession); } }// 获取已经缓存好的 MapperMethodInvoker private MapperMethodInvoker cachedInvoker(Method method) throws Throwable { MapperMethodInvoker invoker = methodCache.get(method); if (invoker != null) { // 如果在缓存中找到了对应的 invoker 方法,那么则直接返回 return invoker; }// 如果没有缓存对应的 invoker 方法,则构建并缓存起来 return methodCache.computeIfAbsent(method, m -> { if (m.isDefault()) { // 如果是一个默认方法 if (privateLookupInMethod == null) { // 当前版本为JDK 8 return new DefaultMethodInvoker(getMethodHandleJava8(method)); } else { // 当前版本为JDK 9+ return new DefaultMethodInvoker(getMethodHandleJava9(method)); } } else { // 如果不是默认方法,说明是一个需要映射的方法,即我们常用的 userMapper.getById 这种。 return new PlainMethodInvoker( new MapperMethod(mapperInterface, method, sqlSession.getConfiguration())); } }

注:我在粘贴代码段的时候,除非 try catch 中有重要逻辑,否则都会将其删除掉。尽量减少代码数量,让小伙伴们能专注于重点的逻辑。
小伙伴们应该知道在 JDK8 中接口引入了默认方法,默认方法其实和普通类的方法一样,是不强制子类去实现它的。所以直接反射调用默认方法就好了,而非默认方法就不行,必须要自己去实现方法中的业务逻辑。
可以看到返回的是 MapperMethodInvoker 的两个实现类:DefaultMethodInvokerPlainMethodInvoker,对应 invoke 方法实现如下。
// PlainMethodInvoker public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable { // 执行对应的 SQL return mapperMethod.execute(sqlSession, args); }// DefaultMethodInvoker public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable { // 等同于 method.invoke(),但是比它的性能更高 return methodHandle.bindTo(proxy).invokeWithArguments(args); }

MapperMethod
MapperMethod 对应的是一个映射方法。映射方法包含两个关键要素,一个是要执行的 SQL,一个是方法的签名。
核心成员变量
// SQL 信息 private final SqlCommand command; // 方法签名 private final MethodSignature 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()) { // 如果没有返回值,并且请求参数中有 ResultHandler 才执行这个方法 executeWithResultHandler(sqlSession, args); result = null; } else if (method.returnsMany()) { // 如果返回参数是一个集合或者数组的话,则调用这个方法 result = executeForMany(sqlSession, args); } else if (method.returnsMap()) { // 如果返回的是一个 Map 类型 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; // 只是用来刷新缓存的语句,不映射任何一条 SQL case FLUSH: result = sqlSession.flushStatements(); break; default: throw new BindingException("Unknown execution method for: " + command.getName()); } // 如果要求返回一个原生类型,但是 result 又是 null,那么就需要抛出异常,因为 null 转换为 int 或者其他基本类型会报空指针异常。 if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) { throw new BindingException(); } return result; }

可以看到说的是 execute 方法,实际上调用的还是 SqlSession 的方法,这是设计模式中的委派者模式。至于 SqlSession 里面的代码,我们会在后面的分析,一步一步慢慢来哟。
MapperMethod 的静态内部类 MethodSignature、SqlCommand、ParamMap
MethodSignature MethodSignature 是方法的签名,在 Java 语言中签名就是方法的名称和方法的请求参数,而 Mybatis 中定义的这个签名和 Java 的有点区别。
核心成员变量
/* 返回值的相关信息 */ // 返回值类型为 Collection 或 Array private final boolean returnsMany; // 返回值类型为 Map private final boolean returnsMap; // 没有返回值 private final boolean returnsVoid; // 返回值类型为 Cursor private final boolean returnsCursor; // 返回值类型为 Optional private final boolean returnsOptional; // 返回值类型 private final Class returnType; // mapKey的值 private final String mapKey; /* 请求参数的相关信息 */ // 请求参数中 ResultHandler 的下标 private final Integer resultHandlerIndex; // 请求参数中 RowBounds 的下标 private final Integer rowBoundsIndex; // 请求参数名称解析器,解析后返回的是一个 ParamMap private final ParamNameResolver paramNameResolver;

【mybatis源码学习|Mybatis 源码学习(十二) —— binding 包】核心方法
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(); } // 是否返回的是 void this.returnsVoid = void.class.equals(this.returnType); // 是否返回的是集合或数组 this.returnsMany = configuration.getObjectFactory().isCollection(this.returnType) || this.returnType.isArray(); // 是否返回时游标 this.returnsCursor = Cursor.class.equals(this.returnType); // 是否返回的是 optional this.returnsOptional = Optional.class.equals(this.returnType); // 得到 MapKey this.mapKey = getMapKey(method); // 如果存在 MapKey 说明返回值类型是 Map this.returnsMap = this.mapKey != null; // 得到请求参数类型为 RowBounds 的下标 this.rowBoundsIndex = getUniqueParamIndex(method, RowBounds.class); // 得到请求参数类型为 ResultHandler 的下标 this.resultHandlerIndex = getUniqueParamIndex(method, ResultHandler.class); // 得到参数名称解析器 this.paramNameResolver = new ParamNameResolver(configuration, method); }

这是 MethodSignature 的构造方法,是用来初始化会初始化所有的成员变量。
SqlCommand SqlCommand 记录了该映射方法所映射到的 SQL 的一些关键信息。SQL 的类型和 SQL 的唯一ID,通过 SQL 的类型可以决定使用什么方式处理执行结果,通过 SQL 的唯一ID可以找到需要执行的 SQL。
核心成员变量
// SQL 的名称,在整个项目中是唯一的,等同于 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,实际上是从 Configuration 中获取的 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(); } } else { // SQL 指令的名称为 MappedStatement 的ID,是唯一的 name = ms.getId(); type = ms.getSqlCommandType(); if (type == SqlCommandType.UNKNOWN) { throw new BindingException(); } } }

ParamMap 继承自 HashMapkey 代表请求参数的名称,value 代表请求参数具体的值,只覆盖了 get 方法。
核心方法
public V get(Object key) { if (!super.containsKey(key)) { throw new BindingException( "Parameter '" + key + "' not found. Available parameters are " + keySet()); } return super.get(key); }

如果找不到对应的名称,直接抛出异常。
总结:可以发现,这篇文章的代码是相当的多的,因为这个包是相当重要的包。其中的类也不多,所以基本上每行代码都是非常的重要,因此希望大家可以去把这个包中的内容都仔细的看一下。肯定能对以后的编码有所帮助。

    推荐阅读