前端網頁列印外掛print.js(可匯出pdf)
前言
剛開始使用Mybaits的同學有沒有這樣的疑惑,為什麼我們沒有編寫Mapper的實現類,卻能呼叫Mapper的方法呢?本篇文章我帶大家一起來解決這個疑問
上一篇文章我們獲取到了DefaultSqlSession,接著我們來看第一篇文章測試用例後面的程式碼
//獲取對應的mapper
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
//執行方法
List<User> list = userMapper.getAll();
為 Mapper 介面建立代理物件
我們先從 DefaultSqlSession 的 getMapper 方法開始看起,如下:
public class DefaultSqlSession implements SqlSession {
private final Configuration configuration;
@Override
public <T> T getMapper(Class<T> type) {
return configuration.<T>getMapper(type, this);
}
}
public class Configuration {
protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
}
public class MapperRegistry {
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>();
@SuppressWarnings("unchecked")
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
// 從 knownMappers 中獲取與 type 對應的 MapperProxyFactory
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);
}
}
}
這裡最重要就是加了註釋的兩行程式碼。
獲取MapperProxyFactory
根據名稱看,可以理解為Mapper代理的建立工廠,是不是Mapper的代理物件由它建立呢?我們先來回顧一下knownMappers 集合中的元素是何時存入的。這要在我前面的文章中找答案,MyBatis 在解析配置檔案的 <mappers> 節點的過程中,會呼叫 MapperRegistry 的 addMapper 方法將 Class 到 MapperProxyFactory 物件的對映關係存入到 knownMappers。有興趣的同學可以看看我之前的文章,我們來回顧一下原始碼:
public class XMLMapperBuilder extends BaseBuilder {
private final XPathParser parser;
private final MapperBuilderAssistant builderAssistant;
private void bindMapperForNamespace() {
// 獲取對映檔案的名稱空間
String namespace = builderAssistant.getCurrentNamespace();
if (namespace != null) {
Class<?> boundType = null;
try {
// 根據名稱空間解析 mapper 型別
boundType = Resources.classForName(namespace);
} catch (ClassNotFoundException e) {
//ignore, bound type is not required
}
if (boundType != null) {
// 檢測當前 mapper 類是否被繫結過
if (!configuration.hasMapper(boundType)) {
// Spring may not know the real resource name so we set a flag
// to prevent loading again this resource from the mapper interface
// look at MapperAnnotationBuilder#loadXmlResource
configuration.addLoadedResource("namespace:" + namespace);
// 繫結 mapper 類
configuration.addMapper(boundType);
}
}
}
}
}
public class Configuration {
protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
public <T> void addMapper(Class<T> type) {
// 通過 MapperRegistry 繫結 mapper 類
mapperRegistry.addMapper(type);
}
}
public class MapperRegistry {
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>();
public <T> void addMapper(Class<T> type) {
if (type.isInterface()) {
if (hasMapper(type)) {
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
/*
* 將 type 和 MapperProxyFactory 進行繫結,MapperProxyFactory 可為 mapper 介面生成代理類
*/
knownMappers.put(type, new MapperProxyFactory<T>(type));
// It's important that the type is added before the parser is run
// otherwise the binding may automatically be attempted by the
// mapper parser. If the type is already known, it won't try.
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
// 解析註解中的資訊
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
}
在解析Mapper.xml的最後階段,獲取到Mapper.xml的namespace,然後利用反射,獲取到namespace的Class,並建立一個MapperProxyFactory的例項,namespace的Class作為引數,最後將namespace的Class為key,MapperProxyFactory的例項為value存入knownMappers。
注意,我們這裡是通過對映檔案的名稱空間的Class當做knownMappers的Key。然後我們看看getMapper方法,是通過引數User.class也就是Mapper介面的Class來獲取MapperProxyFactory,所以我們明白了為什麼要求xml配置中的namespace要和和對應的Mapper介面的全限定名了。
生成代理物件
我們看return mapperProxyFactory.newInstance(sqlSession);
,很明顯是呼叫了MapperProxyFactory的一個工廠方法,我們跟進去看看
public class MapperProxyFactory<T> {
//存放Mapper介面Class
private final Class<T> mapperInterface;
private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>();
public MapperProxyFactory(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
}
public Class<T> getMapperInterface() {
return mapperInterface;
}
public Map<Method, MapperMethod> getMethodCache() {
return methodCache;
}
@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
//生成mapperInterface的代理類
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
public T newInstance(SqlSession sqlSession) {
/*
* 建立 MapperProxy 物件,MapperProxy 實現了 InvocationHandler 介面,代理邏輯封裝在此類中
* 將sqlSession傳入MapperProxy物件中,第二個引數是Mapper的介面,並不是其實現類
*/
final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
}
上面的程式碼首先建立了一個 MapperProxy 物件,該物件實現了 InvocationHandler 介面。然後將物件作為引數傳給過載方法,並在過載方法中呼叫 JDK 動態代理介面為 Mapper介面 生成代理物件。
這裡要注意一點,MapperProxy這個InvocationHandler 建立的時候,傳入的引數並不是Mapper介面的實現類,我們以前是怎麼建立JDK動態代理的?先建立一個介面,然後再建立一個介面的實現類,最後建立一個InvocationHandler並將實現類傳入其中作為目標類,建立介面的代理類,然後呼叫代理類方法時會回撥InvocationHandler的invoke方法,最後在invoke方法中呼叫目標類的方法,但是我們這裡呼叫Mapper介面代理類的方法時,需要呼叫其實現類的方法嗎?不需要,我們需要呼叫對應的配置檔案的SQL,所以這裡並不需要傳入Mapper的實現類到MapperProxy中,那Mapper介面的代理物件是如何呼叫對應配置檔案的SQL呢?下面我們來看看。
Mapper代理類如何執行SQL?
上面一節中我們已經獲取到了EmployeeMapper的代理類,並且其InvocationHandler為MapperProxy,那我們接著看Mapper介面方法的呼叫
List<User> list = userMapper.getAll();
知道JDK動態代理的同學都知道,呼叫代理類的方法,最後都會回撥到InvocationHandler的Invoke方法,那我們來看看這個InvocationHandler(MapperProxy)
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 {
try {
// 如果方法是定義在 Object 類中的,則直接呼叫
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
//如果是介面中的default方法,JDK8的新特性之一
} else if (isDefaultMethod(method)) {
//如果使用者執行的是介面中的default方法的話,MyBatis就需要為使用者提供正常的代理流程。
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
// 從快取中獲取 MapperMethod 物件,若快取未命中,則建立 MapperMethod 物件
final MapperMethod mapperMethod = cachedMapperMethod(method);
// 呼叫 execute 方法執行 SQL
return mapperMethod.execute(sqlSession, args);
}
private MapperMethod cachedMapperMethod(Method method) {
MapperMethod mapperMethod = methodCache.get(method);
if (mapperMethod == null) {
//建立一個MapperMethod,引數為mapperInterface和method還有Configuration
mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
methodCache.put(method, mapperMethod);
}
return mapperMethod;
}
}
如上,回撥函式invoke邏輯會首先檢測被攔截的方法是不是定義在 Object 中的,比如 equals、hashCode 方法等。對於這類方法,直接執行即可。緊接著從快取中獲取或者建立 MapperMethod 物件,然後通過該物件中的 execute 方法執行 SQL。我們先來看看如何建立MapperMethod
建立 MapperMethod 物件
public class MapperMethod {
//包含SQL相關資訊,比喻MappedStatement的id屬性,(mapper.UserMapper.getAll)
private final SqlCommand command;
//包含了關於執行的Mapper方法的引數型別和返回型別。
private final MethodSignature method;
public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
// 建立 SqlCommand 物件,該物件包含一些和 SQL 相關的資訊
this.command = new SqlCommand(config, mapperInterface, method);
// 建立 MethodSignature 物件,從類名中可知,該物件包含了被攔截方法的一些資訊
this.method = new MethodSignature(config, mapperInterface, method);
}
}
MapperMethod包含SqlCommand 和MethodSignature 物件,我們來看看其建立過程
1、建立 SqlCommand 物件
public static class SqlCommand {
//name為MappedStatement的id,也就是namespace.methodName(mapper.UserMapper.getAll)
private final String name;
//SQL的型別,如insert,delete,update
private final SqlCommandType type;
public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
//拼接Mapper介面名和方法名,(mapper.UserMapper.getAll)
final String methodName = method.getName();
final Class<?> declaringClass = method.getDeclaringClass();
//檢測configuration是否有key為mapper.UserMapper.getAll的MappedStatement
//獲取MappedStatement
MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass,
configuration);
// 檢測當前方法是否有對應的 MappedStatement
if (ms == null) {
if (method.getAnnotation(Flush.class) != null) {
name = null;
type = SqlCommandType.FLUSH;
} else {
throw new BindingException("Invalid bound statement (not found): "
+ mapperInterface.getName() + "." + methodName);
}
} else {
// 設定 name 和 type 變數
name = ms.getId();
type = ms.getSqlCommandType();
if (type == SqlCommandType.UNKNOWN) {
throw new BindingException("Unknown execution method for: " + name);
}
}
}
private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName,
Class<?> declaringClass, Configuration configuration) {
String statementId = mapperInterface.getName() + "." + methodName;
//檢測configuration是否有key為statementName的MappedStatement
if (configuration.hasStatement(statementId)) {
return configuration.getMappedStatement(statementId);
} else if (mapperInterface.equals(declaringClass)) {
return null;
}
for (Class<?> superInterface : mapperInterface.getInterfaces()) {
if (declaringClass.isAssignableFrom(superInterface)) {
MappedStatement ms = resolveMappedStatement(superInterface, methodName,
declaringClass, configuration);
if (ms != null) {
return ms;
}
}
}
return null