MyBatis 中 Mapper 接口的使用原理
MyBatis 3 推薦使用 Mapper 接口的方式來執行 xml 配置中的 SQL,用起來很方便,也很靈活。在方便之余,想了解一下這是如何實現的,之前也大致知道是通過 JDK 的動態代理做到的,但這次想知道細節。
東西越多就越復雜,所以就以一個簡單的僅依賴 MyBatis 3.4.0 的 CRUD 來逐步了解 Mapper 接口的調用。
通常是通過 xml 配置文件來創建SqlSessionFactory
對象,然后再獲取SqlSession
對象,接着獲取自定義的 Mapper 接口的代理對象,最后調用接口方法,示例如下:
/**
*
* @author xi
* @date 2018/10/01 14:12
*/
public class Demo {
public static void main(String[] args) throws IOException {
String resource = "mybatis-config.xml";
InputStream is = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory =
new SqlSessionFactoryBuilder().build(is);
SqlSession sqlSession = sqlSessionFactory.openSession();
RoleMapper roleMapper = sqlSession.getMapper(RoleMapper.class);
Role role = roleMapper.getRole(1L);
System.out.println(role);
}
}
如何解析配置文件,創建工廠,獲取會話皆不是本次關注的重點,直接看下面這行即可:
RoleMapper roleMapper = sqlSession.getMapper(RoleMapper.class);
獲取自定義 Mapper 代理對象的方法位於:org.apache.ibatis.session.SqlSession#getMapper
,還是個范型方法
/**
* Retrieves a mapper.
* @param <T> the mapper type
* @param type Mapper interface class
* @return a mapper bound to this SqlSession
*/
<T> T getMapper(Class<T> type);
實現該方法的子類有:DefaultSqlSession
和SqlSessionManager
,這里關注默認實現即可:org.apache.ibatis.session.defaults.DefaultSqlSession#getMapper
@Override
public <T> T getMapper(Class<T> type) {
return configuration.<T>getMapper(type, this);
}
這里面出現了 Configuration 對象,簡單來說就是包含了 xml 配置解析內容的對象,同樣它也不是現在關注的重點,繼續往下跟進:org.apache.ibatis.session.Configuration#getMapper
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
這里出現了MapperRegistry
對象,它是解析 Mapper.xml 中的內容(mapper
標簽中的namespace
就包含了 Mapper 接口的全限定名稱)得來的,含有一個 HashMap 類型的成員變量org.apache.ibatis.binding.MapperRegistry#knownMappers
,key 是 Mapper 接口的Class
對象,value 是org.apache.ibatis.binding.MapperProxyFactory
,從名稱就可以看出是用來創建 Mapper 接口的代理對象的工廠,后面會用到。
具體這個knownMappers
是怎么填充的,詳見org.apache.ibatis.binding.MapperRegistry#addMapper
方法,暫時不管,先往下走:org.apache.ibatis.binding.MapperRegistry#getMapper
@SuppressWarnings("unchecked")
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
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);
}
}
根據 Mapper 接口的類型,從knownMappers
中拿到對應的工廠,然后創建代理對象,繼續跟進:org.apache.ibatis.binding.MapperProxyFactory#newInstance(org.apache.ibatis.session.SqlSession)
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
這里又出現了一個MapperProxy
對象,理解起來像是一個代理對象,打開一看它實現了java.lang.reflect.InvocationHandler
接口,這是掛羊頭賣狗肉哇。
先不看狗肉了,繼續跟進:org.apache.ibatis.binding.MapperProxyFactory#newInstance(org.apache.ibatis.binding.MapperProxy<T>)
@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
在這,看到了熟悉的java.lang.reflect.Proxy
,這里的mapperInterface
是創建工廠時傳入的 Mapper 接口。真正的 Mapper 接口的代理對象此時才產生,是真羊頭。
這不是既熟悉又陌生的 JDK 動態代理么,說它熟悉,因為前面的狗肉mapperProxy
是一個InvocationHandler
對象,它攔截了所有對代理對象接口方法的調用。說它陌生是因為之前使用 JDK 動態代理時會有實現了該接口的被代理類和被代理對象,而 MyBatis 中只有接口和代理對象(代理類)。因為 MyBatis 只需要攔截接口方法,找到方法對應的 SQL 去執行就可以了,沒必要多此一舉加上被代理類,這也正是 MyBatis 方便的地方。接下來看看org.apache.ibatis.binding.MapperProxy
:
/**
* @author Clinton Begin
* @author Eduardo Macarron
*/
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;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 判斷 method 是不是 Object 類的方法,如 hashCode()、toString()
if (Object.class.equals(method.getDeclaringClass())) {
try {// 如果是,則調用當前 MapperProxy 對象的這些方法
// 跟 Mapper 接口的代理對象沒關系
return method.invoke(this, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
// 到這了,說明調用的是接口中的方法,具體的執行就不是本次關注的重點了
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}
// 對 MapperMethod 做了緩存,這個 methodCache 是個 ConcurrentHashMap,在 MapperProxyFactory 中創建的
private MapperMethod cachedMapperMethod(Method method) {
MapperMethod mapperMethod = methodCache.get(method);
if (mapperMethod == null) {
mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
methodCache.put(method, mapperMethod);
}
return mapperMethod;
}
}
具體說明都在代碼注釋里面,沒啥好說的了。
總結
- 通過 JDK 動態代理模式,創建 Mapper 接口的代理對象,攔截對接口方法的調用;
- Mapper 接口中不能使用重載,具體原因參見
org.apache.ibatis.binding.MapperMethod.SqlCommand#SqlCommand
,MyBatis 是通過mapperInterface.getName() + "." + method.getName()
去獲取 xml 中解析出來的 SQL 的,具體可能還要看一下org.apache.ibatis.session.Configuration#mappedStatements
。
最后的最后,古話說的好:遇事不決,先開大龍(看源碼)(逃