天天看點

Mybatis是如何通過接口實作 sql 執行原了解析的

作者:小滿隻想睡覺

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

1、環境搭建

首先引入 mybatis 的依賴,在 resources 目錄下建立 mybatis 核心配置檔案 mybatis-config.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>

    <!-- 環境、事務工廠、資料源 -->
    <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>

</configuration>

複制代碼           

在 com.myboy.demo.mapper.user 包下建立一個接口 UserMapper

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.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.myboy.demo.mapper.user.UserMapper">

    <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>
</mapper>
複制代碼           

建立 main 方法測試

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 方法

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) 代碼如下:

/**
 * 根據 sqlSession 建立 mapper 的動态代理對象
 * @param sqlSession sqlSession
 * @return 代理類
 */
public T newInstance(SqlSession sqlSession) {
    // 建立 MapperProxy 對象,這個對象實作 InvocationHandler 接口,裡面封裝類 mapper 動态代理方法的執行的核心邏輯
    final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
}

protected T newInstance(MapperProxy<T> mapperProxy) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
複制代碼           

代碼一目了然,通過 jdk 動态代理技術建立了 mapper 接口的代理對象,其 InvocationHandler 的實作是 MapperProxy,那麼 mapper 接口中方法的執行,最終都會被 MapperProxy 增強

3、MapperProxy 增強 mapper 接口

MapperProxy 類實作了 InvocationHandler 接口,那麼其核心方法必然是在其 invoke 方法内部

/**
 * 所有 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 關鍵字,使接口中的方法也可以有方法體,但是預設方法和普通方法的反射執行方式不同,需要用擴充卡适配一下才能統一執行,具體代碼如下

/**
 * 擴充卡模式,由于預設方法和普通方法反射執行的方式不同,是以用 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

首先看下構造方法

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() 方法中:

/**
   * 執行 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 的查詢方法

List<User> find(@Param("name")String name, @Param("age")Integer age)
複制代碼           

在我們的 mapper.xml 中,寫出來的 sql 可以是這樣的:

select * from user where name = #{name} and age > #{age}
複制代碼           

當然不使用 @Param 注解也可以的,按參數順序來

select * from user where name = #{arg0} and age > #{arg1}
或
select * from user where name = #{param1} and age > #{param2}
複制代碼           

是以如果要通過占位符比對到具體參數,就要将接口參數封裝成 map 了,如下所示

{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 方法處理,這個方法很簡單,就是将資料庫傳回的資料處理成接口傳回類型,代碼很簡單,如下

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 操作資料庫的邏輯,我總結出執行邏輯圖如下:

Mybatis是如何通過接口實作 sql 執行原了解析的