Mybatis源码解析--Mapper代理对象

1 篇文章 0 订阅
1 篇文章 0 订阅

Mybatis源码解析--Mapper代理对象

背景

在开始之前,我们需要大概知道我们操作数据库的演变历程:最初使用数据库提供商提供的接口--->JDBC--->简单封装的工具类(DBUtils)--->持久层框架(Mybatis、Hibernate)

开玩笑归开玩笑,相信能看到这篇博客,就说明想搞明白Mybatis的底层原理,咱们一起学习交流。言归正传,上面这个演进太繁琐,下面整个缩水版的

 

由于本篇主要围绕Mapper代理对象生成过程的源码分析,如果知道这些内容,可以跳过,不知道的可以点进去看看,咱们互相学习交流。

相关技术知识点博文地址
JDBC使用JDBC操作数据库的流程;JDBC标准带给我们什么好处https://blog.csdn.net/qq_36118179/article/details/114004161
SqlSession的statementId与Mapper接口SqlSession的创建以及主要AP;statementId与Mapper接口的原理与比较https://blog.csdn.net/qq_36118179/article/details/114004104

巴巴了这么多,到底想说明啥呢?使用过Mybatis的SqlSession原生接口(非整合Spring)的都知道,Mapper接口方式更简单且安全(Mybatis官方推荐的方式),Spring整合Mybatis底层也是使用类似这种方式,所以这也是为什么要学习Mapper代理对象产生过程的源码分析。下面我们进入今天的主题....

Mapper的获取

Mybatis源码解析--SqlSession中已经分析过,通过SqlSession的getMapper(Class<T> type)获取指定Mapper接口的代理对象,而且SqlSession是个接口,真正创建是DefaultSqlSession对象。

DefaultSqlSession

@Override
public <T> T getMapper(Class<T> type) {
    return getConfiguration().getMapper(type, this);
}

DefaultSqlSession#getMapper(Class<T> type)方法调用的Configuration#getMapper(Class<T> type, SqlSession sqlSession)方法,将当前SqlSession对象作为入参。

Configuration

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    return mapperRegistry.getMapper(type, sqlSession);
}

Configuration#getMapper(Class<T> type, SqlSession sqlSession)是从Configuration管理的MapperRegistry成员变量中获取

MapperRegistry

public class MapperRegistry {
​
  private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
​
  public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    // 通过Mapper接口获取对应的工厂对象
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
    // 工厂通过sqlSession创建对应mapper接口的代理对象
    return mapperProxyFactory.newInstance(sqlSession);
  }
}

MapperRegistry维护了一个Map<Class<?>, MapperProxyFactory<?>>,先从Map中获取到指定Mapper的代理工厂对象,然后通过代理工厂创建对应的代理对象并返回。说明getMapper方法每次都会生成新的代理对象

getMapper方法需要两个参数:

  • Class<T>:Mapper接口的Class对象

  • SqlSession:Mybatis对外暴露的访问数据库对象SqlSession(代表一次数据库连接)

时序图

Mapper的添加

通过上面的分析知道getMapper方法最终是通过MapperProxyFactory生成对应Mapper的代理对象,而MapperProxyFactory是从MapperRegistry维护的Map<Class<?>, MapperProxyFactory<?>>中获取的,该Map初始化是空的HashMap,既然能获取到MapperProxyFactory,那肯定是先放进去再获取,那我们就看一下添加过程

MapperRegistry

public <T> void addMapper(Class<T> type) {
    // 1.该Class是接口才会处理
    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<>(type));
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        parser.parse();
        loadCompleted = true;
      } finally {
        if (!loadCompleted) {
          knownMappers.remove(type);
        }
      }
    }
  }

通过入参Class对象构造了一个MapperProxyFactory,然后以Class对象为key,MapperProxyFactory为值存放Map中。结合Mapper获取的过程可以推测出Map的key就是Mapper接口的Class对象。

该方法除了上面的功能外,还对Mapper接口内的注解进行了解析。

Mapper代理对象的生成

Mapper与MapperProxyFactory的关系

