1. 程式人生 > 實用技巧 >美團面試題:為什麼能直接呼叫userMapper介面的方法?

美團面試題:為什麼能直接呼叫userMapper介面的方法?

關注“Java後端技術全棧”

回覆“面試”獲取全套面試資料
字數: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類中使用了兩種設計模式:

單例模式methodCache(註冊式單例模式)。

工廠模式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型別的觸發管理類。這裡我們就拿到代理類了,後面我們就可以使用這個代理物件進行方法呼叫。

問題涉及到的設計模式:

代理模式。

工廠模式。

單例模式。

整個流程圖:

圖片

冰凍三尺,非一日之寒表面意義是冰凍了三尺,並不是一天的寒冷所能達到的效果。學習亦如此,你每一天的一點點努力,都是為你以後的成功做鋪墊。

推薦閱讀

面試官:Integer快取最大範圍只能是-128到127嗎?
6000多字 | 秒殺系統設計注意點【理論】
面試官:說說你對Java異常的理解
《程式設計師面試寶典》.pdf下載