美團面試題:為什麼能直接呼叫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下載