1. 程式人生 > 程式設計 >Mybatis Mapper介面工作原理例項解析

Mybatis Mapper介面工作原理例項解析

KeyWords: Mybatis 原理,原始碼,Mybatis Mapper 介面實現類,代理模式,動態代理,Java動態代理,

Proxy.newProxyInstance,Mapper 對映,Mapper 實現

MyBatis 是一款優秀的持久層框架,它支援定製化 SQL、儲存過程以及高階對映。MyBatis 避免了幾乎所有的 JDBC 程式碼和手動設定引數以及獲取結果集。我們在使用 Mybaits 進行 ,通常只需要定義幾個 Mapper 介面,然後在編寫一個 xml 檔案,我們在配置檔案中寫好 sql,Mybatis 幫我們完成 Mapper 介面道具體實現的呼叫。以及將結果對映到 model bean 中。

我們在專案中所編寫的眾多的 Mapper 類只是一個介面(interface ),根據 Java 的多型性我們知道,可以使用介面介面作為形參,進而在執行時確定具體實現的物件是什麼。但是,對於 Mapper 介面,我們並沒有編寫其實現類!Mybatis是如何找到其實現類,進而完成具體的 CRUD 方法呼叫的呢?原理何在?

Mapper 介面是怎麼找到實現類的

為了弄清楚 Mapper 介面是如何找到實現類的,我們先回憶一下 Mybatis 是怎麼使用的,根據實際的例子,進而一點點的去分析。這裡的使用指的是Mybatis 單獨使用,而不是整合 spring,因為整合 spring 的話,還需要涉及 Mapper dao 裝載到 spring 容器的問題,spring 幫忙建立資料來源配置等問題。

通常我們使用 Mybatis 的主要步驟是:

  • 構建 SqlSessionFactory ( 通過 xml 配置檔案,或者直接編寫Java程式碼)
  • 從 SqlSessionFactory 中獲取 SqlSession
  • 從SqlSession 中獲取 Mapper
  • 呼叫 Mapper 的方法 ,例如:blogMapper.selectBlog(int blogId)

從一段程式碼看起

上面我們概括了使用 Mybatis 的4個步驟。這4個步驟看起來很簡單,但是用程式碼寫出來就很多。我們不妨先記著這4個步驟,再去看程式碼,會容易點。

// 1. 
DataSource dataSource = BlogDataSourceFactory.getBlogDataSource();
TransactionFactory transactionFactory = new JdbcTransactionFactory();
Environment environment = new Environment("development",transactionFactory,dataSource);
Configuration configuration = new Configuration(environment);
configuration.addMapper(BlogMapper.class);// 新增Mapper介面
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);
// 2. 
SqlSession session = sqlSessionFactory.openSession();
try {
 // 3. 
 BlogMapper mapper = session.getMapper(BlogMapper.class);
 // 4.
 Blog blog = mapper.selectBlog(1);
} finally {
 session.close();
}

在這塊程式碼中,第 1 部分我們使用了 Java 編碼的形式來實現 SqlSessionFactory ,也可以使用 xml 。如果使用xml的話,上面的第一部分程式碼就是這樣的:

String resource = "org/mybatis/example/mybatis-config.xml"; // xml內容就不貼了
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

我們本次的目標是弄清楚 “ Mapper 是如何找到實現類的 ”,我們注意上面程式碼 3,4 的位置:

 // 3. 
 BlogMapper mapper = session.getMapper(BlogMapper.class);
 // 4.
 Blog blog = mapper.selectBlog(1);

這裡 mapper 可以呼叫selectBlog(1) 這個方法,說明 mapper 是個物件,因為物件才具有方法行為實現啊。BlogMapper介面是不能例項化的,更沒有具體方法實現。我們並沒有定義一個類,讓它實現BlogMapper介面,而在這裡它只是通過呼叫session.getMapper() 所得到的。由此,我們可以推斷:肯定是session.getMapper() 方法內部產生了BlogMapper的實現類。有什麼技術可以根據BlogMapper 介面生成了一個實現類呢?想到這裡,對於有動態代理 使用經驗的程式設計師來說,很容易想到,這背後肯定是基於動態代理技術,具體怎麼實現的呢?下面我們來根據原始碼一探究竟。

Mapper 介面的註冊

從上面的程式碼中,我們知道 BlogMapper 介面的實現類是從session.getMapper中得來的,大概是基於動態代理技術實現。我們既然能夠從SqlSession中得到BlogMapper介面的,那麼我們肯定需要先在哪裡把它放進去了,然後 SqlSession 才能生成我們想要的代理類啊。上面程式碼中有這麼一行:

