Mybatis 通過接口實現 sql 執行原理解析

語言: CN / TW / HK

使用過 mybatis 框架的小夥伴們都知道,mybatis 是個半 orm 框架,通過寫 mapper 接口就能自動實現數據庫的增刪改查,但是對其中的原理一知半解,接下來就讓我們深入框架的底層一探究竟

1、環境搭建

首先引入 mybatis 的依賴,在 resources 目錄下創建 mybatis 核心配置文件 mybatis-config.xml ```xml

<!-- 環境、事務工廠、數據源 -->
<environments default="dev">
    <environment id="dev">
        <transactionManager type="JDBC"/>
        <dataSource type="UNPOOLED">
            <property name="driver" value="org.apache.derby.jdbc.EmbeddedDriver"/>
            <property name="url" value="jdbc:derby:db-user;create=true"/>
        </dataSource>
    </environment>
</environments>

<!-- 指定 mapper 接口-->
<mappers>
    <mapper class="com.myboy.demo.mapper.user.UserMapper"/>
</mappers>

在 com.myboy.demo.mapper.user 包下新建一個接口 UserMapperjava public interface UserMapper {

UserEntity getById(Long id);

void insertOne(@Param("id") Long id, @Param("name") String name, @Param("json") List<String> json);

}

在 resources 的 com.myboy.demo.mapper.user 包下創建 UserMapper.xmlxml

<select id="getById" resultType="com.myboy.demo.db.entity.UserEntity">
    select * from demo_user where id = #{id}
</select>

<insert id="insertOne">
    insert into demo_user (id, name, json) values (#{id}, #{name}, #{json})
</insert>

創建 main 方法測試java try(InputStream in = Resources.getResourceAsStream("com/myboy/demo/sqlsession/mybatis-config.xml")){ SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(in); sqlSession = sqlSessionFactory.openSession(); # 拿到代理類對象 UserMapper mapper = sqlSession.getMapper(UserMapper.class); # 執行方法 UserEntity userEntity = mapper.getById(2L); System.out.println(userEntity); sqlSession.close(); }catch (Exception e){ e.printStackTrace(); } ```

2、動態代理類的生成

🤔 通過上面的示例,我們需要思考兩個問題:

  1. mybatis 如何生成 mapper 的動態代理類?
  2. 通過 sqlSession.getMapper 獲取到的動態代理類是什麼內容?

