作者:小傅哥
沉澱、分享、成長,讓自己和他人都能有所收獲!????
一、前言
你是怎麼面對功能疊代的?
其實很多程式員在剛開始做程式設計或者新加入一家公司時,都沒有多少機會可以做一個新項目,大部分時候都是在老項目上不斷的疊代更新。在這個過程你可能要學習N個前人留下的各式各樣的風格迥異的代碼片段,在這些縱橫交錯的流程中,找到一席之地,把自己的
ifelse
加進去。
雖然這樣胡亂的加
ifelse
,剛上手就“擺爛”的心态,讓人很難受。但要想在已經被壓縮的工期下,還能傳遞出高品質的代碼其實也很難完成,是以一部分研發被逼到能用就行,能跑就可以。
但說回來,其實不能逐漸清理一片屎山,讓代碼在你的手上逐漸清晰、整潔、幹淨,很多時候也是作為碼農自身經驗的不足,不懂得系統重構、不了解設計原則、不熟悉業務背景、不清楚産品走向等等原因造成的。是以最好的辦法是提升自身的能力,沒接到一次需求都有一些技術上的改變,既然它是屎山,那就當做打怪更新了,修一點、改一塊、補一片,總會在你手上越來越易于維護和擴充的。
二、目标
在我們漸進式的逐漸實作 Mybatis 架構過程中,首先我們要有一個目标導向的思路,也就是說 Mybatis 的核心邏輯怎麼實作。
其實我們可以把這樣一個 ORM 架構的目标,簡單的描述成是為了給一個接口提供代理類,類中包括了對 Mapper 也就是 xml 檔案中的 SQL 資訊(
類型
、
入參
、
出參
、
條件
)進行解析和處理,這個處理過程就是對資料庫的操作以及傳回對應的結果給到接口。如圖 4-1
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiI0gTMx81dsQWZ4lmZf1GLlpXazVmcvwFciV2dsQXYtJ3bm9CX9s2RkBnVHFmb1clWvB3MaVnRtp1XlBXe0xCMy81dvRWYoNHLwEzX5xCMx8FesU2cfdGLwMzX0xiRGZkRGZ0Xy9GbvNGLpZTY1EmMZVDUSFTU4VFRR9Fd4VGdsYTMfVmepNHLrJXYtJXZ0F2dvwVZnFWbp1zczV2YvJHctM3cv1Ce-cmbw5SOxMjN2czY0IWZiljMhVTNyYzX1MjMwADMzEzLcRDMyIDMy8CXn9Gbi9CXzV2Zh1WavwVbvNmLvR3YxUjLyM3Lc9CX6MHc0RHaiojIsJye.png)
那麼按照 ORM 核心流程的執行過程,我們本章節就需要在上一章節的基礎上,繼續擴充對 Mapper 檔案的解析以及提取出對應的 SQL 檔案。并在目前這個階段,可以滿足我們調用 DAO 接口方法的時候,可以傳回 Mapper 中對應的待執行 SQL 語句。為了不至于把整個工程撐大,小傅哥會帶着大家逐漸完成這些内容,是以本章節暫時不會對資料庫進行操作,待後續逐漸實作
三、設計
結合上一章節我們使用了
MapperRegistry
對包路徑進行掃描注冊映射器,并在
DefaultSqlSession
中進行使用。那麼在我們可以把這些命名空間、SQL描述、映射資訊統一維護到每一個 DAO 對應的 Mapper XML 的檔案以後,其實 XML 就是我們的源頭了。通過對 XML 檔案的解析和處理就可以完成 Mapper 映射器的注冊和 SQL 管理。這樣也就更加我們操作和使用了。如圖 4-2
- 首先需要定義
工廠建造者模式類,通過入口 IO 的方式對 XML 檔案進行解析。目前我們主要以解析 SQL 部分為主,并注冊映射器,串聯出整個核心流程的脈絡。SqlSessionFactoryBuilder
- 檔案解析以後會存放到 Configuration 配置類中,接下來你會看到這個配置類會被串聯到整個 Mybatis 流程中,所有内容存放和讀取都離不開這個類。如我們在 DefaultSqlSession 中擷取 Mapper 和執行 selectOne 也同樣是需要在 Configuration 配置類中進行讀取操作。
四、實作
1. 工程結構
mybatis-step-03
└── src
├── main
│ └── java
│ └── cn.bugstack.mybatis
│ ├── binding
│ │ ├── MapperMethod.java
│ │ ├── MapperProxy.java
│ │ ├── MapperProxyFactory.java
│ │ └── MapperRegistry.java
│ ├── builder
│ │ ├── xml
│ │ │ └── XMLConfigBuilder.java
│ │ └── BaseBuilder.java
│ ├── io
│ │ └── Resources.java
│ ├── mapping
│ │ ├── MappedStatement.java
│ │ └── SqlCommandType.java
│ └── session
│ ├── defaults
│ │ ├── DefaultSqlSession.java
│ │ └── DefaultSqlSessionFactory.java
│ ├── Configuration.java
│ ├── SqlSession.java
│ ├── SqlSessionFactory.java
│ └── SqlSessionFactoryBuilder.java
└── test
├── java
│ └── cn.bugstack.mybatis.test.dao
│ ├── dao
│ │ └── IUserDao.java
│ ├── po
│ │ └── User.java
│ └── ApiTest.java
└── resources
├── mapper
│ └──User_Mapper.xml
└── mybatis-config-datasource.xml
工程源碼:
XML 解析和注冊類實作關系,如圖 4-2
- SqlSessionFactoryBuilder 作為整個 Mybatis 的入口,提供建造者工廠,包裝 XML 解析處理,并傳回對應 SqlSessionFactory 處理類。
- 通過解析把 XML 資訊注冊到 Configuration 配置類中,再通過傳遞 Configuration 配置類到各個邏輯處理類裡,包括 DefaultSqlSession 中,這樣就可以在擷取映射器和執行SQL的時候,從配置類中拿到對應的内容了。
2. 建構SqlSessionFactory建造者工廠
源碼詳見:
cn.bugstack.mybatis.session.SqlSessionFactoryBuilder
public class SqlSessionFactoryBuilder {
public SqlSessionFactory build(Reader reader) {
XMLConfigBuilder xmlConfigBuilder = new XMLConfigBuilder(reader);
return build(xmlConfigBuilder.parse());
}
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
}
- SqlSessionFactoryBuilder 是作為整個 Mybatis 的入口類,通過指定解析XML的IO,引導整個流程的啟動。
- 從這個類開始新增加了 XMLConfigBuilder、Configuration 兩個處理類,分别用于解析 XML 和串聯整個流程的對象儲存操作。接下來我們會分别介紹這些新引入的對象。
3. XML 解析處理
源碼詳見:
cn.bugstack.mybatis.builder.xml.XMLConfigBuilder
public class XMLConfigBuilder extends BaseBuilder {
private Element root;
public XMLConfigBuilder(Reader reader) {
// 1. 調用父類初始化Configuration
super(new Configuration());
// 2. dom4j 處理 xml
SAXReader saxReader = new SAXReader();
try {
Document document = saxReader.read(new InputSource(reader));
root = document.getRootElement();
} catch (DocumentException e) {
e.printStackTrace();
}
}
public Configuration parse() {
try {
// 解析映射器
mapperElement(root.element("mappers"));
} catch (Exception e) {
throw new RuntimeException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
return configuration;
}
private void mapperElement(Element mappers) throws Exception {
List<Element> mapperList = mappers.elements("mapper");
for (Element e : mapperList) {
// 解析處理,具體參照源碼
// 添加解析 SQL
configuration.addMappedStatement(mappedStatement);
}
// 注冊Mapper映射器
configuration.addMapper(Resources.classForName(namespace));
}
}
}
- XMLConfigBuilder 核心操作在于初始化 Configuration,因為 Configuration 的使用離解析 XML 和存放是最近的操作,是以放在這裡比較适合。
- 之後就是具體的 parse() 解析操作,并把解析後的資訊,通過 Configuration 配置類進行存放,包括:添加解析 SQL、注冊Mapper映射器。
- 解析配置整體包括:類型别名、插件、對象工廠、對象包裝工廠、設定、環境、類型轉換、映射器,但目前我們還不需要那麼多,是以隻做一些必要的 SQL 解析處理。
4. 通過配置類包裝注冊機和SQL語句
源碼詳見(配置項):
cn.bugstack.mybatis.session.Configuration
public class Configuration {
/**
* 映射注冊機
*/
protected MapperRegistry mapperRegistry = new MapperRegistry(this);
/**
* 映射的語句,存在Map裡
*/
protected final Map<String, MappedStatement> mappedStatements = new HashMap<>();
public <T> void addMapper(Class<T> type) {
mapperRegistry.addMapper(type);
}
public void addMappedStatement(MappedStatement ms) {
mappedStatements.put(ms.getId(), ms);
}
}
在配置類中添加映射器注冊機和映射語句的存放;
- 映射器注冊機是我們上一章節實作的内容,用于注冊 Mapper 映射器鎖提供的操作類。
- 另外一個 MappedStatement 是本章節新添加的 SQL 資訊記錄對象,包括記錄:SQL類型、SQL語句、入參類型、出參類型等。詳細可參照源碼
5. DefaultSqlSession結合配置項擷取資訊
源碼詳見:
cn.bugstack.mybatis.session.defaults.DefaultSqlSession
public class DefaultSqlSession implements SqlSession {
private Configuration configuration;
@Override
public <T> T selectOne(String statement, Object parameter) {
MappedStatement mappedStatement = configuration.getMappedStatement(statement);
return (T) ("你被代理了!" + "\n方法:" + statement + "\n入參:" + parameter + "\n待執行SQL:" + mappedStatement.getSql());
}
@Override
public <T> T getMapper(Class<T> type) {
return configuration.getMapper(type, this);
}
}
- DefaultSqlSession 相對于上一章節,小傅哥這裡把
替換為MapperRegistry mapperRegistry
,這樣才能傳遞更豐富的資訊内容,而不隻是注冊器操作。Configuration configuration
- 之後在 DefaultSqlSession#selectOne、DefaultSqlSession#getMapper 兩個方法中都使用 configuration 來擷取對應的資訊。
- 目前 selectOne 方法中隻是把擷取的資訊進行列印,後續将引入 SQL 執行器進行結果查詢并傳回。
五、測試
1. 事先準備
public interface IUserDao {
String queryUserInfoById(String uId);
}
<mapper namespace="cn.bugstack.mybatis.test.dao.IUserDao">
<select id="queryUserInfoById" parameterType="java.lang.Long" resultType="cn.bugstack.mybatis.test.po.User">
SELECT id, userId, userHead, createTime
FROM user
where id = #{id}
</select>
</mapper>
2. 單元測試
@Test
public void test_SqlSessionFactory() throws IOException {
// 1. 從SqlSessionFactory中擷取SqlSession
Reader reader = Resources.getResourceAsReader("mybatis-config-datasource.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
SqlSession sqlSession = sqlSessionFactory.openSession();
// 2. 擷取映射器對象
IUserDao userDao = sqlSession.getMapper(IUserDao.class);
// 3. 測試驗證
String res = userDao.queryUserInfoById("10001");
logger.info("測試結果:{}", res);
}
- 目前的使用方式就和 Mybatis 非常像了,通過加載 xml 配置檔案,交給 SqlSessionFactoryBuilder 進行建構解析,并擷取 SqlSessionFactory 工廠。這樣就可以順利的開啟 Session 以及完成後續的操作。
07:07:40.519 [main] INFO cn.bugstack.mybatis.test.ApiTest - 測試結果:你被代理了!
方法:cn.bugstack.mybatis.test.dao.IUserDao.queryUserInfoById
入參:[Ljava.lang.Object;@23223dd8
待執行SQL:
SELECT id, userId, userHead, createTime
FROM user
where id = ?
Process finished with exit code 0
- 從測試結果我們可以看到,目前的代理操作已經可以把我們從 XML 中解析的 SQL 資訊進行列印了,後續我們将結合這部分的處理繼續完成資料庫的操作。
六、總結
- 了解 ORM 處理的核心流程,知曉目前我們所處在的步驟和要完成的内容,隻有非常清楚的知道這個代理、封裝、解析和傳回結果的過程才能更好的完成整個架構的實作。
- SqlSessionFactoryBuilder 的引入包裝了整個執行過程,包括:XML 檔案的解析、Configuration 配置類的處理,讓 DefaultSqlSession 可以更加靈活的拿到對應的資訊,擷取 Mapper 和 SQL 語句。
- 另外從整個工程搭建的過程中,可以看到有很多工廠模式、建造者模式、代理模式的使用,也有很多設計原則的運用,這些技巧都可以讓整個工程變得易于維護和易于疊代。這也是研發人員在學習源碼的過程中,非常值得重點關注的地方。