configuration.addMapper(BlogMapper.class);

跟著這個 addMapper 方法的程式碼實現是這樣的:

 public <T> void addMapper(Class<T> type) {
  mapperRegistry.addMapper(type);
 }

我們看到這裡 mapper 實際上被新增到 mapperRegissry 中。繼續跟進程式碼:

public class MapperRegistry {
 private final Map<Class<?>,MapperProxyFactory<?>> knownMappers 
                         = new HashMap<Class<?>,MapperProxyFactory<?>>();
 
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 {
    knownMappers.put(type,new MapperProxyFactory<T>(type)); // 注意這裡
 
    MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config,type);
    parser.parse();
    loadCompleted = true;

   } finally {
    if (!loadCompleted) {
     knownMappers.remove(type);
    }
   }
  }
 }
}

看到這裡我們知道上面所執行的configuration.addMapper(BlogMapper.class); 其實最終被放到了HashMap中,其名為knownMappers ,knowMappers是MapperRegistry 類的一個私有屬性,它是一個HashMap 。其Key 為當前Class物件,value 為一個MapperProxyFactory 例項。

這裡我們總結一下: 諸如BlogMapper 之類的Mapper介面被新增到了MapperRegistry 中的一個HashMap中。並以 Mapper 介面的 Class 物件作為 Key,以一個攜帶Mapper介面作為屬性的MapperProxyFactory 例項作為value 。MapperProxyFacory從名字來看,好像是一個工廠,用來建立Mapper Proxy的工廠。我們繼續往下看。

Mapper介面的動態代理類的生成

上面我們已經知道,Mapper 介面被到註冊到了MapperRegistry中——放在其名為knowMappers 的HashMap屬性中,我們在呼叫Mapper介面的方法的時候,是這樣的:

BlogMapper mapper = session.getMapper(BlogMapper.class);

這裡,我們跟蹤一下session.getMapper() 方法的程式碼實現,這裡 SqlSession 是一個介面,他有兩個實現類,一個是DefaultSqlSession,另外一個是SqlSessionManager,這裡我們用的是DefaultSqlSession. 為什麼是DefaultSqlSession呢?因為我們在初始化SqlSessionFactory的時候所呼叫的SqlSessionFactoryBuilder的build()方法裡邊配置的就是DefaultSqlSession,所以,我們進入到DefaultSession類中,看看它對session.getMapper(BlogMapper.class)是怎麼實現的:

public class DefaultSqlSession implements SqlSession {
 private Configuration configuration; 
  @Override
 public <T> T getMapper(Class<T> type) {
  return configuration.<T>getMapper(type,this); //最後會去呼叫MapperRegistry.getMapper
 }
}

如程式碼所示,這裡的 getMapper 呼叫了 configuration.getMapper,這一步操作其實最終是呼叫了MapperRegistry,而此前我們已經知道,MapperRegistry是存放了一個HashMap的,我們繼續跟蹤進去看看,那麼這裡的get,肯定是從這個hashMap中取資料。我們來看看程式碼:

public class MapperRegistry {
 
private final Map<Class<?>,MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>,MapperProxyFactory<?>>();// Mapper 對映
 
public <T> T getMapper(Class<T> type,SqlSession sqlSession) {
  final MapperProxyFactory<T> mapperProxyFactory =
                 (MapperProxyFactory<T>) knownMappers.get(type);
  try {
   return mapperProxyFactory.newInstance(sqlSession); // 重點看這裡
  } catch (Exception e) {
  }
 }
}

我們呼叫的session.getMapper(BlogMapper.class);最終會到達上面這個方法,這個方法,根據BlogMapper的class物件,以它為key在knowMappers 中找到了對應的value —— MapperProxyFactory(BlogMapper) 物件,然後呼叫這個物件的newInstance()方法。根據這個名字,我們就能猜到這個方法是建立了一個物件,程式碼是這樣的:

public class MapperProxyFactory<T> { //對映器代理工廠

 private final Class<T> mapperInterface;
 private Map<Method,MapperMethod> methodCache = new ConcurrentHashMap<Method,MapperMethod>();

 public MapperProxyFactory(Class<T> mapperInterface) {
  this.mapperInterface = mapperInterface;
 }
 // 刪除部分程式碼,便於閱讀

 @SuppressWarnings("unchecked")
 protected T newInstance(MapperProxy<T> mapperProxy) {
  //使用了JDK自帶的動態代理生成對映器代理類的物件
  return (T) Proxy.newProxyInstance(
       mapperInterface.getClassLoader(),new Class[] { mapperInterface },mapperProxy);
 }
 public T newInstance(SqlSession sqlSession) {
  final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession,mapperInterface,methodCache);
  return newInstance(mapperProxy);
 }

}

看到這裡,就清楚了,最終是通過Proxy.newProxyInstance產生了一個BlogMapper的代理物件。Mybatis 為了完成 Mapper 介面的實現,運用了代理模式。具體是使用了JDK動態代理,這個Proxy.newProxyInstance方法生成代理類的三個要素是:

  • ClassLoader —— 指定當前介面的載入器即可
  • 當前被代理的介面是什麼 —— 這裡就是 BlogMapper
  • 代理類是什麼 —— 這裡就是 MapperProxy

代理模式中,代理類(MapperProxy)中才真正的完成了方法呼叫的邏輯。我們貼出MapperProxy的程式碼,如下:

public class MapperProxy<T> implements InvocationHandler,Serializable {// 實現了InvocationHandler
 @Override
 public Object invoke(Object proxy,Method method,Object[] args) throws Throwable {
  //代理以後,所有Mapper的方法呼叫時,都會呼叫這個invoke方法
  if (Object.class.equals(method.getDeclaringClass())) {
   try {
    return method.invoke(this,args); // 注意1
   } catch (Throwable t) {
    throw ExceptionUtil.unwrapThrowable(t);
   }
  }
  final MapperMethod mapperMethod = cachedMapperMethod(method); // 使用了快取
  //執行CURD
  return mapperMethod.execute(sqlSession,args); // 注意2
 }  
}

我們呼叫的 Blog blog = mapper.selectBlog(1); 實際上最後是會呼叫這個MapperProxy的invoke方法。這段程式碼中,if 語句先判斷,我們想要呼叫的方法是否來自Object類,這裡的意思就是,如果我們呼叫toString()方法,那麼是不需要做代理增強的,直接還呼叫原來的method.invoke()就行了。只有呼叫selectBlog()之類的方法的時候,才執行增強的呼叫——即mapperMethod.execute(sqlSession,args);這一句程式碼邏輯。

而mapperMethod.execute(sqlSession,args);這句最終就會執行增刪改查了,程式碼如下:

 public Object execute(SqlSession sqlSession,Object[] args) {
  Object result;
  if (SqlCommandType.INSERT == command.getType()) {     //insert 處理,呼叫SqlSession的insert
   Object param = method.convertArgsToSqlCommandParam(args);
   result = rowCountResult(sqlSession.insert(command.getName(),param));
  } else if (SqlCommandType.UPDATE == command.getType()) { // update
   Object param = method.convertArgsToSqlCommandParam(args);
   result = rowCountResult(sqlSession.update(command.getName(),param));
  } else if (SqlCommandType.DELETE == command.getType()) {  // delete
   Object param = method.convertArgsToSqlCommandParam(args);
   result = rowCountResult(sqlSession.delete(command.getName(),param));
  } else if (SqlCommandType.SELECT == command.getType()) {
   // 刪除部分程式碼 
  } else {
   throw new BindingException("Unknown execution method for: " + command.getName());
  }
   // 刪除部分程式碼
  return result;
 }

再往下一層,就是執行JDBC那一套了,獲取連結,執行,得到ResultSet,解析ResultSet對映成JavaBean。

至此,我們已經摸清楚了Blog blog = mapper.selectBlog(1); 中,BlogMapper介面呼叫到得到資料庫資料過程中,Mybaitis 是如何為介面生成實現類的,以及在哪裡出發了最終的CRUD呼叫。實際上,如果我們在呼叫Blog blog = mapper.selectBlog(1);之前,把從slqSession中得到的 mapper 物件打印出來就會看到,輸出大概是這樣的:

com.sun.proxy.$Proxy17

動態代理沒錯吧,Java動態代理實在是太美妙了。

總結

上面我們用層層深入的方式摸清楚了 Mapper介面是如何找到實現類的。我們分析了 Mapper介面是如何註冊的,Mapper介面是如何產生動態代理物件的,Maper介面方法最終是如何執行的。總結起來主要就是這幾個點:

1. Mapper 介面在初始SqlSessionFactory 註冊的。

2. Mapper 介面註冊在了名為 MapperRegistry 類的 HashMap中, key = Mapper class value = 建立當前Mapper的工廠。

3. Mapper 註冊之後,可以從SqlSession中get

4. SqlSession.getMapper 運用了 JDK動態代理,產生了目標Mapper介面的代理物件。

5. 動態代理的 代理類是 MapperProxy ,這裡邊最終完成了增刪改查方法的呼叫。

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支援我們。