通過查看源碼,sqlSession.getMapper() 底層調用的是 mapperRegistry 的 getMapper 方法 java public <T> T getMapper(Class<T> type, SqlSession sqlSession) { // sqlSessionFactory build 的時候,就已經掃描了所有的 mapper 接口,並生成了一個 MapperProxyFactory 對象 // 這裏根據 mapper 接口類獲取 MapperProxyFactory 對象,這個對象可以用於生成 mapper 的代理對象 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 接口在解析時會對應生成一個 MapperProxyFactory,保存到 knownMappers 中,mapper 接口的實現類(也就是動態代理類)通過這個 MapperProxyFactory 生成,mapperProxyFactory.newInstance(sqlSession) 代碼如下: ```java /* * 根據 sqlSession 創建 mapper 的動態代理對象 * @param sqlSession sqlSession * @return 代理類 / public T newInstance(SqlSession sqlSession) { // 創建 MapperProxy 對象,這個對象實現 InvocationHandler 接口,裏面封裝類 mapper 動態代理方法的執行的核心邏輯 final MapperProxy mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache); return newInstance(mapperProxy); }

protected T newInstance(MapperProxy mapperProxy) { return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); } ``` 代碼一目瞭然,通過 jdk 動態代理技術創建了 mapper 接口的代理對象,其 InvocationHandler 的實現是 MapperProxy,那麼 mapper 接口中方法的執行,最終都會被 MapperProxy 增強

3、MapperProxy 增強 mapper 接口

MapperProxy 類實現了 InvocationHandler 接口,那麼其核心方法必然是在其 invoke 方法內部 java /** * 所有 mapper 代理對象的方法的核心邏輯 */ @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); } else { // 1、根據method創建方法執行器對象 MapperMethodInvoker,用於適配不同的方法執行過程 // 2、執行方法邏輯 return cachedInvoker(method).invoke(proxy, method, args, sqlSession); } } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } }

3.1、cachedInvoker(method)

由於 jdk8 對接口增加了 default 關鍵字,使接口中的方法也可以有方法體,但是默認方法和普通方法的反射執行方式不同,需要用適配器適配一下才能統一執行,具體代碼如下 java /** * 適配器模式,由於默認方法和普通方法反射執行的方式不同,所以用 MapperMethodInvoker 接口適配下 * DefaultMethodInvoker 用於執行默認方法 * PlainMethodInvoker 用於執行普通方法 */ private MapperMethodInvoker cachedInvoker(Method method) throws Throwable { try { return MapUtil.computeIfAbsent(methodCache, method, m -> { // 返回默認方法執行器 DefaultMethodInvoker if (m.isDefault()) { try { if (privateLookupInMethod == null) { return new DefaultMethodInvoker(getMethodHandleJava8(method)); } else { return new DefaultMethodInvoker(getMethodHandleJava9(method)); } } catch (IllegalAccessException | InstantiationException | InvocationTargetException | NoSuchMethodException e) { throw new RuntimeException(e); } } // 返回普通方法執行器,只有一個 invoke 執行方法,實際上就是調用 MapperMethod 的執行方法 else { return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration())); } }); } catch (RuntimeException re) { Throwable cause = re.getCause(); throw cause == null ? re : cause; } } 如果判定執行的是接口的默認方法,則原始方法封裝成 DefaultMethodInvoker,這個類的 invoke 方法就是利用反射調用原始方法,沒什麼好説的

如果是普通的接口方法,則將方法封裝成封裝成 MapperMethod,然後再將 MapperMethod 封裝到 PlainMethodInvoker 中,PlainMethodInvoker 沒什麼好看的,底層的執行方法還是調用 MapperMethod 的執行方法,至於 MapperMethod,咱們放到下一章來看

3.2、MapperMethod

首先看下構造方法 java public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) { // 通過這個 SqlCommand 可以拿到 sql 類型和sql 對應的 MappedStatement this.command = new SqlCommand(config, mapperInterface, method); // 包裝了 mapper 接口的一個方法,可以拿到方法的信息,比如方法返回值類型、返回是否集合、返回是否為空 this.method = new MethodSignature(config, mapperInterface, method); } 代碼裏的註釋寫的很清楚了,MapperMethod 構造方法創建了兩個對象 SqlCommand 和 MethodSignature

mapper 接口的執行核心邏輯在其 execute() 方法中: ```java /* * 執行 mapper 方法的核心邏輯 * @param sqlSession sqlSession * @param args 方法入參數組 * @return 接口方法返回值 / public Object execute(SqlSession sqlSession, Object[] args) { Object result; switch (command.getType()) { case INSERT: { // 參數處理,單個參數直接返回,多個參數封裝成 map Object param = method.convertArgsToSqlCommandParam(args); // 調用 sqlSession 的插入方法 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()) { // 方法返回值為 void,但是參數裏有 ResultHandler 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()) { // 方法返回指針 result = executeForCursor(sqlSession, args); } else { // 方法返回單個對象 // 將參數進行轉換,如果是一個參數,則原樣返回,如果多個參數,則返回map,key是參數name(@Param註解指定 或 arg0、arg1 或 param1、param2 ),value 是參數值 Object param = method.convertArgsToSqlCommandParam(args); // selectOne 從數據庫獲取數據,封裝成返回值類型,取出第一個 result = sqlSession.selectOne(command.getName(), param);

      // 如果返回值為空,並且返回值類型是 Optional,則將返回值用 Optional.ofNullable 包裝
      if (method.returnsOptional()
          && (result == null || !method.getReturnType().equals(result.getClass()))) {
        result = Optional.ofNullable(result);
      }
    }
    break;
  case FLUSH:
    result = sqlSession.flushStatements();
    break;
  default:
    throw new BindingException("Unknown execution method for: " + command.getName());
}
if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
  throw new BindingException("Mapper method '" + command.getName()
      + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
}
return result;

} ```

代碼邏輯很清晰,拿 Insert 方法來看,他只做了兩件事

  1. 參數轉換
  2. 調用 sqlSession 對應的 insert 方法

3.2.1、參數轉換 method.convertArgsToSqlCommandParam(args)

在 mapper 接口中,假設我們定義了一個 user 的查詢方法 java List<User> find(@Param("name")String name, @Param("age")Integer age) 在我們的 mapper.xml 中,寫出來的 sql 可以是這樣的: sql select * from user where name = #{name} and age > #{age}

當然不使用 @Param 註解也可以的,按參數順序來 sql select * from user where name = #{arg0} and age > #{arg1} 或 select * from user where name = #{param1} and age > #{param2}

因此如果要通過佔位符匹配到具體參數,就要將接口參數封裝成 map 了,如下所示 java {arg1=12, arg0="abc", param1="abc", param2=12} 或 {name="abc", age=12, param1="abc", param2=12} 這裏的這個 method.convertArgsToSqlCommandParam(args) 就是這個作用,當然只有一個參數的話就不用轉成 map 了, 直接就能匹配

3.2.2、調用 sqlSession 的方法獲取結果

真正要操作數據庫還是要藉助 sqlSession,因此很快就看到了 sqlSession.insert(command.getName(), param) 方法的執行,其第一個參數是 statement 的 id,就是 mpper.xml 中 namespace 和 insert 標籤的 id的組合,如 com.myboy.demo.mapper.MoonAppMapper.getAppById,第二個參數就是上面轉換過的參數,至於 sqlSession 內部處理邏輯,不在本章敍述範疇

sqlSession 方法執行完後的執行結果交給 rowCountResult 方法處理,這個方法很簡單,就是將數據庫返回的數據處理成接口返回類型,代碼很簡單,如下 java private Object rowCountResult(int rowCount) { final Object result; if (method.returnsVoid()) { result = null; } else if (Integer.class.equals(method.getReturnType()) || Integer.TYPE.equals(method.getReturnType())) { result = rowCount; } else if (Long.class.equals(method.getReturnType()) || Long.TYPE.equals(method.getReturnType())) { result = (long) rowCount; } else if (Boolean.class.equals(method.getReturnType()) || Boolean.TYPE.equals(method.getReturnType())) { result = rowCount > 0; } else { throw new BindingException("Mapper method '" + command.getName() + "' has an unsupported return type: " + method.getReturnType()); } return result; }

4、小結

到目前為止,我們已經搞清楚了通過 mapper 接口生成動態代理對象,以及代理對象調用 sqlSession 操作數據庫的邏輯,我總結出執行邏輯圖如下:

image.png