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

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 面试官:你说一下为什么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我们继续加油!你的肯定是我最大的动力

相关文章
|
7天前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
26 2
|
3月前
|
JavaScript 前端开发
【Vue面试题二十五】、你了解axios的原理吗?有看过它的源码吗?
这篇文章主要讨论了axios的使用、原理以及源码分析。 文章中首先回顾了axios的基本用法,包括发送请求、请求拦截器和响应拦截器的使用,以及如何取消请求。接着,作者实现了一个简易版的axios,包括构造函数、请求方法、拦截器的实现等。最后,文章对axios的源码进行了分析,包括目录结构、核心文件axios.js的内容,以及axios实例化过程中的配置合并、拦截器的使用等。
【Vue面试题二十五】、你了解axios的原理吗?有看过它的源码吗?
|
17天前
|
SQL 缓存 Java
【详细实用のMyBatis教程】获取参数值和结果的各种情况、自定义映射、动态SQL、多级缓存、逆向工程、分页插件
本文详细介绍了MyBatis的各种常见用法MyBatis多级缓存、逆向工程、分页插件 包括获取参数值和结果的各种情况、自定义映射resultMap、动态SQL
【详细实用のMyBatis教程】获取参数值和结果的各种情况、自定义映射、动态SQL、多级缓存、逆向工程、分页插件
|
3月前
|
JavaScript 前端开发
【Vue面试题二十七】、你了解axios的原理吗?有看过它的源码吗?
文章讨论了Vue项目目录结构的设计原则和实践,强调了项目结构清晰的重要性,提出了包括语义一致性、单一入口/出口、就近原则、公共文件的绝对路径引用等原则,并展示了单页面和多页面Vue项目的目录结构示例。
|
1月前
|
SQL Java 数据库连接
mybatis使用四:dao接口参数与mapper 接口中SQL的对应和对应方式的总结,MyBatis的parameterType传入参数类型
这篇文章是关于MyBatis中DAO接口参数与Mapper接口中SQL的对应关系,以及如何使用parameterType传入参数类型的详细总结。
35 10
|
2月前
|
设计模式 Java 关系型数据库
【Java笔记+踩坑汇总】Java基础+JavaWeb+SSM+SpringBoot+SpringCloud+瑞吉外卖/谷粒商城/学成在线+设计模式+面试题汇总+性能调优/架构设计+源码解析
本文是“Java学习路线”专栏的导航文章,目标是为Java初学者和初中高级工程师提供一套完整的Java学习路线。
420 37
|
2月前
|
SQL XML Java
mybatis复习04高级查询 一对多,多对一的映射处理,collection和association标签的使用
文章介绍了MyBatis中高级查询的一对多和多对一映射处理,包括创建数据库表、抽象对应的实体类、使用resultMap中的association和collection标签进行映射处理,以及如何实现级联查询和分步查询。此外,还补充了延迟加载的设置和用法。
mybatis复习04高级查询 一对多,多对一的映射处理,collection和association标签的使用
|
3月前
|
SQL Java 数据库连接
Mybatis系列之 Error parsing SQL Mapper Configuration. Could not find resource com/zyz/mybatis/mapper/
文章讲述了在使用Mybatis时遇到的资源文件找不到的问题,并提供了通过修改Maven配置来解决资源文件编译到target目录下的方法。
Mybatis系列之 Error parsing SQL Mapper Configuration. Could not find resource com/zyz/mybatis/mapper/
|
2月前
|
SQL XML Java
mybatis :sqlmapconfig.xml配置 ++++Mapper XML 文件(sql/insert/delete/update/select)(增删改查)用法
当然,这些仅是MyBatis功能的初步介绍。MyBatis还提供了高级特性,如动态SQL、类型处理器、插件等,可以进一步提供对数据库交互的强大支持和灵活性。希望上述内容对您理解MyBatis的基本操作有所帮助。在实际使用中,您可能还需要根据具体的业务要求调整和优化SQL语句和配置。
45 1
|
1月前
|
Java 数据库连接 Maven
mybatis使用一:springboot整合mybatis、mybatis generator,使用逆向工程生成java代码。
这篇文章介绍了如何在Spring Boot项目中整合MyBatis和MyBatis Generator,使用逆向工程来自动生成Java代码,包括实体类、Mapper文件和Example文件,以提高开发效率。
112 2
mybatis使用一:springboot整合mybatis、mybatis generator,使用逆向工程生成java代码。
下一篇
无影云桌面