通过对Mapper的获取流程分析得出SqlSession.getMapper(Class)方法最终是获取mapperProxyFactory.newInstance(sqlSession)`方法创建的Mapper代理对象。

通过对Mapper的添加流程分析得出通过每个Mapper的Class对象创建对应的代理工厂对象new MapperProxyFactory<>(type),所以Mapper与MapperProxyFactory是一对一关系,每个代理工厂只生产绑定Mapper的代理对象(在创建MapperProxyFactory时进行了绑定)。

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

代理对象的创建

搞清楚了Mapper与MapperProxyFactory的关系,那我们就集中火力看mapperProxyFactory.newInstance(sqlSession)是如何创建代理对象的。

public class MapperProxyFactory<T> {
​
  private final Class<T> mapperInterface;
  private final Map<Method, MapperMethodInvoker> methodCache = new ConcurrentHashMap<>();
  // 构造方法
  public MapperProxyFactory(Class<T> mapperInterface) {
    this.mapperInterface = mapperInterface;
  }
  // 创建代理对象
  public T newInstance(SqlSession sqlSession) {
    final MapperProxy<T> mapperProxy = new MapperPnuroxy<>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
  }
}

该方法就干了两件事:

  1. 创建MapperProxy对象,构造MapperProxy对象涉及到3个参数:

    • sqlSession是创建代理对象调用newInstance方法传进来的,该对象是Mybatis框架操作数据库的核心对象;

    • mapperInterface是构造MapperProxyFactory对象时通过构造方法传入,该参数是Mapper接口的Class对象,指定该工厂是生产该Mapper的代理对象;

    • methodCache初始为空的ConcurrentHashMap

  2. 调用重载方法newInstance(MapperProxy)创建代理对象

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

最终使用JDK动态代理生成Mapper接口的代理对象并返回。

代理逻辑分析

JDK动态代理创建的代理对象其代理逻辑是在InvocationHandler实现类的invoke方法中实现的,所以我们看看MapperProxy类的invoke方法。

MapperProxy#invoke

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try { 
      if (Object.class.equals(method.getDeclaringClass())) {
        // 如果是在Object类中定义的方法,则直接调用  
        return method.invoke(this, args);
      } else {
        // 否则就会走下面的逻辑  
        return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
  }

如果当前执行方法是在Object类中声明的,会直接调用。否则调用cachedInvoker(method).invoke(proxy, method, args, sqlSession)

MapperProxy#cachedInvoker

private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
      MapperMethodInvoker invoker = methodCache.get(method);
      if (invoker != null) {
        return invoker;
      }

      return methodCache.computeIfAbsent(method, m -> {
        if (m.isDefault()) {
          //如果是默认方法,创建 DefaultMethodInvoker 对象
          return new DefaultMethodInvoker(MethodHandle methodHandle);
        } else {
          //否则创建 PlainMethodInvoker 对象 
          return new PlainMethodInvoker(MapperMethod mapperMethod);
        }
      });
  }

首先会去methodCache中获取指定methodMapperMehodInvoker对象,如果有就返回;没有就根据当前方法是否是默认方法(JDK1.8新特性)创建对应的MapperMethodInvoker。再以method为key,MapperMethodInvoker为值存放到methodCache中。

methodCache是在MapperProxyFactory#newInstance(SqlSession sqlSession)方法中创建MapperProxy时需要3个参数:sqlSession, mapperInterface, methodCache其中methodCache初始化为new ConcurrentHashMap<>()传入

一般我们在Mapper接口中定义的方法都是非默认方法,所以我们主要看PlainMethodInvoker的内部实现

PlainMethodInvoker类

private static class PlainMethodInvoker implements MapperMethodInvoker {
    private final MapperMethod mapperMethod;

    public PlainMethodInvoker(MapperMethod mapperMethod) {
      super();
      this.mapperMethod = mapperMethod;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
      return mapperMethod.execute(sqlSession, args);
    }
  }

PlainMethodInvoker#invoke方法内部调用了MapperMethod#execute方法,而MapperMethod是通过构造传入的。所以我们主要关注MapperMethod对象的内部实现

MapperMethod#execute

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:
        // 逻辑太多忽略
        break;
      case FLUSH:
        result = sqlSession.flushStatements();
        break;
      default:
        throw new BindingException("Unknown execution method for: " + command.getName());
    }
    return result;
  }

execute方法内部根据不同的类型最终调用SqlSessionstatementId风格的API

到此我们可以得出结论:Mapper代理方式内部最终还是走的statementId风格的实现逻辑

存在的问题

通过上面的分析,会有以下两个问题存在:

  1. SqlSession是没有实现任何Mapper接口,所以没有办法通过method.invoke(target, args)来调用被代理者的实现逻辑,所以首先要解决的问题就是如何将Mapper接口的方法和SqlSession提供的API进行映射

  2. Mybatis源码解析--SqlSession中我们已经分析过statementId方式是需要一个唯一标识来获取MappedStatement对象,所以Mapper代理方式想要调用statementId方式的接口,就必须提供这个唯一标识。

如何解决

我们带着上面两个疑问来看看Mybatis是如何解决这些问题的。首先我们先看以下MapperMethod的构造过程。

MapperMethod

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

MapperMethod内部通过入参构造了两个对象

execute(SqlSession sqlSession, Object[] args)方法中可以看到sqlSession.insert(command.getName(), param)sqlSession.selectOne(command.getName(), param)类似这些代码,则说明唯一标识是通过辅助类SqlCommand command来处理的。说明这两个问题和两个辅助类紧密相关,下面我们就看一下这两个类的内部实现。

SqlCommand的创建

public static class SqlCommand {

    private final String name;
    private final SqlCommandType type;

    public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
      final String methodName = method.getName();
      final Class<?> declaringClass = method.getDeclaringClass();
      // 从configuration中获取MappedStatement
      MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass,
          configuration);
      // 下面操作会对自己两个属性(name和type)进行赋值
      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();
        type = ms.getSqlCommandType();
        if (type == SqlCommandType.UNKNOWN) {
          throw new BindingException("Unknown execution method for: " + name);
        }
      }
    }
  }

SqlCommand#resolveMappedStatement

private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName,
        Class<?> declaringClass, Configuration configuration) {
      // 拼装的statementId = Mapper接口全限定名 + 方法名
      String statementId = mapperInterface.getName() + "." + methodName;
      if (configuration.hasStatement(statementId)) {
        // 如果有,直接返回
        return configuration.getMappedStatement(statementId);
      } else if (mapperInterface.equals(declaringClass)) {
        return null;
      }
      // 走到这说明  configuration中没有该statementId对应的MappedStatement 且
      //           当前方法没有在该mapperInterface接口中声明(从父接口继承的方法)
      for (Class<?> superInterface : mapperInterface.getInterfaces()) {
        if (declaringClass.isAssignableFrom(superInterface)) {
          // 通过父接口再去找
          MappedStatement ms = resolveMappedStatement(superInterface, methodName,
              declaringClass, configuration);
          if (ms != null) {
            // 如果获取到 则返回
            return ms;
          }
        }
      }
      // 遍历了所有的接口都没有找到 则返回null
      return null;
    }

在构造方法中通过调用resolveMappedStatement方法获取MappedStatement对象,通过MappedStatementnametype两个成员属性赋值。在resolveMappedStatement方法中是通过Mapper接口的全限定名+方法名作为唯一标识获取MappedStatement对象。

注意:所以当使用Mapper代理对象方式配合mapper配置文件使用时,需要注意<mapper>标签的namespace属性必须是Mapper接口的全限定名,每个<select><update><insert><delete>标签的id属性必须是对应的方法名。

MethodSignature

public static class MethodSignature {

    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.returnsOptional = Optional.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;
    }

    public String getMapKey() {
      return mapKey;
    }

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

小结

SqlSession.getMapper(Class)是通过JDK的方式创建Mapper接口的代理对象,而代理对象内部是通过调用SqlSession接口中statementId风格的接口,由于SqlSession没有实现任何Mapper接口无法通过常见的method.invoke(target, args)调用目标对象。对于该问题Mybatis框架通过SqlCommandType和Mapper接口定义的返回值来推断调用SqlSession提供的statementId风格的API。

涉及的设计模式:代理模式

注意事项:当SqlSession.getMapper(Class)配合mapper配置文件时,需要保证<mapper>标签的namespace属性必须与对应Mapper接口的全限定名称保持一致且内部<selece><update><insert><delete>标签的id属性必须和Mapper内部声明的方法名保持一致。

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值