写点什么

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

用户头像
田维常
关注
发布于: 2020 年 12 月 30 日

字数:2434,阅读耗时:3 分 40 秒。

老规矩,先上案例代码,这样大家可以更加熟悉是如何使用的,看过 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();            }        }
复制代码

看源码有什么用?


图片


通过源码的学习,我们可以收获 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);    }
复制代码

这里有三个问题:


图片


问题 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。
复制代码


图片


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

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


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

JDK 动态代理

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

  • 被代理类(即就是实现类)

  • 接口

  • 实现了 InvocationHanndler 的触发管理类,用来生成代理对象。

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


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

MyBatis 动态代理:

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


图片


这里我们的目的其实就是根据一个可以执行的方法,直接找到 Mapper.xml 中 statement ID ,方便调用。

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

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

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

请看下面这张图:


图片


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


图片


总结

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

问题涉及到的设计模式:

  1. 代理模式。

  2. 工厂模式。

  3. 单例模式。

整个流程图:


图片


冰冻三尺,非一日之寒表面意义是冰冻了三尺,并不是一天的寒冷所能达到的效果。学习亦如此,你每一天的一点点努力,都是为你以后的成功做铺垫。

推荐阅读

面试官:Integer缓存最大范围只能是-128到127吗?

6000多字 | 秒杀系统设计注意点【理论】

面试官:说说你对Java异常的理解

《程序员面试宝典》.pdf下

发布于: 2020 年 12 月 30 日阅读数: 25
用户头像

田维常

关注

关注公众号:Java后端技术全栈,领500G资料 2020.10.24 加入

关注公众号:Java后端技术全栈,领500G资料

评论

发布
暂无评论
美团面试:为什么就能直接调用userMapper接口的方法?