2024-01-20  阅读(0)
原文作者:墨家巨子@俏如来 原文地址: https://blog.csdn.net/u014494148/article/details/122327261

2021一路有你,2022我们继续加油!你的肯定是我最大的动力

博主在参加博客之星评比,点击链接 , https://bbs.csdn.net/topics/603957267 疯狂打Call!五星好评 感谢

前言

面试官:你说一下为什么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去执行查询。

202401201945495871.png

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

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

博主在参加博客之星评比,点击链接 , https://bbs.csdn.net/topics/603957267 疯狂打Call!五星好评 感谢


Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。

它的内容包括:

  • 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
  • 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
  • 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
  • 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
  • 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
  • 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
  • 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
  • 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw

目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:

想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询

同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。

阅读全文