Mybatis源码学习记录(Mapper接口篇)

前言

利用Mybatis框架,我们只要提供一个Mapper接口,定义好相应的方法,再利用XML文件的配合。就可以实现利用调用Mapper接口的方法来实现SQL语句的查询,这其中是如何实现的呢?本文我们将带着这个问题,结合源码来解答

代码示例

分析源码之前我们首先贴一下代码示例,这样对于分析源码会起到画龙点睛的作用

UserOrderMapper.java

@Mapper
public interface UserOrderMapper {

    // 根据用户手机号查询其下所有已完成订单列表
    List completedOrdersByUserPhone(@Param("phone") String phone);
}

UserOrderMapper.xml







    
    

UserOrderDo.java

public class UserOrderDo {
    private Long id;
    
    private BigDecimal orderAmount;
    
    private Date createTime;
    
    private Date PayTime;
    
    // 省略其他字段以及相应的getter/setter方法...
}

Service层调用代码示例

@Service
public class UserOrderServiceImpl {

    @Autowired
    private UserOrderMapper userOrderMapper;
    
    public List getCompletedUserOrderByPhone(String phone) {
        // 查询
        List completedOrders = userOrderMapper.completedOrdersByUserPhone(phone);
    
        // 处理查询结果返回...
    }
}

以上就是使用Mybatis框架的日常开发模式,我们带着本文需要解决的问题来看,为什么调用userOrderMapper.completedOrdersByUserPhone(phone)就可以直接查询到数据库中用户已完成订单列表了呢?

进入源码

Configuration.java

代表着Mybatis配置的 Configuration对象中持有一个MapperRegistry类型的mapperRegistry属性

public class Configuration { 

    protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
    
    public  void addMapper(Class type) {
        mapperRegistry.addMapper(type);
    }
    
    // 注册Mapper接口
    public void addMappers(String packageName) {
        mapperRegistry.addMappers(packageName);
    }
    
    // 获取指定Mapper的代理
    public  T getMapper(Class type, SqlSession sqlSession) {
        return mapperRegistry.getMapper(type, sqlSession);
    }
}

在Mybatis初始化阶段,会解析mybatis-config.xml文件,实例化出单例的Configuration对象,并调用其
addMappers方法将指定包下满足条件的所有Mapper接口都注册到MapperRegistry中(或者在解析mapper.xml文件阶段,以namaspace命名空间值,调用configuration.addMapper方法注册)

MapperRegistry.java

Mapper注册表,内部持有所有满足条件的Mapper接口的Mapper代理工厂实例集合,并以Mapper接口的Class名作为key,MapperProxyFactory作为value

public class MapperRegistry {

  private final Map, MapperProxyFactory> knownMappers = new HashMap, MapperProxyFactory>();
  
  // 注册指定包下,满足指定超类的所有Mapper接口
  public void addMappers(String packageName, Class superType) {
    ResolverUtil> resolverUtil = new ResolverUtil>();
    resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
    Set>> mapperSet = resolverUtil.getClasses();
    for (Class mapperClass : mapperSet) {
      // 遍历注册
      addMapper(mapperClass);
    }
  }
  
  // 注册Mapper接口
  public  void addMapper(Class type) {
    if (type.isInterface()) {
      if (hasMapper(type)) {
        throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
      }
      boolean loadCompleted = false;
      try {
        // 添加到knownMappers集合中
        knownMappers.put(type, new MapperProxyFactory(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);
        }
      }
    }
  }
  
  public  T getMapper(Class type, SqlSession sqlSession) {
    // 1. 先获取以Mapper的Class类型作为key对应的MapperProxyFactory实例
    final MapperProxyFactory mapperProxyFactory = (MapperProxyFactory) knownMappers.get(type);
    if (mapperProxyFactory == null) {
      throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    }
    try {
      // 2. 调用MapperProxyFactory的newInstance方法创建代理Mapper对象并返回
      return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
      throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
  }
  
  //省略其他属性方法... 

从这里可以看出Mybatis初始化阶段,就已经将所有满足条件的Mapper以接口Class作为key,MapperProxyFactory对象作为value存入到Map集合中了

接下来就是程序调用configuration.getMapper(...)方法获取代理Mapper对象,并注入到spring的bean容器

我们继续向下看这个Mapper代理对象到底是何方神圣?

MapperProxyFactory

一个Mapper代理的工厂类,负责Mapper接口代理对象的创建工作

public class MapperProxyFactory {
    // 持有需要创建代理的Mapper接口的类型
    private final Class mapperInterface;
    
    // 持有一个空的以Method为key,MapperMethod对象为value的Map集合
  private final Map methodCache = new ConcurrentHashMap();
  
    // 构造函数
    public MapperProxyFactory(Class mapperInterface) {
        this.mapperInterface = mapperInterface;
    }
    
    // 省略相应属性的get方法...
    
    protected T newInstance(MapperProxy mapperProxy) {
        return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
    }

    public T newInstance(SqlSession sqlSession) {
        // new了一个MapperProxy作为Mapper代理对象的handler
        final MapperProxy mapperProxy = new MapperProxy(sqlSession, mapperInterface, methodCache);
        return newInstance(mapperProxy);
    }
}

通过newInstance方法可以看到,实际上是利用JDK的动态代理创建了每个Mapper接口的一个代理类,其handler相关的逻辑交给了MapperProxy

MapperProxy

一个标准的InvocationHandler,不仅兼任代理handler的职责,还持有方法缓存Map以及Mybatis中重要的sqlSession对象

public class MapperProxy implements InvocationHandler, Serializable {

    // 持有属性
    // 重要的sqlSession
    private final SqlSession sqlSession;
    // Mapper接口的类型
    private final Class mapperInterface;
    // Mapper接口的方法缓存集合,以Method为key,MapperMethod实例为value
    private final Map 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);
        }
        // 重要的invoke逻辑来了,这里解释下,当我们调用Mapper代理对象的方法,比如(completedOrdersByUserPhone(...))时,会执行到这里,会执行cachedMapperMethod方法以获取缓存的MapperMethod实例,再将具体的执行逻辑交给MapperMethod处理
        final MapperMethod mapperMethod = cachedMapperMethod(method);
        return mapperMethod.execute(sqlSession, args);
    }

    // 尝试根据此次调用的方法作为key,获取已缓存的MapperMethod实例
    private MapperMethod cachedMapperMethod(Method method) {
        MapperMethod mapperMethod = methodCache.get(method);
        // 没有就new一个出来
        if (mapperMethod == null) {
          // 重要的就是这个new MapperMehtod的方法
          mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
          // new之后放入methodCache中缓存起来
          methodCache.put(method, mapperMethod);
        }
        return mapperMethod;
    }
}

