MyBatis 中 Mapper 接口的使用原理


MyBatis 中 Mapper 接口的使用原理

MyBatis 3 推薦使用 Mapper 接口的方式來執行 xml 配置中的 SQL,用起來很方便,也很靈活。在方便之余,想了解一下這是如何實現的,之前也大致知道是通過 JDK 的動態代理做到的,但這次想知道細節。

東西越多就越復雜,所以就以一個簡單的僅依賴 MyBatis 3.4.0 的 CRUD 來逐步了解 Mapper 接口的調用。

通常是通過 xml 配置文件來創建SqlSessionFactory對象,然后再獲取SqlSession對象,接着獲取自定義的 Mapper 接口的代理對象,最后調用接口方法,示例如下:

/**
 *
 * @author xi
 * @date 2018/10/01 14:12
 */
public class Demo {
    public static void main(String[] args) throws IOException {
        String resource = "mybatis-config.xml";
        InputStream is = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory =
                new SqlSessionFactoryBuilder().build(is);
        SqlSession sqlSession = sqlSessionFactory.openSession();
        RoleMapper roleMapper = sqlSession.getMapper(RoleMapper.class);
        Role role = roleMapper.getRole(1L);
        System.out.println(role);
    }
}

如何解析配置文件,創建工廠,獲取會話皆不是本次關注的重點,直接看下面這行即可:

RoleMapper roleMapper = sqlSession.getMapper(RoleMapper.class);

獲取自定義 Mapper 代理對象的方法位於:org.apache.ibatis.session.SqlSession#getMapper,還是個范型方法

  /**
   * Retrieves a mapper.
   * @param <T> the mapper type
   * @param type Mapper interface class
   * @return a mapper bound to this SqlSession
   */
  <T> T getMapper(Class<T> type);

實現該方法的子類有:DefaultSqlSessionSqlSessionManager,這里關注默認實現即可:org.apache.ibatis.session.defaults.DefaultSqlSession#getMapper

  @Override
  public <T> T getMapper(Class<T> type) {
    return configuration.<T>getMapper(type, this);
  }

這里面出現了 Configuration 對象,簡單來說就是包含了 xml 配置解析內容的對象,同樣它也不是現在關注的重點,繼續往下跟進:org.apache.ibatis.session.Configuration#getMapper

  public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    return mapperRegistry.getMapper(type, sqlSession);
  }

這里出現了MapperRegistry對象,它是解析 Mapper.xml 中的內容(mapper標簽中的namespace就包含了 Mapper 接口的全限定名稱)得來的,含有一個 HashMap 類型的成員變量org.apache.ibatis.binding.MapperRegistry#knownMappers,key 是 Mapper 接口的Class對象,value 是org.apache.ibatis.binding.MapperProxyFactory,從名稱就可以看出是用來創建 Mapper 接口的代理對象的工廠,后面會用到。

具體這個knownMappers是怎么填充的,詳見org.apache.ibatis.binding.MapperRegistry#addMapper方法,暫時不管,先往下走:org.apache.ibatis.binding.MapperRegistry#getMapper

  @SuppressWarnings("unchecked")
  public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    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 接口的類型,從knownMappers中拿到對應的工廠,然后創建代理對象,繼續跟進:org.apache.ibatis.binding.MapperProxyFactory#newInstance(org.apache.ibatis.session.SqlSession)

  public T newInstance(SqlSession sqlSession) {
    final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
  }

這里又出現了一個MapperProxy對象,理解起來是一個代理對象,打開一看它實現了java.lang.reflect.InvocationHandler接口,這是掛羊頭賣狗肉哇。
先不看狗肉了,繼續跟進:org.apache.ibatis.binding.MapperProxyFactory#newInstance(org.apache.ibatis.binding.MapperProxy<T>)

  @SuppressWarnings("unchecked")
  protected T newInstance(MapperProxy<T> mapperProxy) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }

在這,看到了熟悉的java.lang.reflect.Proxy,這里的mapperInterface是創建工廠時傳入的 Mapper 接口。真正的 Mapper 接口的代理對象此時才產生,是真羊頭。

這不是既熟悉又陌生的 JDK 動態代理么,說它熟悉,因為前面的狗肉mapperProxy是一個InvocationHandler對象,它攔截了所有對代理對象接口方法的調用。說它陌生是因為之前使用 JDK 動態代理時會有實現了該接口的被代理類和被代理對象,而 MyBatis 中只有接口和代理對象(代理類)。因為 MyBatis 只需要攔截接口方法,找到方法對應的 SQL 去執行就可以了,沒必要多此一舉加上被代理類,這也正是 MyBatis 方便的地方。接下來看看org.apache.ibatis.binding.MapperProxy

/**
 * @author Clinton Begin
 * @author Eduardo Macarron
 */
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 {
      // 判斷 method 是不是 Object 類的方法,如 hashCode()、toString()
    if (Object.class.equals(method.getDeclaringClass())) {
      try {// 如果是,則調用當前 MapperProxy 對象的這些方法
      // 跟 Mapper 接口的代理對象沒關系
        return method.invoke(this, args);
      } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
      }
    }
    // 到這了,說明調用的是接口中的方法,具體的執行就不是本次關注的重點了
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);
  }

// 對 MapperMethod 做了緩存,這個 methodCache 是個 ConcurrentHashMap,在 MapperProxyFactory 中創建的
  private MapperMethod cachedMapperMethod(Method method) {
    MapperMethod mapperMethod = methodCache.get(method);
    if (mapperMethod == null) {
      mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
      methodCache.put(method, mapperMethod);
    }
    return mapperMethod;
  }

}

具體說明都在代碼注釋里面,沒啥好說的了。

總結

  1. 通過 JDK 動態代理模式,創建 Mapper 接口的代理對象,攔截對接口方法的調用;
  2. Mapper 接口中不能使用重載,具體原因參見org.apache.ibatis.binding.MapperMethod.SqlCommand#SqlCommand,MyBatis 是通過mapperInterface.getName() + "." + method.getName()去獲取 xml 中解析出來的 SQL 的,具體可能還要看一下org.apache.ibatis.session.Configuration#mappedStatements

最后的最后,古話說的好:遇事不決,先開大龍(看源碼)(逃


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM