面试官:为什么Mybatis就能直接调用userMapper接口的方法?

先上案例代码,这样大家可以更加熟悉是如何使用的,看过Mybatis系列的小伙伴,对这段代码差不多都可以背下来了。

哈哈~,有点夸张吗?不夸张的,就这行代码。

    public class MybatisApplication {
        public static final String URL = "jdbc:mysql://localhost:3306/mblog";
        public static final String USER = "root";
        public static final String PASSWORD = "123456";

        public static void main(String[] args) {
            String resource = "mybatis-config.xml";
            InputStream inputStream = null;
            SqlSession sqlSession = null;
            try {
                inputStream = Resources.getResourceAsStream(resource);
                SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
                sqlSession = sqlSessionFactory.openSession();
                //今天主要这行代码
                UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
                System.out.println(userMapper.selectById(1));

            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                sqlSession.close();
            }
        }

看源码有什么用?

image.png

通过源码的学习,我们可以收获Mybatis的核心思想和框架设计,另外还可以收获设计模式的应用。

Mybatis配置文件解析到获取SqlSession,下面我们来分析从SqlSession到userMapper:

     UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

已经知道了这里的sqlSession使用的是默认实现类DefaultSqlSession。所以我们直接进入DefaultSqlSession的getMapper方法。

    //DefaultSqlSession中  
    private final Configuration configuration;
    //type=UserMapper.class
    @Override
    public <T> T getMapper(Class<T> type) {
      return configuration.getMapper(type, this);
    }

这里有三个问题:

image.png

问题1:getMapper返回的是个什么对象?

上面可以看出,getMapper方法调用的是Configuration中的getMapper方法。然后我们进入Configuration中

    //Configuration中  
    protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
    ////type=UserMapper.class
    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
        return mapperRegistry.getMapper(type, sqlSession);
    }

这里也没做什么,继续调用MapperRegistry中的getMapper:

    //MapperRegistry中
    public class MapperRegistry {
      //主要是存放配置信息
      private final Configuration config;
      //MapperProxyFactory 的映射
      private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();

      //获得 Mapper Proxy 对象
      //type=UserMapper.class,session为当前会话
      public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
        //这里是get,那就有add或者put
        final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
        if (mapperProxyFactory == null) {
          throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
        }
       try {
          //创建实例
          return mapperProxyFactory.newInstance(sqlSession);
        } catch (Exception e) {
          throw new BindingException("Error getting mapper instance. Cause: " + e, e);
        }
      }

      //解析配置文件的时候就会调用这个方法,
      //type=UserMapper.class
      public <T> void addMapper(Class<T> type) {
        // 判断 type 必须是接口,也就是说 Mapper 接口。
        if (type.isInterface()) {
            //已经添加过,则抛出 BindingException 异常
            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));
                //创建 MapperAnnotationBuilder 对象,解析 Mapper 的注解配置
                MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
                parser.parse();
                //标记加载完成
                loadCompleted = true;
            } finally {
                //若加载未完成,从 knownMappers 中移除
                if (!loadCompleted) {
                    knownMappers.remove(type);
                }
            }
        }
    }
    }

MapperProxyFactory对象里保存了mapper接口的class对象,就是一个普通的类,没有什么逻辑。

在MapperProxyFactory类中使用了两种设计模式:

  1. 单例模式methodCache(注册式单例模式)。
  2. 工厂模式getMapper()。

继续看MapperProxyFactory中的newInstance方法。

    public class MapperProxyFactory<T> {
      private final Class<T> mapperInterface;
      private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<>();

      public MapperProxyFactory(Class<T> mapperInterface) {
        this.mapperInterface = mapperInterface;
      }
     public T newInstance(SqlSession sqlSession) {
      //创建MapperProxy对象
      final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
      return newInstance(mapperProxy);
    }
    //最终以JDK动态代理创建对象并返回
     protected T newInstance(MapperProxy<T> mapperProxy) {
        return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
    }
    }

从代码中可以看出,依然是稳稳地基于 JDK Proxy 实现的,而 InvocationHandler 参数是 MapperProxy 对象。

    //UserMapper 的类加载器
    //接口是UserMapper
    //h是mapperProxy对象
    public static Object newProxyInstance(ClassLoader loader,
                                              Class<?>[] interfaces,
                                           InvocationHandler h){
    }

问题2:为什么就可以调用他的方法?

上面调用newInstance方法时候创建了MapperProxy对象,并且是当做newProxyInstance的第三个参数,所以MapperProxy类肯定实现了InvocationHandler。

进入MapperProxy类中:

    //果然实现了InvocationHandler接口
    public class MapperProxy<T> implements InvocationHandler, Serializable {

      private static final long serialVersionUID = -6424540398559729838L;
      private final SqlSession sqlSession;
      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;
      }
      //调用userMapper.selectById()实质上是调用这个invoke方法
      @Override
      public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
          //如果是Object的方法toString()、hashCode()等方法  
          if (Object.class.equals(method.getDeclaringClass())) {
            return method.invoke(this, args);
          } else if (method.isDefault()) {
            //JDK8以后的接口默认实现方法  
            return invokeDefaultMethod(proxy, method, args);
          }
        } catch (Throwable t) {
          throw ExceptionUtil.unwrapThrowable(t);
        }
        //创建MapperMethod对象
        final MapperMethod mapperMethod = cachedMapperMethod(method);
        //下一篇再聊
        return mapperMethod.execute(sqlSession, args);
      }
    }

也就是说,getMapper方法返回的是一个JDK动态代理对象(类型是$Proxy+数字)。这个代理对象会继承Proxy类,实现被代理的接口UserMpper,里面持有了一个MapperProxy类型的触发管理类。

当我们调用UserMpper的方法时候,实质上调用的是MapperProxy的invoke方法。

userMapper=$Proxy6@2355。
image.png

为什么要在MapperRegistry中保存一个工厂类?

原来他是用来创建并返回代理类的。这里是代理模式的一个非常经典的应用。

image.png

MapperProxy如何实现对接口的代理?

JDK动态代理

我们知道,JDK动态代理有三个核心角色:

  • 被代理类(即就是实现类)
  • 接口
  • 实现了InvocationHanndler的触发管理类,用来生成代理对象。

被代理类必须实现接口,因为要通过接口获取方法,而且代理类也要实现这个接口。

image.png

而Mybatis中并没有Mapper接口的实现类,怎么被代理呢?它忽略了实现类,直接对Mapper接口进行代理。

MyBatis动态代理:

在Mybatis中,JDK动态代理为什么不需要实现类呢?

image.png

请看下面这张图:

最后返回的userMapper就是MapperProxyFactory的创建的代理对象,然后这个对象中包含了MapperProxy对象,

问题3:到底是怎么根据Mapper.java找到Mapper.xml的?

最后我们调用userMapper.selectUserById(),本质上调用的是MapperProxy的invoke()方法。

请看下面这张图:

image.png

如果根据(接口+方法名找到Statement ID ),这个逻辑在InvocationHandler子类(MapperProxy类)中就可以完成了,其实也就没有必要在用实现类了。

image.png

总结

本文中主要是讲getMapper方法,该方法实质上是获取一个JDK动态代理对象(类型是Proxy+数字),这个代理类会继承MapperProxy类,实现被代理的接口UserMapper,并且里面持有一个MapperProxy类型的触发管理类。这里我们就拿到代理类了,后面我们就可以使用这个代理对象进行方法调用。

问题涉及到的设计模式:

  1. 代理模式。
  2. 工厂模式。
  3. 单例模式。

整个流程图:


image.png
全部评论

相关推荐

求问!考研下岸,打算参加春招,我这个bg能进啥厂,或者需要搞点深度项目再投吗
Java抽象带篮子_...:直接海投,可以看看我的考研失利速成冲春招贴,里面详细写了简历怎么写,学哪些项目可以速成
点赞 评论 收藏
分享
一共一个小时,面试难度以及自己的回答算是最近的面试压力比较大的,实习问了30分钟,中间穿插八股。1.redis数据结构2.redis持久化机制3.mysql索引底层4.聚簇索引与非聚簇索引5.索引优化6.索引失效7.mysql执行一条sql8.那么多索引mysql怎么选(不会)9.tcp与udp区别10.tcp为什么可靠11.消息队列作用12.kafka怎么保证消息有序性13.mcp是什么?14.skills是什么?15.jvm内存分配与回收过程(我讲了从创建对象到判断垃圾对象到垃圾回收我全说了一遍,是这个吗?)16.fullgc触发机制17.tcp的拥塞控制流程(不会了)18.分布式事务解决方案,说了2pc,3pc,tcc。算法是反转双向链表,没有按格式输出,但是面试官没让继续写了,面完以为挂了,结果晚上秒过,看看复试什么情况吧。今天百度打电话准备发offer了,业务跟在手子的差不多,很垂,并且说不分日常暑期,只看表现,会有转正机会,但是考虑再三还是拒绝了,百度实习薪资确实有点低,title也不如之前了,但是面试的二位业务老师我很喜欢,对我的评价也不错,希望之后能有机会共事。从三月份到现在一共面了六家,面试次数总共是8场,情况如下:脉脉二面(无答复,默认挂)百度二面已oc美团一面过,下周一二面shein一面过直接HR面游族一面过直接HR面腾讯一面过等待约二面滴滴明天一面面试通过率还是蛮高的,但是大部分都是日常,感觉对我现在的加成不大,大概率不会去,不知道暑期会是什么情况呢唉,希望能有面试吧,继续加油。字节被无hc直接取消了,现在还没人捞,有没有字节HR救救我
不管什么都不想跳动了:本人美团百度快手都待过,建议肯定是直接留快手多一点产出后转正or直接冲字节腾讯暑期吧。一是快手从福利到基建都吊打另外两家。美团现在这个业务比较惨,本来毛利就很低,亏损严重,今年很可能要优化人力降低成本,去了别说日常,就算暑期后面都很可能被优化。百度其实实习生权限挺高的,可以接触到一些含金量高的项目,但是现在的风评不如之前了,薪资也不高。二是转正概率和薪资是跟产出挂钩的,你都在手子已经积累产出了,去其他家日常实习产出都是从0开始,肯定不可能有你在手子转正可能性大啊,现在日常压根没必要去,而且我有两个师弟都是在快手日常转正的,不用太担心,安心留在手子一边多做一点产出然后一边冲字节腾讯暑期,字节腾讯今年实习岗位非常多的,不如好好把握这个,加油。
今天你投了哪些公司?
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务