代码分析到了这里,我们可以知道当我们在我们的service层利用@autowired注解拿到Mapper代理对象之后,第一次调用它的某个(比如查询)方法时,会进入MapperProxyinvoke方法中,并调用new出来的MapperMethodexecute方法执行真正的SQL调用,之后再调用同一个查询方法时,就不会再new MapperMethod实例了,而是从mehtodCache这个map缓存中获取,以提高性能,接下来我们继续往下,看看MapperMethod做了什么工作?

MapperMethod

每个Mapper接口中定义的查询/删除/新增/更新方法都对应一个MapperMethod实例,该实例持有两个重要的内部类SqlCommand和MehtodSignature属性,通过这两个内部类,就可以囊括所有查询SQL之前所需的各种基础信息

public class MapperMethod {

  // 持有SQL命令相关,主要两个属性一个name一个type
  private final SqlCommand command;
  // Java方法签名相关
  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 {
    // name属性并不是方法名哦,而是MappedStatement实例的id属性,至于怎么获取这个值,下面会分析
    private final String name;
    // 对应MappedStatement实例的sqlCommandType属性值,是一个枚举类型
    private final SqlCommandType type;
    
    // 构造函数,就是想办法初始化上面的两个属性
    public SqlCommand(Configuration configuration, Class mapperInterface, Method method) {
      final String methodName = method.getName();
      final Class declaringClass = method.getDeclaringClass();
      // 根据方法名以及方法所声明的class的类型以及Mapper代理类接口的类型去获取configuration中缓存的MappedStatement实例
      MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass,
          configuration);
      // 若MappedStatement实例不存在,则判断方法是否含有@Flush注解  
      if (ms == null) {
        if (method.getAnnotation(Flush.class) != null) {
          // 存在该注解,则将name置为null,type置为SqlCommandType.FLUSH
          name = null;
          type = SqlCommandType.FLUSH;
        } else {
          // 否则就抛出异常,找不到方法对应的statement实例
          throw new BindingException("Invalid bound statement (not found): "
              + mapperInterface.getName() + "." + methodName);
        }
      } else {
        // 找到了对应的MappedStatement实例,则取其id属性作为name,sqlCommandType属性作为type
        name = ms.getId();
        type = ms.getSqlCommandType();
        // 注意这里的sqlCommandType属性值若为UNKNOWN类型,则依赖会抛错
        if (type == SqlCommandType.UNKNOWN) {
          throw new BindingException("Unknown execution method for: " + name);
        }
      }
    }
    
    // 下面我们重点看其是如何根据Method就能找到对应的MappedStatement对象的
    private MappedStatement resolveMappedStatement(Class mapperInterface, String methodName,
        Class declaringClass, Configuration configuration) {
      // 拼接Mapper接口名.方法名    
      String statementId = mapperInterface.getName() + "." + methodName;
      if (configuration.hasStatement(statementId)) {
        // 从configuration中根据上面拼接后的作为statementId查询
        return configuration.getMappedStatement(statementId);
      // 当前方法申明的类型就是Mapper接口的类型,说明找不到,返回null    
      } else if (mapperInterface.equals(declaringClass)) {
        return null;
      }
      // 说明当前调用的方法可能是父类Mapper接口中定义的,那就递归调用直到找到对应的MappedStatement对象
      for (Class superInterface : mapperInterface.getInterfaces()) {
        if (declaringClass.isAssignableFrom(superInterface)) {
          MappedStatement ms = resolveMappedStatement(superInterface, methodName,
              declaringClass, configuration);
          if (ms != null) {
            return ms;
          }
        }
      }
      return null;
    }

源码分析到这里我们可以看出Mybatis中Mapper接口支持多层继承关系的,至此整个SqlCommand对象实例化完成,该有的属性也已经赋值完毕,接下来我们看MethodSignature这个内部类的实例化过程

public static class MethodSignature {
    
