如何實(shí)現(xiàn)映射器的注冊(cè)和使用
一、前言
如何面對(duì)復(fù)雜系統(tǒng)的設(shè)計(jì)?
我們可以把 Spring、Mybatis、Dubbo 這樣的大型框架或者一些公司內(nèi)部的較核心項(xiàng)目,都可以稱(chēng)為復(fù)雜的系統(tǒng)。這樣的工程也不在是初學(xué)編程手里的玩具項(xiàng)目,沒(méi)有所謂的CRUD,更多時(shí)候要面對(duì)的都是對(duì)系統(tǒng)分層的結(jié)構(gòu)設(shè)計(jì)和聚合邏輯功能的實(shí)現(xiàn),再通過(guò)層層轉(zhuǎn)換進(jìn)行實(shí)現(xiàn)和調(diào)用。
這對(duì)于很多剛上道的小碼農(nóng)來(lái)說(shuō),會(huì)感覺(jué)非常難受,不知道要從哪下手,但又想著可以一口吃個(gè)胖子。其實(shí)這是不現(xiàn)實(shí)的,因?yàn)檫@些復(fù)雜系統(tǒng)中的框架中有太多的內(nèi)容你還沒(méi)用了解和熟悉,越是硬搞越難受,信心越受打擊。
其實(shí)對(duì)于解決這類(lèi)復(fù)雜的項(xiàng)目問(wèn)題,核心在于要將主干問(wèn)題點(diǎn)縮小,具體的手段包括:分治、抽象和知識(shí)。運(yùn)用設(shè)計(jì)模式和設(shè)計(jì)原則等相關(guān)知識(shí),把問(wèn)題空間合理切割為若干子問(wèn)題,問(wèn)題越小也就越容易理解和處理。就像你可以把很多內(nèi)容做成單個(gè)獨(dú)立的案例一樣,最終在進(jìn)行聚合使用。
二、目標(biāo)
在上一章節(jié)我們初步的了解了怎么給一個(gè)接口類(lèi)生成對(duì)應(yīng)的映射器代理,并在代理中完成一些用戶對(duì)接口方法的調(diào)用處理。雖然我們已經(jīng)看到了一個(gè)核心邏輯的處理方式,但在使用上還是有些刀耕火種的,包括:需要編碼告知 MapperProxyFactory 要對(duì)哪個(gè)接口進(jìn)行代理,以及自己編寫(xiě)一個(gè)假的 SqlSession 處理實(shí)際調(diào)用接口時(shí)的返回結(jié)果。
那么結(jié)合這兩塊問(wèn)題點(diǎn),我們本章節(jié)要對(duì)映射器的注冊(cè)提供注冊(cè)機(jī)處理,滿足用戶可以在使用的時(shí)候提供一個(gè)包的路徑即可完成掃描和注冊(cè)。與此同時(shí)需要對(duì) SqlSession 進(jìn)行規(guī)范化處理,讓它可以把我們的映射器代理和方法調(diào)用進(jìn)行包裝,建立一個(gè)生命周期模型結(jié)構(gòu),便于后續(xù)的內(nèi)容的添加。
三、設(shè)計(jì)
鑒于我們希望把整個(gè)工程包下關(guān)于數(shù)據(jù)庫(kù)操作的 DAO 接口與 Mapper 映射器關(guān)聯(lián)起來(lái),那么就需要包裝一個(gè)可以掃描包路徑的完成映射的注冊(cè)器類(lèi)。
當(dāng)然我們還要把上一章節(jié)中簡(jiǎn)化的 SqlSession 進(jìn)行完善,由 SqlSession 定義數(shù)據(jù)庫(kù)處理接口和獲取 Mapper 對(duì)象的操作,并把它交給映射器代理類(lèi)進(jìn)行使用。這一部分是對(duì)上一章節(jié)內(nèi)容的完善。
有了 SqlSession 以后,你可以把它理解成一種功能服務(wù),有了功能服務(wù)以后還需要給這個(gè)功能服務(wù)提供一個(gè)工廠,來(lái)對(duì)外統(tǒng)一提供這類(lèi)服務(wù)。比如我們?cè)? Mybatis 中非常常見(jiàn)的操作,開(kāi)啟一個(gè) SqlSession。整個(gè)設(shè)計(jì)可以如圖 3-1:
圖 3-1 映射器的注冊(cè)和使用
- 以包裝接口提供映射器代理類(lèi)為目標(biāo),補(bǔ)全映射器注冊(cè)機(jī) MapperRegistry,自動(dòng)掃描包下接口并把每個(gè)接口類(lèi)映射的代理類(lèi)全部存入映射器代理的 HashMap 緩存中。
- 而 SqlSession、SqlSessionFactory 是在此注冊(cè)映射器代理的上次層使用標(biāo)準(zhǔn)定義和對(duì)外服務(wù)提供的封裝,便于用戶使用。我們把使用方當(dāng)成用戶 經(jīng)過(guò)這樣的封裝就就可以更加方便我們后續(xù)在框架上功能的繼續(xù)擴(kuò)展了,也希望大家可以在學(xué)習(xí)的過(guò)程中對(duì)這樣的設(shè)計(jì)結(jié)構(gòu)有一些思考,它可以幫助你解決一些業(yè)務(wù)功能開(kāi)發(fā)過(guò)程中的領(lǐng)域服務(wù)包裝。
四、實(shí)現(xiàn)
1. 工程結(jié)構(gòu)
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
工程源碼:https://t.zsxq.com/bmqNFQ7。
映射器標(biāo)準(zhǔn)定義實(shí)現(xiàn)關(guān)系,如圖 3-2:
圖 3-2 映射器標(biāo)準(zhǔn)定義實(shí)現(xiàn)關(guān)系
- MapperRegistry 提供包路徑的掃描和映射器代理類(lèi)注冊(cè)機(jī)服務(wù),完成接口對(duì)象的代理類(lèi)注冊(cè)處理。
- SqlSession、DefaultSqlSession 用于定義執(zhí)行 SQL 標(biāo)準(zhǔn)、獲取映射器以及將來(lái)管理事務(wù)等方面的操作?;疚覀兤匠J褂?Mybatis 的 API 接口也都是從這個(gè)接口類(lèi)定義的方法進(jìn)行使用的。
- SqlSessionFactory 是一個(gè)簡(jiǎn)單工廠模式,用于提供 SqlSession 服務(wù),屏蔽創(chuàng)建細(xì)節(jié),延遲創(chuàng)建過(guò)程。
2. 映射器注冊(cè)機(jī)
源碼詳見(jiàn):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 必須是接口才會(huì)注冊(cè) */
if (type.isInterface()) {
if (hasMapper(type)) {
// 如果重復(fù)添加了,報(bào)錯(cuò)
throw new RuntimeException("Type " + type + " is already known to the MapperRegistry.");
}
// 注冊(cè)映射器代理工廠
knownMappers.put(type, new MapperProxyFactory<>(type));
}
}
public void addMappers(String packageName) {
Set<Class<?>> mapperSet = ClassScanner.scanPackage(packageName);
for (Class<?> mapperClass : mapperSet) {
addMapper(mapperClass);
}
}
}
- MapperRegistry 映射器注冊(cè)類(lèi)的核心主要在于提供了 ClassScanner.scanPackage 掃描包路徑,調(diào)用 addMapper 方法,給接口類(lèi)創(chuàng)建 MapperProxyFactory 映射器代理類(lèi),并寫(xiě)入到 knownMappers 的 HashMap 緩存中。
- 另外就是這個(gè)類(lèi)也提供了對(duì)應(yīng)的 getMapper 獲取映射器代理類(lèi)的方法,其實(shí)這步就包裝了我們上一章節(jié)手動(dòng)操作實(shí)例化的過(guò)程,更加方便在 DefaultSqlSession 中獲取 Mapper 時(shí)進(jìn)行使用。
3. SqlSession 標(biāo)準(zhǔn)定義和實(shí)現(xiàn)
源碼詳見(jiàn):cn.bugstack.mybatis.session.SqlSession。
public interface SqlSession {
/**
* Retrieve a single row mapped from the statement key
* 根據(jù)指定的SqlID獲取一條記錄的封裝對(duì)象
*
* @param <T> the returned object type 封裝之后的對(duì)象類(lèi)型
* @param statement sqlID
* @return Mapped object 封裝之后的對(duì)象
*/
<T> T selectOne(String statement);
/**
* Retrieve a single row mapped from the statement key and parameter.
* 根據(jù)指定的SqlID獲取一條記錄的封裝對(duì)象,只不過(guò)這個(gè)方法容許我們可以給sql傳遞一些參數(shù)
* 一般在實(shí)際使用中,這個(gè)參數(shù)傳遞的是pojo,或者M(jìn)ap或者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.
* 得到映射器,這個(gè)巧妙的使用了泛型,使得類(lèi)型安全
*
* @param <T> the mapper type
* @param type Mapper interface class
* @return a mapper bound to this SqlSession
*/
<T> T getMapper(Class<T> type);
}
在 SqlSession 中定義用來(lái)執(zhí)行 SQL、獲取映射器對(duì)象以及后續(xù)管理事務(wù)操作的標(biāo)準(zhǔn)接口。
目前這個(gè)接口中對(duì)于數(shù)據(jù)庫(kù)的操作僅僅只提供了 selectOne,后續(xù)還會(huì)有相應(yīng)其他方法的定義。
源碼詳見(jiàn):cn.bugstack.mybatis.session.defaults。
public class DefaultSqlSession implements SqlSession {
/**
* 映射器注冊(cè)機(jī)
*/
private MapperRegistry mapperRegistry;
@Override
public <T> T selectOne(String statement, Object parameter) {
return (T) ("你被代理了!" + "方法:" + statement + " 入?yún)ⅲ? + parameter);
}
@Override
public <T> T getMapper(Class<T> type) {
return mapperRegistry.getMapper(type, this);
}
}
- 通過(guò) DefaultSqlSession 實(shí)現(xiàn)類(lèi)對(duì) SqlSession 接口進(jìn)行實(shí)現(xiàn)。
- getMapper 方法中獲取映射器對(duì)象是通過(guò) MapperRegistry 類(lèi)進(jìn)行獲取的,后續(xù)這部分會(huì)被配置類(lèi)進(jìn)行替換。
- 在 selectOne 中是一段簡(jiǎn)單的內(nèi)容返回,目前還沒(méi)有與數(shù)據(jù)庫(kù)進(jìn)行關(guān)聯(lián),這部分在我們漸進(jìn)式的開(kāi)發(fā)過(guò)程中逐步實(shí)現(xiàn)。
4. SqlSessionFactory 工廠定義和實(shí)現(xiàn)
源碼詳見(jiàn):cn.bugstack.mybatis.session.SqlSessionFactory。
public interface SqlSessionFactory {
/**
* 打開(kāi)一個(gè) session
* @return SqlSession
*/
SqlSession openSession();
}
- 這其實(shí)就是一個(gè)簡(jiǎn)單工廠的定義,在工廠中提供接口實(shí)現(xiàn)類(lèi)的能力,也就是 SqlSessionFactory 工廠中提供的開(kāi)啟 SqlSession 的能力。
源碼詳見(jiàn):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);
}
}
- 默認(rèn)的簡(jiǎn)單工廠實(shí)現(xiàn),處理開(kāi)啟 SqlSession 時(shí),對(duì) DefaultSqlSession 的創(chuàng)建以及傳遞 mapperRegistry,這樣就可以在使用 SqlSession 時(shí)獲取每個(gè)代理類(lèi)的映射器對(duì)象了。
五、測(cè)試
1. 事先準(zhǔn)備
在同一個(gè)包路徑下,提供2個(gè)以上的 Dao 接口:
public interface ISchoolDao {
String querySchoolName(String uId);
}
public interface IUserDao {
String queryUserName(String uId);
Integer queryUserAge(String uId);
}
2. 單元測(cè)試
@Test
public void test_MapperProxyFactory() {
// 1. 注冊(cè) 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. 獲取映射器對(duì)象
IUserDao userDao = sqlSession.getMapper(IUserDao.class);
// 4. 測(cè)試驗(yàn)證
String res = userDao.queryUserName("10001");
logger.info("測(cè)試結(jié)果:{}", res);
}
在單元測(cè)試中通過(guò)注冊(cè)機(jī)掃描包路徑注冊(cè)映射器代理對(duì)象,并把注冊(cè)機(jī)傳遞給 SqlSessionFactory 工廠,這樣完成一個(gè)鏈接過(guò)程。
之后通過(guò) SqlSession 獲取對(duì)應(yīng) DAO 類(lèi)型的實(shí)現(xiàn)類(lèi),并進(jìn)行方法驗(yàn)證。
測(cè)試結(jié)果
22:43:23.254 [main] INFO cn.bugstack.mybatis.test.ApiTest - 測(cè)試結(jié)果:你被代理了!方法:queryUserName 入?yún)ⅲ篬Ljava.lang.Object;@50cbc42f
Process finished with exit code 0
通過(guò)測(cè)試大家可以看到,目前我們已經(jīng)在一個(gè)有 Mybatis 影子的手寫(xiě) ORM 框架中,完成了代理類(lèi)的注冊(cè)和使用過(guò)程。
六、總結(jié)
首先要從設(shè)計(jì)結(jié)構(gòu)上了解工廠模式對(duì)具體功能結(jié)構(gòu)的封裝,屏蔽過(guò)程細(xì)節(jié),限定上下文關(guān)系,把對(duì)外的使用減少耦合。
從這個(gè)過(guò)程上讀者伙伴也能發(fā)現(xiàn),使用 SqlSessionFactory 的工廠實(shí)現(xiàn)類(lèi)包裝了 SqlSession 的標(biāo)準(zhǔn)定義實(shí)現(xiàn)類(lèi),并由 SqlSession 完成對(duì)映射器對(duì)象的注冊(cè)和使用。
本章學(xué)習(xí)要注意幾個(gè)重要的知識(shí)點(diǎn),包括:映射器、代理類(lèi)、注冊(cè)機(jī)、接口標(biāo)準(zhǔn)、工廠模式、上下文。這些工程開(kāi)發(fā)的技巧都是在手寫(xiě) Mybatis 的過(guò)程中非常重要的部分,了解和熟悉才能更好的在自己的業(yè)務(wù)中進(jìn)行使用。