Mybaits 原始碼解析 (五)----- 面試原始碼系列:Mapper介面底層原理(為什麼Mapper不用寫實現類就能訪問到資料庫?)
剛開始使用Mybaits的同學有沒有這樣的疑惑,為什麼我們沒有編寫Mapper的實現類,卻能呼叫Mapper的方法呢?本篇文章我帶大家一起來解決這個疑問
上一篇文章我們獲取到了DefaultSqlSession,接著我們來看第一篇文章測試用例後面的程式碼
EmployeeMapper employeeMapper = sqlSession.getMapper(Employee.class); List<Employee> allEmployees = employeeMapper.getAll();
為 Mapper 介面建立代理物件
我們先從 DefaultSqlSession 的 getMapper 方法開始看起,如下:
1 public <T> T getMapper(Class<T> type) { 2 return configuration.<T>getMapper(type, this); 3 } 4 5 // Configuration 6 public <T> T getMapper(Class<T> type, SqlSession sqlSession) { 7 return mapperRegistry.getMapper(type, sqlSession); 8 } 9 10 // MapperRegistry 11 public <T> T getMapper(Class<T> type, SqlSession sqlSession) { 12 // 從 knownMappers 中獲取與 type 對應的 MapperProxyFactory 13 final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type); 14 if (mapperProxyFactory == null) { 15 throw new BindingException("Type " + type + " is not known to the MapperRegistry."); 16 } 17 try { 18 // 建立代理物件 19 return mapperProxyFactory.newInstance(sqlSession); 20 } catch (Exception e) { 21 throw new BindingException("Error getting mapper instance. Cause: " + e, e); 22 } 23 }
這裡最重要就是兩行程式碼,第13行和第19行,我們接下來就分析這兩行程式碼
獲取MapperProxyFactory
根據名稱看,可以理解為Mapper代理的建立工廠,是不是Mapper的代理物件由它建立呢?我們先來回顧一下knownMappers 集合中的元素是何時存入的。這要在我前面的文章中找答案,MyBatis 在解析配置檔案的 <mappers> 節點的過程中,會呼叫 MapperRegistry 的 addMapper 方法將 Class 到 MapperProxyFactory 物件的對映關係存入到 knownMappers。有興趣的同學可以看看我之前的文章,我們來回顧一下原始碼:
private void bindMapperForNamespace() { // 獲取對映檔案的名稱空間 String namespace = builderAssistant.getCurrentNamespace(); if (namespace != null) { Class<?> boundType = null; try { // 根據名稱空間解析 mapper 型別 boundType = Resources.classForName(namespace); } catch (ClassNotFoundException e) { } if (boundType != null) { // 檢測當前 mapper 類是否被繫結過 if (!configuration.hasMapper(boundType)) { configuration.addLoadedResource("namespace:" + namespace); // 繫結 mapper 類 configuration.addMapper(boundType); } } } } // Configuration public <T> void addMapper(Class<T> type) { // 通過 MapperRegistry 繫結 mapper 類 mapperRegistry.addMapper(type); } // MapperRegistry 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)); 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方法的13行,是通過引數Employee.class也就是Mapper介面的Class來獲取MapperProxyFactory,所以我們明白了為什麼要求xml配置中的namespace要和和對應的Mapper介面的全限定名了
生成代理物件
我們看第19行程式碼 return mapperProxyFactory.newInstance(sqlSession);,很明顯是呼叫了MapperProxyFactory的一個工廠方法,我們跟進去看看
public class MapperProxyFactory<T> { //存放Mapper介面Class private final Class<T> mapperInterface; private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap(); public MapperProxyFactory(Class<T> mapperInterface) { this.mapperInterface = mapperInterface; } public Class<T> getMapperInterface() { return this.mapperInterface; } public Map<Method, MapperMethod> getMethodCache() { return this.methodCache; } protected T newInstance(MapperProxy<T> mapperProxy) { //生成mapperInterface的代理類 return Proxy.newProxyInstance(this.mapperInterface.getClassLoader(), new Class[]{this.mapperInterface}, mapperProxy); } public T newInstance(SqlSession sqlSession) { /* * 建立 MapperProxy 物件,MapperProxy 實現了 InvocationHandler 介面,代理邏輯封裝在此類中 * 將sqlSession傳入MapperProxy物件中,第二個引數是Mapper的介面,並不是其實現類 */ MapperProxy<T> mapperProxy = new MapperProxy(sqlSession, this.mapperInterface, this.methodCache); return this.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<Employee> allEmployees = employeeMapper.getAll();
知道JDK動態代理的同學都知道,呼叫代理類的方法,最後都會回撥到InvocationHandler的Invoke方法,那我們來看看這個InvocationHandler(MapperProxy)
public class MapperProxy<T> implements InvocationHandler, Serializable { 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; } public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 如果方法是定義在 Object 類中的,則直接呼叫 if (Object.class.equals(method.getDeclaringClass())) { try { return method.invoke(this, args); } catch (Throwable var5) { throw ExceptionUtil.unwrapThrowable(var5); } } else { // 從快取中獲取 MapperMethod 物件,若快取未命中,則建立 MapperMethod 物件 MapperMethod mapperMethod = this.cachedMapperMethod(method); // 呼叫 execute 方法執行 SQL return mapperMethod.execute(this.sqlSession, args); } } private MapperMethod cachedMapperMethod(Method method) { MapperMethod mapperMethod = (MapperMethod)this.methodCache.get(method); if (mapperMethod == null) { //建立一個MapperMethod,引數為mapperInterface和method還有Configuration mapperMethod = new MapperMethod(this.mapperInterface, method, this.sqlSession.getConfiguration()); this.methodCache.put(method, mapperMethod); } return mapperMethod; } }
如上,回撥函式invoke邏輯會首先檢測被攔截的方法是不是定義在 Object 中的,比如 equals、hashCode 方法等。對於這類方法,直接執行即可。緊接著從快取中獲取或者建立 MapperMethod 物件,然後通過該物件中的 execute 方法執行 SQL。我們先來看看如何建立MapperMethod
建立 MapperMethod 物件
public class MapperMethod { //包含SQL相關資訊,比喻MappedStatement的id屬性,(mapper.EmployeeMapper.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 物件,我們來看看其建立過程
① 建立 SqlCommand 物件
public static class SqlCommand { //name為MappedStatement的id,也就是namespace.methodName(mapper.EmployeeMapper.getAll) private final String name; //SQL的型別,如insert,delete,update private final SqlCommandType type; public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) { //拼接Mapper介面名和方法名,(mapper.EmployeeMapper.getAll) String statementName = mapperInterface.getName() + "." + method.getName(); MappedStatement ms = null; //檢測configuration是否有key為mapper.EmployeeMapper.getAll的MappedStatement if (configuration.hasStatement(statementName)) { //獲取MappedStatement ms = configuration.getMappedStatement(statementName); } else if (!mapperInterface.equals(method.getDeclaringClass())) { String parentStatementName = method.getDeclaringClass().getName() + "." + method.getName(); if (configuration.hasStatement(parentStatementName)) { ms = configuration.getMappedStatement(parentStatementName); } } // 檢測當前方法是否有對應的 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); } } } } public boolean hasStatement(String statementName, boolean validateIncompleteStatements) { //檢測configuration是否有key為statementName的MappedStatement return this.mappedStatements.containsKey(statementName); }
通過拼接介面名和方法名,在configuration獲取對應的MappedStatement,並設定設定 name 和 type 變數,程式碼很簡單
② 建立 MethodSignature 物件
MethodSignature 包含了被攔截方法的一些資訊,如目標方法的返回型別,目標方法的引數列表資訊等。下面,我們來看一下 MethodSignature 的構造方法。
public static class MethodSignature { private final boolean returnsMany; private final boolean returnsMap; private final boolean returnsVoid; private final boolean returnsCursor; private final Class<?> returnType; private final String mapKey; private final Integer resultHandlerIndex; private final Integer rowBoundsIndex; private final ParamNameResolver paramNameResolver; public MethodSignature(Configuration configuration, Class<?> mapperInterface, Method method) { // 通過反射解析方法返回型別 Type resolvedReturnType = TypeParameterResolver.resolveReturnType(method, mapperInterface); if (resolvedReturnType instanceof Class<?>) { this.returnType = (Class<?>) resolvedReturnType; } else if (resolvedReturnType instanceof ParameterizedType) { this.returnType = (Class<?>) ((ParameterizedType) resolvedReturnType).getRawType(); } else { this.returnType = method.getReturnType(); } // 檢測返回值型別是否是 void、集合或陣列、Cursor、Map 等 this.returnsVoid = void.class.equals(this.returnType); this.returnsMany = configuration.getObjectFactory().isCollection(this.returnType) || this.returnType.isArray(); this.returnsCursor = Cursor.class.equals(this.returnType); // 解析 @MapKey 註解,獲取註解內容 this.mapKey = getMapKey(method); this.returnsMap = this.mapKey != null; /* * 獲取 RowBounds 引數在引數列表中的位置,如果引數列表中 * 包含多個 RowBounds 引數,此方法會丟擲異常 */ this.rowBoundsIndex = getUniqueParamIndex(method, RowBounds.class); // 獲取 ResultHandler 引數在引數列表中的位置 this.resultHandlerIndex = getUniqueParamIndex(method, ResultHandler.class); // 解析引數列表 this.paramNameResolver = new ParamNameResolver(configuration, method); } }
執行 execute 方法
前面已經分析了 MapperMethod 的初始化過程,現在 MapperMethod 建立好了。那麼,接下來要做的事情是呼叫 MapperMethod 的 execute 方法,執行 SQL。傳遞引數sqlSession和method的執行引數args
return mapperMethod.execute(this.sqlSession, args);
我們去MapperMethod 的execute方法中看看
MapperMethod
public Object execute(SqlSession sqlSession, Object[] args) { Object result; // 根據 SQL 型別執行相應的資料庫操作 switch (command.getType()) { case INSERT: { // 對使用者傳入的引數進行轉換,下同 Object param = method.convertArgsToSqlCommandParam(args); // 執行插入操作,rowCountResult 方法用於處理返回值 result = rowCountResult(sqlSession.insert(command.getName(), param)); break; } case UPDATE: { Object param = method.convertArgsToSqlCommandParam(args); // 執行更新操作 result = rowCountResult(sqlSession.update(command.getName(), param)); break; } case DELETE: { Object param = method.convertArgsToSqlCommandParam(args); // 執行刪除操作 result = rowCountResult(sqlSession.delete(command.getName(), param)); break; } case SELECT: // 根據目標方法的返回型別進行相應的查詢操作 if (method.returnsVoid() && method.hasResultHandler()) { executeWithResultHandler(sqlSession, args); result = null; } else if (method.returnsMany()) { // 執行查詢操作,並返回多個結果 result = executeForMany(sqlSession, args); } else if (method.returnsMap()) { // 執行查詢操作,並將結果封裝在 Map 中返回 result = executeForMap(sqlSession, args); } else if (method.returnsCursor()) { // 執行查詢操作,並返回一個 Cursor 物件 result = executeForCursor(sqlSession, args); } else { Object param = method.convertArgsToSqlCommandParam(args); // 執行查詢操作,並返回一個結果 result = sqlSession.selectOne(command.getName(), param); } break; case FLUSH: // 執行重新整理操作 result = sqlSession.flushStatements(); break; default: throw new BindingException("Unknown execution method for: " + command.getName()); } return result; }
如上,execute 方法主要由一個 switch 語句組成,用於根據 SQL 型別執行相應的資料庫操作。我們先來看看是引數的處理方法convertArgsToSqlCommandParam是如何將方法引數陣列轉化成Map的
public Object convertArgsToSqlCommandParam(Object[] args) { return paramNameResolver.getNamedParams(args); } public Object getNamedParams(Object[] args) { final int paramCount = names.size(); if (args == null || paramCount == 0) { return null; } else if (!hasParamAnnotation && paramCount == 1) { return args[names.firstKey()]; } else { //建立一個Map,key為method的引數名,值為method的執行時引數值 final Map<String, Object> param = new ParamMap<Object>(); int i = 0; for (Map.Entry<Integer, String> entry : names.entrySet()) { // 新增 <引數名, 引數值> 鍵值對到 param 中 param.put(entry.getValue(), args[entry.getKey()]); final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1); if (!names.containsValue(genericParamName)) { param.put(genericParamName, args[entry.getKey()]); } i++; } return param; } }
我們看到,將Object[] args轉化成了一個Map<引數名, 引數值> ,接著我們就可以看查詢過程分析了,如下
// 執行查詢操作,並返回一個結果 result = sqlSession.selectOne(command.getName(), param);
我們看到是通過sqlSession來執行查詢的,並且傳入的引數為command.getName()和param,也就是namespace.methodName(mapper.EmployeeMapper.getAll)和方法的執行引數。
查詢操作我們下一篇文章單獨來講
&n