    // 是否返回Map,是否返回Void,是否返回Collection,是否返回Cursor以及如果指定了resultHandler,那么对应的参数index是多少,类似的还有rowBounds等
    private final boolean returnsMany;
    private final boolean returnsMap;
    private final boolean returnsVoid;
    private final boolean returnsCursor;
    private final Class returnType;
    private final String mapKey;
    private final Integer resultHandlerIndex;
    private final Integer rowBoundsIndex;
    
    // 重要的属性来了,重点看这个属性的初始化过程
    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);
    }

MethodSignature内部类的实例化中有一个重要的属性ParamNameResolver对象,它会事先将Method的方法中的参数按照一定的规则先解析好,保存起来,将来执行SQL之前可以根据代理invoke方法中传入的Object[] args参数,生成真正SQL语句执行时需要的(paranName,paramValue),以方便使用

ParamNameResolver.java

参数名称解析器,内部持有一个SortedMap names属性,利用Java的反射,事先将Mapper代理对象的Method方法的参数解析出来缓存起来,以备将来SQL执行之前使用

public class ParamNameResolver {

  private static final String GENERIC_NAME_PREFIX = "param";
  
  // key为参数列表的index,value则为参数的name
  // 参数name是从@Param注解获得的,当没有指定该注解时,则以String形式的index作为value,另外注意的是当参数中存在特殊参数(`RowBounds`和`ResultHandler`时,会跳过特殊参数)
  // aMethod(@Param("M") int a, @Param("N") int b)  --> {{0, "M"}, {1, "N"}}
  // aMethod(int a, int b) --> {{0, "0"}, {1, "1"}}
  // aMethod(int a, RowBounds rb, int b) --> {{0, "0"}, {2, "1"}}
  private final SortedMap names;

  private boolean hasParamAnnotation;
  
  // 构造函数
  public ParamNameResolver(Configuration config, Method method) {
    final Class[] paramTypes = method.getParameterTypes();
    final Annotation[][] paramAnnotations = method.getParameterAnnotations();
    final SortedMap map = new TreeMap();
    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]) {
        if (annotation instanceof Param) {
          hasParamAnnotation = true;
          name = ((Param) annotation).value();
          break;
        }
      }
      if (name == null) {
        // @Param was not specified.
        if (config.isUseActualParamName()) {
          name = getActualParamName(method, paramIndex);
        }
        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的执行方法之前会调用该方法,以将代理invoke方法得到的实际参数数组根据names属性中保存的参数名集合转为Sql执行需要的参数名对应参数值形式
  // 注意:
  // 1. 当参数列表为0个时,返回null
  // 2. 当参数列表为1个且没有@Param注解时,则直接返回实际的参数值(非key,valueMap的形式)
  // 3. 以上二者都不是的情况下,返回一个Map形式的以name作为key,实际传递的参数值作为value,需要注意的一点是多了一组默认参数以"param1, param2..."作为key的entry对象哦,也就是说我们可以用这些key在xml文件中直接写#{param1}, #{param2}来获取实际传递的value
  public Object getNamedParams(Object[] args) {
    final int paramCount = names.size();
    if (args == null || paramCount == 0) {
      return null;
    } else if (!hasParamAnnotation && paramCount == 1) {
      return args[names.firstKey()];
    } else {
      final Map param = new ParamMap();
      int i = 0;
      for (Map.Entry entry : names.entrySet()) {
        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;
    }
  }

这里简单举个例子,可以将ParamNameResolver类的作用看的更清楚

还是以上面的List completedOrdersByUserPhone(@Param("phone") String phone);为例

当我们completedOrdersByUserPhone方法对应的MappedMehtod实例创建完成后,也即对应的MethodSignature.ParamNameResolver实例也创建完成,此时ParamNameResolver实例所持有的names属性值为:

names = {{0, "phone"}} // 0代表参数下标index,"phone"代表参数的name

当我们调用实际查询SQL前,假设传递给查询方法的参数phone的值为"15800000000", 经过Mapper代理,调用invoke方法执行查询,最终在调用Sqlsession的查询方法会调用MethodSignature.convertArgsToSqlCommandParam(args)方法进行参数的转换(将args转为SqlCommand可以使用的参数对象), 会继而转到ParamNameResolver.getNamedParams方法处理,最终经过该方法处理后得到的Object对象如下:

{{"phone", "15800000000"}, {"param1", "15800000000"}}

这样在继续调用sqlSession的执行方法时就可以直接取出对应name的value值啦,也就是为什么我们在xml文件中可以写#{phone}就可以被替换成实际传递的手机号的原因

总结

为避免篇幅过于长,本文内容会直接在下文中继续分析,下文SqlSession的调用执行源码。

你可能感兴趣的:(Mybatis源码学习记录(Mapper接口篇))