五.吃透Mybatis源码-面试官问我mapper映射器是如何工作的

简介: 面试官:你说一下为什么Mapper映射器是一个interface,而我们却可以直接调用它的方法,还能执行对应的SQL。额…也许你不知道,也许你知道个大概,本篇文章将带你从源码的角度彻彻底底理解Mybatis的Mapper映射器

前言

面试官:你说一下为什么Mapper映射器是一个interface,而我们却可以直接调用它的方法,还能执行对应的SQL。额...也许你不知道,也许你知道个大概,本篇文章将带你从源码的角度彻彻底底理解Mybatis的Mapper映射器

Mapper的注册

我们在执行Mybatis的时候可以使用 sqlSession.selectOne("cn.whale.mapper.StudentMapper.selectById",1L)这种最原生的方式,这种方式的弊端是太麻烦,每次都要去拼接 statementId。所以我们在项目中通常是使用Mapper映射器来执行。如下:

StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
Student student = mapper.selectById(1L);

下面我们就来分析一下通过Mapper映射器是如何工作的。在之前的文章中我们有分析到,在SqlSessionFactoryBuilder.buid的时候会通过XMLConfigBuilder对mybatis-config.xml进行解析,其中有一个步骤就是对Mapper.xml的解析 ,如:<mapper resource="mapper/StudentMapper.xml"/> 。代码直接来到XMLConfigBuilder#mapperElement

private void mapperElement(XNode parent) throws Exception {
   
   
    if (parent != null) {
   
   
      for (XNode child : parent.getChildren()) {
   
   
        if ("package".equals(child.getName())) {
   
   
          //package配置方式
          String mapperPackage = child.getStringAttribute("name");
          configuration.addMappers(mapperPackage);
        } else {
   
   
          //拿到配置的资源 如: mapper/studentMapper.xml
          String resource = child.getStringAttribute("resource");
          String url = child.getStringAttribute("url");
          String mapperClass = child.getStringAttribute("class");
          if (resource != null && url == null && mapperClass == null) {
   
   
            ErrorContext.instance().resource(resource);
            //把资源加载为流
            InputStream inputStream = Resources.getResourceAsStream(resource);
            //mapper.xml的解析器
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
            //【重点】解析mapper.xml
            mapperParser.parse();
          } else if (resource == null && url != null && mapperClass == null) {
   
   
            ErrorContext.instance().resource(url);
            InputStream inputStream = Resources.getUrlAsStream(url);
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
            mapperParser.parse();
          } else if (resource == null && url == null && mapperClass != null) {
   
   
            Class<?> mapperInterface = Resources.classForName(mapperClass);
            configuration.addMapper(mapperInterface);
          } else {
   
   
            throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
          }
        }
      }
    }
  }

上面代码会加载mapper.xml文件然后使用XMLMapperBuilder去解析xml,代码来到
org.apache.ibatis.builder.xml.XMLMapperBuilder#parse

 public void parse() {
   
   
    if (!configuration.isResourceLoaded(resource)) {
   
   
    //解析mapper.xml中的  <cache ,<resultMap ,<parameterMap,<sql ,select|insert|update|delete 等元素
      configurationElement(parser.evalNode("/mapper"));
      configuration.addLoadedResource(resource);
      //【重点】我们的重点在这,为当前namespace绑定mapper接口
      bindMapperForNamespace();
    }

    parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();
  }

我们看到上面解析完mapper.xml后执行了这样一个方法bindMapperForNamespace,看名字能猜到他的作用是为当前namespace绑定mapper接口

private void bindMapperForNamespace() {
   
   
    //拿到namespace
    String namespace = builderAssistant.getCurrentNamespace();
    if (namespace != null) {
   
   
      Class<?> boundType = null;
      try {
   
   
        //拿到namespace对应的mapper接口的class:比如:cn.whale.mapper.StudentMapper
        boundType = Resources.classForName(namespace);
      } catch (ClassNotFoundException e) {
   
   
        //ignore, bound type is not required
      }
      if (boundType != null) {
   
   
        //判断configuration中是否包含 当前 mapper
        if (!configuration.hasMapper(boundType)) {
   
   

          // Spring may not know the real resource name so we set a flag
          // to prevent loading again this resource from the mapper interface
          // look at MapperAnnotationBuilder#loadXmlResource
          configuration.addLoadedResource("namespace:" + namespace);
          //把Mapper接口添加到configuration中
          configuration.addMapper(boundType);
        }
      }
    }
  }

我们看到方法中拿到 namespace 后转换为 Class 。然后调用 configuration.addMapper把Mapper接口添加到configuration中,继续跟进org.apache.ibatis.session.Configuration#addMapper

