Mybaits 源碼解析 (五)----- 面試源碼系列:Mapper介面底層原理(為什麼Mapper不用寫實現類就能訪問到資料庫?)

  • 2019 年 11 月 1 日
  • 筆記

剛開始使用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)和方法的運行參數。

查詢操作我們下一篇文章單獨來講

 

Tags: