作者:小傅哥
沉澱、分享、成長,讓自己和他人都能有所收獲!????
一、前言
如何面對複雜系統的設計?
我們可以把 Spring、Mybatis、Dubbo 這樣的大型架構或者一些公司内部的較核心項目,都可以稱為複雜的系統。這樣的工程也不在是初學程式設計手裡的玩具項目,沒有所謂的CRUD,更多時候要面對的都是對系統分層的結構設計和聚合邏輯功能的實作,再通過層層轉換進行實作和調用。
這對于很多剛上道的小碼農來說,會感覺非常難受,不知道要從哪下手,但又想着可以一口吃個胖子。其實這是不現實的,因為這些複雜系統中的架構中有太多的内容你還沒用了解和熟悉,越是硬搞越難受,信心越受打擊。
其實對于解決這類複雜的項目問題,核心在于要将主幹問題點縮小,具體的手段包括:分治、抽象和知識。運用設計模式和設計原則等相關知識,把問題空間合理切割為若幹子問題,問題越小也就越容易了解和處理。就像你可以把很多内容做成單個獨立的案例一樣,最終在進行聚合使用。
二、目标
在上一章節我們初步的了解了怎麼給一個接口類生成對應的映射器代理,并在代理中完成一些使用者對接口方法的調用處理。雖然我們已經看到了一個核心邏輯的處理方式,但在使用上還是有些刀耕火種的,包括:需要編碼告知 MapperProxyFactory 要對哪個接口進行代理,以及自己編寫一個假的 SqlSession 處理實際調用接口時的傳回結果。
那麼結合這兩塊問題點,我們本章節要對映射器的注冊提供注冊機處理,滿足使用者可以在使用的時候提供一個包的路徑即可完成掃描和注冊。與此同時需要對 SqlSession 進行規範化處理,讓它可以把我們的映射器代理和方法調用進行包裝,建立一個生命周期模型結構,便于後續的内容的添加。
三、設計
鑒于我們希望把整個工程包下關于資料庫操作的 DAO 接口與 Mapper 映射器關聯起來,那麼就需要包裝一個可以掃描包路徑的完成映射的注冊器類。
當然我們還要把上一章節中簡化的 SqlSession 進行完善,由 SqlSession 定義資料庫處理接口和擷取 Mapper 對象的操作,并把它交給映射器代理類進行使用。這一部分是對上一章節内容的完善
有了 SqlSession 以後,你可以把它了解成一種功能服務,有了功能服務以後還需要給這個功能服務提供一個工廠,來對外統一提供這類服務。比如我們在 Mybatis 中非常常見的操作,開啟一個 SqlSession。整個設計可以如圖 3-1
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiI0gTMx81dsQWZ4lmZf1GLlpXazVmcvwFciV2dsQXYtJ3bm9CX9s2RkBnVHFmb1clWvB3MaVnRtp1XlBXe0xCMy81dvRWYoNHLwEzX5xCMx8FesU2cfdGLwMzX0xiRGZkRGZ0Xy9GbvNGLpZTY1EmMZVDUSFTU4VFRR9Fd4VGdsYTMfVmepNHLrJXYtJXZ0F2dvwVZnFWbp1zczV2YvJHctM3cv1Ce-cmbw5iNzkzNxYjYhRzMlljMhVTNyYzX4MjMwADMzEzLcRDMyIDMy8CXn9Gbi9CXzV2Zh1WavwVbvNmLvR3YxUjLyM3Lc9CX6MHc0RHaiojIsJye.png)
- 以包裝接口提供映射器代理類為目标,補全映射器注冊機
,自動掃描包下接口并把每個接口類映射的代理類全部存入映射器代理的 HashMap 緩存中。MapperRegistry
- 而 SqlSession、SqlSessionFactory 是在此注冊映射器代理的上次層使用标準定義和對外服務提供的封裝,便于使用者使用。我們把使用方當成使用者經過這樣的封裝就就可以更加友善我們後續在架構上功能的繼續擴充了,也希望大家可以在學習的過程中對這樣的設計結構有一些思考,它可以幫助你解決一些業務功能開發過程中的領域服務包裝。
四、實作
1. 工程結構
mybatis-step-02
└── src
├── main
│ └── java
│ └── cn.bugstack.mybatis
│ ├── binding
│ │ ├── MapperProxy.java
│ │ ├── MapperProxyFactory.java
│ │ └── MapperRegistry.java
│ └── session
│ ├── defaults
│ │ ├── DefaultSqlSession.java
│ │ └── DefaultSqlSessionFactory.java
│ ├── SqlSession.java
│ └── SqlSessionFactory.java
└── test
└── java
└── cn.bugstack.mybatis.test.dao
├── dao
│ ├── ISchoolDao.java
│ └── IUserDao.java
└── ApiTest.java
工程源碼:
映射器标準定義實作關系,如圖 3-2
- MapperRegistry 提供包路徑的掃描和映射器代理類注冊機服務,完成接口對象的代理類注冊處理。
- SqlSession、DefaultSqlSession 用于定義執行 SQL 标準、擷取映射器以及将來管理事務等方面的操作。基本我們平常使用 Mybatis 的 API 接口也都是從這個接口類定義的方法進行使用的。
- SqlSessionFactory 是一個簡單工廠模式,用于提供 SqlSession 服務,屏蔽建立細節,延遲建立過程。
2. 映射器注冊機
源碼詳見:
cn.bugstack.mybatis.binding.MapperRegistry
public class MapperRegistry {
/**
* 将已添加的映射器代理加入到 HashMap
*/
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new RuntimeException("Type " + type + " is not known to the MapperRegistry.");
}
try {
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new RuntimeException("Error getting mapper instance. Cause: " + e, e);
}
}
public <T> void addMapper(Class<T> type) {
/* Mapper 必須是接口才會注冊 */
if (type.isInterface()) {
if (hasMapper(type)) {
// 如果重複添加了,報錯
throw new RuntimeException("Type " + type + " is already known to the MapperRegistry.");
}
// 注冊映射器代理工廠
knownMappers.put(type, new MapperProxyFactory<>(type));
}
}
public void addMappers(String packageName) {
Set<Class<?>> mapperSet = ClassScanner.scanPackage(packageName);
for (Class<?> mapperClass : mapperSet) {
addMapper(mapperClass);
}
}
}
- MapperRegistry 映射器注冊類的核心主要在于提供了
掃描包路徑,調用ClassScanner.scanPackage
方法,給接口類建立addMapper
映射器代理類,并寫入到 knownMappers 的 HashMap 緩存中。MapperProxyFactory
- 另外就是這個類也提供了對應的 getMapper 擷取映射器代理類的方法,其實這步就包裝了我們上一章節手動操作執行個體化的過程,更加友善在 DefaultSqlSession 中擷取 Mapper 時進行使用。
3. SqlSession 标準定義和實作
源碼詳見:
cn.bugstack.mybatis.session.SqlSession
public interface SqlSession {
/**
* Retrieve a single row mapped from the statement key
* 根據指定的SqlID擷取一條記錄的封裝對象
*
* @param <T> the returned object type 封裝之後的對象類型
* @param statement sqlID
* @return Mapped object 封裝之後的對象
*/
<T> T selectOne(String statement);
/**
* Retrieve a single row mapped from the statement key and parameter.
* 根據指定的SqlID擷取一條記錄的封裝對象,隻不過這個方法容許我們可以給sql傳遞一些參數
* 一般在實際使用中,這個參數傳遞的是pojo,或者Map或者ImmutableMap
*
* @param <T> the returned object type
* @param statement Unique identifier matching the statement to use.
* @param parameter A parameter object to pass to the statement.
* @return Mapped object
*/
<T> T selectOne(String statement, Object parameter);
/**
* 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);
}
- 在 SqlSession 中定義用來執行 SQL、擷取映射器對象以及後續管理事務操作的标準接口。
- 目前這個接口中對于資料庫的操作僅僅隻提供了 selectOne,後續還會有相應其他方法的定義。
源碼詳見:
cn.bugstack.mybatis.session.defaults
public class DefaultSqlSession implements SqlSession {
/**
* 映射器注冊機
*/
private MapperRegistry mapperRegistry;
@Override
public <T> T selectOne(String statement, Object parameter) {
return (T) ("你被代理了!" + "方法:" + statement + " 入參:" + parameter);
}
@Override
public <T> T getMapper(Class<T> type) {
return mapperRegistry.getMapper(type, this);
}
}
- 通過 DefaultSqlSession 實作類對 SqlSession 接口進行實作。
- getMapper 方法中擷取映射器對象是通過 MapperRegistry 類進行擷取的,後續這部分會被配置類進行替換。
- 在 selectOne 中是一段簡單的内容傳回,目前還沒有與資料庫進行關聯,這部分在我們漸進式的開發過程中逐漸實作。
4. SqlSessionFactory 工廠定義和實作
源碼詳見:
cn.bugstack.mybatis.session.SqlSessionFactory
public interface SqlSessionFactory {
/**
* 打開一個 session
* @return SqlSession
*/
SqlSession openSession();
}
- 這其實就是一個簡單工廠的定義,在工廠中提供接口實作類的能力,也就是 SqlSessionFactory 工廠中提供的開啟 SqlSession 的能力。
源碼詳見:
cn.bugstack.mybatis.session.defaults.DefaultSqlSessionFactory
public class DefaultSqlSessionFactory implements SqlSessionFactory {
private final MapperRegistry mapperRegistry;
public DefaultSqlSessionFactory(MapperRegistry mapperRegistry) {
this.mapperRegistry = mapperRegistry;
}
@Override
public SqlSession openSession() {
return new DefaultSqlSession(mapperRegistry);
}
}
- 預設的簡單工廠實作,處理開啟 SqlSession 時,對 DefaultSqlSession 的建立以及傳遞 mapperRegistry,這樣就可以在使用 SqlSession 時擷取每個代理類的映射器對象了。
五、測試
1. 事先準備
public interface ISchoolDao {
String querySchoolName(String uId);
}
public interface IUserDao {
String queryUserName(String uId);
Integer queryUserAge(String uId);
}
2. 單元測試
@Test
public void test_MapperProxyFactory() {
// 1. 注冊 Mapper
MapperRegistry registry = new MapperRegistry();
registry.addMappers("cn.bugstack.mybatis.test.dao");
// 2. 從 SqlSession 工廠擷取 Session
SqlSessionFactory sqlSessionFactory = new DefaultSqlSessionFactory(registry);
SqlSession sqlSession = sqlSessionFactory.openSession();
// 3. 擷取映射器對象
IUserDao userDao = sqlSession.getMapper(IUserDao.class);
// 4. 測試驗證
String res = userDao.queryUserName("10001");
logger.info("測試結果:{}", res);
}
- 在單元測試中通過注冊機掃描包路徑注冊映射器代理對象,并把注冊機傳遞給 SqlSessionFactory 工廠,這樣完成一個連結過程。
- 之後通過 SqlSession 擷取對應 DAO 類型的實作類,并進行方法驗證。
22:43:23.254 [main] INFO cn.bugstack.mybatis.test.ApiTest - 測試結果:你被代理了!方法:queryUserName 入參:[Ljava.lang.Object;@50cbc42f
Process finished with exit code 0
- 通過測試大家可以看到,目前我們已經在一個有 Mybatis 影子的手寫 ORM 架構中,完成了代理類的注冊和使用過程。
六、總結
- 首先要從設計結構上了解工廠模式對具體功能結構的封裝,屏蔽過程細節,限定上下文關系,把對外的使用減少耦合。
- 從這個過程上讀者夥伴也能發現,使用 SqlSessionFactory 的工廠實作類包裝了 SqlSession 的标準定義實作類,并由 SqlSession 完成對映射器對象的注冊和使用。
- 本章學習要注意幾個重要的知識點,包括:映射器、代理類、注冊機、接口标準、工廠模式、上下文。這些工程開發的技巧都是在手寫 Mybatis 的過程中非常重要的部分,了解和熟悉才能更好的在自己的業務中進行使用。