public class Configuration {
   
   
//mapper的注册器
protected final MapperRegistry mapperRegistry = new MapperRegistry(this);

public <T> void addMapper(Class<T> type) {
   
   
    //把mapper的class添加到 MapperRegistry
    mapperRegistry.addMapper(type);
  }

mapper的class被添加到了Configuration#mapperRegistry中,那么mapperRegistry又是个什么东西呢,见org.apache.ibatis.binding.MapperRegistry#addMapper

public class MapperRegistry {
   
   

  private final Configuration config; 
  //保存Mapper接口的真正结构是一个HashMap
  private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>();

  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 {
   
   
        //【重点】 ,以mapper的class为key, 把mapper的class封装到MapperProxyFactory作为Value。
        //存储到knownMappers 一个HashMap中。
        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.
        //映射器注释生成器,Mapper注解上的注解
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        //该方法会解析Mapper接口上的注解,比如: @Select("select * from student")
        //然后会把这些SQL语句封装成MappedStatement
        parser.parse();
        loadCompleted = true;
      } finally {
   
   
        if (!loadCompleted) {
   
   
          knownMappers.remove(type);
        }
      }
    }
  }

我们重点看这行代码,knownMappers.put(type, new MapperProxyFactory<T>(type)); 以mapper的class为key, 把mapper的class封装到MapperProxyFactory作为Value。存储到knownMappers 一个HashMap中,MapperProxyFactory我们可以看做是mapper的代理工厂,它封装了mapperInterface成class和接口中的方法,同时提供了创建mapper接口代理的方法。

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) {
   
   
    //反射创建代理
    final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
  }

}

到了到这里就差不多了,总结一下,就是在sqlSessionFactoryBuilder.buid 时候就会解析mapper.xml,然后根据namespace找到对应的mapper接口,把mapper接口的clazz封装到一个 MapperProxyFactory 代理工厂里面,然后以mapper的class为key把MapperProxyFactory存储到MapperRegistry中的 knownMappers 属性中。而MapperRegistry本身又是存储在 Configuration对象中。

Mapper的代理

上面我们分析了Mapper映射器的注册流程,我们接下来分析它的代理。入口就是我们执行sqlSession.getMapper(StudentMapper.class);的时候,该方法会调用org.apache.ibatis.session.defaults.DefaultSqlSession#getMapper获取Mapper

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

DefaultSqlSession#getMapper又调用了org.apache.ibatis.session.Configuration#getMapper,最终从mapperRegistry中获取Mpaper见:org.apache.ibatis.session.Configuration#getMapper

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

下面是org.apache.ibatis.binding.MapperRegistry#getMapper获取Mapper的方法

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
   
   
   //先从knownMappers拿到Mapper,Mapper被封装到MapperProxyFactory代理工厂年
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
    if (mapperProxyFactory == null) {
   
   
      throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    }
    try {
   
   
     //【重要重要】 这里在调用MapperProxyFactory#newInstance创建Mapper的实例了
      return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
   
   
      throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
  }

MapperRegistry#getMapper方法中从 knownMappers 中获取到Mapper,mapper被封装成了MapperProxyFactory,然后调用mapperProxyFactory.newInstance创建Mapper的代理类,见:org.apache.ibatis.binding.MapperProxyFactory#newInstance(org.apache.ibatis.session.SqlSession)

  public T newInstance(SqlSession sqlSession) {
   
   
    final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
  }

 protected T newInstance(MapperProxy<T> mapperProxy) {
   
   
    //JDK动态代理 , mapperProxy 是一个 InvocationHandler
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] {
   
    mapperInterface }, mapperProxy);
  }

MapperProxyFactory#newInstance方法中依然是使用了JDK动态代理为Mapper接口创建代理类,需要注意的是 MapperProxy ,这个类,它是一个 InvocationHandler ,也就是说当Mapper接口的方法被调用(其实是代理类被调用),请求会被 InvocationHandler#invoke 拦截。

到这里,sqlSession.getMapper方法的源码分析完了,其实就是从Configuration的MapperRegistry中取出封装了Mapper接口的MapperProxyFactory代理工厂类,然后执行 mapperProxyFactory.newInstance为接口生成代理类。

Mapper接口的执行

上面我们知道了,Mapper接口是通过 MapperProxyFactory.newInstance 生成的代理,当Mapper的方法被调用的时候就会被 MapperProxy#invoke 拦截器,见:org.apache.ibatis.binding.MapperProxy

public class MapperProxy<T> implements InvocationHandler, Serializable {
   
   

  private static final long serialVersionUID = -6424540398559729838L;
  private final SqlSession sqlSession;
  //mapper接口的class
  private final Class<T> mapperInterface;
  //方法的缓存
  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;
  }
    //invoke会拦截mapper的方法执行
  @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,如果没有就会把Method封装为MapperMethod,写入缓存
    //在MapperMethod的构造器中会把  mapperInterface.getName() + "." + methodName; 即:namespace加上方法名作为 statementId 
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    //[重点]执行mapper方法了
    return mapperMethod.execute(sqlSession, args);
  }

