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

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

[[361093]]

老规矩,先上案例代码,这样大家可以更加熟悉是如何使用的,看过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(); 
            } 
        } 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.

看源码有什么用?

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

前两篇文章我们已经Mybatis配置文件解析到获取SqlSession,下面我们来分析从SqlSession到userMapper:

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

前面那篇文章已经知道了这里的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.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

这里有三个问题:

问题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); 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

这里也没做什么,继续调用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); 
            } 
        } 
    } 


  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.

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); 
    } 
    } 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.

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

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

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

问题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); 
    } 
  } 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.

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

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

userMapper=$Proxy6@2355。 
  • 1.

为什么要在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. 单例模式。

整个流程图:

本文转载自微信公众号「Java后端技术全栈」,可以通过以下二维码关注。转载本文请联系Java后端技术全栈公众号。

 

责任编辑:武晓燕 来源: Java后端技术全栈
相关推荐

2020-03-23 12:58:34

美团公有云互联网

2018-04-23 09:50:54

2023-03-28 21:33:53

面试隔离MVCC

2024-06-07 08:10:14

Netty操作系统零拷贝

2023-05-22 08:17:04

2024-05-16 17:58:30

线程任务线程通讯线程池

2023-12-20 07:36:58

GoLinux语言

2022-02-15 07:03:04

start 源码run线程

2022-06-15 09:02:32

JVM线程openJDK

2022-09-05 15:36:47

线程方法Java

2023-05-09 10:05:24

HashMapNull

2023-11-30 08:16:19

SpringjarTomcat

2013-08-20 13:11:58

技术美团

2022-03-03 16:45:02

美团述职反馈

2023-09-29 11:50:10

接口编程代码

2016-11-27 20:43:26

云计算迭代

2021-08-29 18:36:17

MySQL技术面试题

2017-06-01 10:52:35

互联网

2023-01-31 08:44:50

SQL语句查询

2018-07-20 10:18:05

走进美团工程师文化
点赞
收藏

51CTO技术栈公众号