上面invoke方法中会把method封装为mapperMethod,在MapperMethod的构造器中会以mapperInterface.getName() + "." + methodName得到statementId,然后会把MapperMethod进行缓存,然后执行 org.apache.ibatis.binding.MapperMethod#execute

public class MapperMethod {
   
   

  private final SqlCommand command;
  private final MethodSignature method;

  public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
   
   
  //处理SQL命令,SqlCommand的name属性就是: mapperInterface.getName() + "." + methodName
    this.command = new SqlCommand(config, mapperInterface, method);
    this.method = new MethodSignature(config, mapperInterface, method);
  }

public Object execute(SqlSession sqlSession, Object[] args) {
   
   
    Object result;
    //判断类型
    switch (command.getType()) {
   
   
    //是执行insert操作
      case INSERT: {
   
   
      Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.insert(command.getName(), param));
        break;
      }
      //是执行update操作
      case UPDATE: {
   
   
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.update(command.getName(), param));
        break;
      }
      //是执行delete操作
      case DELETE: {
   
   
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.delete(command.getName(), param));
        break;
      }
      //是执行select操作
      case SELECT:
        if (method.returnsVoid() && method.hasResultHandler()) {
   
   
        //有指定结果处理器
          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);
          //最终调用了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;
  }

在 MapperMethod构造器中会创建一个SqlCommand , 根据 mapperInterface.getName() + "." + methodName 得到statementid,作为 SqlCommand的name 。当方法被执行,也就是execute被执行,会根据执行的SQL的类型做不同的处理,最终还是会调用SqlSession#selectOne方法去执行SQL。而statementid就是 mapperInterface.getName() + "." + methodName

总结

到这里就分析的差不多了我们总结一下整体流程

  1. sqlSessionFactoryBuilder.buid 的时候就会解析mybatis-config.xml ,然后解析 mapper.xml,然后根据namespace找到对应的mapper接口,把mapper接口的clazz封装到一个 MapperProxyFactory 代理工厂里面,然后以mapper的class为key把MapperProxyFactory存储到MapperRegistry中的 knownMappers 属性中。而MapperRegistry本身又是存储在 Configuration对象中。
  2. 当执行 sqlSession.getMapper的时候,就从Configuration的MapperRegistry中取出封装了Mapper接口的MapperProxyFactory代理工厂类,然后执行 mapperProxyFactory.newInstance为接口生成代理类。
  3. 最后在执行mapper接口的方法的时候,请求会被MapperProxy#invoke方法拦截器,在该方法中会把Method封装成MapperMethod后缓存,然后再执行MapperMethod#execute 。最终以 mapperInterface.getName() + "." + methodName为 statement ,调用sqlSession去执行查询。

在这里插入图片描述

所以最开始的面试题你会答了吗?

文章结束,喜欢就给我去点个五星好评吧。2021一路有你,2022我们继续加油!你的肯定是我最大的动力

相关文章
|
1月前
|
存储 安全 Java
面试题:用过ThreadLocal吗?ThreadLocal是在哪个包下的?看过ThreadLocal源码吗?讲一下ThreadLocal的get和put是怎么实现的?
字节面试题:用过ThreadLocal吗?ThreadLocal是在哪个包下的?看过ThreadLocal源码吗?讲一下ThreadLocal的get和put是怎么实现的?
37 0
|
1月前
|
SQL 缓存 Java
Mybatis面试题
Mybatis面试题
|
10天前
|
SQL Java 数据库连接
15:MyBatis对象关系与映射结构-Java Spring
15:MyBatis对象关系与映射结构-Java Spring
30 4
|
13天前
|
SQL Java 数据库连接
一文细说Mybatis八大核心源码
以上 是V哥给大家整理的8大核心组件的全部内容,为什么说选择 Java 就是选择未来,真正爱 Java 的人,一定喜欢深入研究,学习源码只是第一步,要有一杆子捅到操作系统才够刺激。
|
16天前
|
SQL Java 数据库连接
【Mybatis】深入学习MyBatis:概述、主要特性以及配置与映射
【Mybatis】深入学习MyBatis:概述、主要特性以及配置与映射
【Mybatis】深入学习MyBatis:概述、主要特性以及配置与映射
|
17天前
|
SQL 缓存 Java
|
18天前
|
Java 调度
Java面试必考题之线程的生命周期,结合源码,透彻讲解!
Java面试必考题之线程的生命周期,结合源码,透彻讲解!
42 1
|
26天前
|
XML Java 数据库连接
java对象有集合mybatis如何映射
java对象有集合mybatis如何映射
19 4
|
1月前
|
XML Java 数据库连接
探秘MyBatis:手写Mapper代理的源码解析与实现
探秘MyBatis:手写Mapper代理的源码解析与实现
21 1
|
1月前
|
SQL Java 数据库连接
MyBatis精髓揭秘:Mapper代理实现的黑盒探索
MyBatis精髓揭秘:Mapper代理实现的黑盒探索
23 1