流程解耦,封裝結果集處理器
一、前言
碼農,如何為自己的職業生涯續期?
上班就像打怪升級,拿著一把西瓜刀,從南天門砍到北天門。但時間長了,怪越來越兇了,西瓜刀也不得手了。咋辦,在游戲里大家肯定是想辦法換裝備了、買武器了、學技能了,這樣才能有機會打通更多的關卡。
其實我們作為程序員上班也是一樣的,如果一直都以為這點技術夠寫寫CRUD就夠了,反正現在還能應付的了。但3年后呢、5年后呢,總有一天你的技術根本沒法滿足公司對你現階段的要求,最簡單的CRUD也早已交給了曾經年輕的另外的你。
有人說:“程序員不是技術牛就能一直行!” 但其實技術牛就是行,當你牛到一定的階段,解決別人解決不了的問題,處理別人處理的不了的方案,蝎子粑粑獨一份,誰又能攔得住你呢。在哪里工作都是你自己來定的,你只管技術牛,就能橫著走。
二、目標
延續著上一章節,我們對參數的封裝和調用,使用了策略模式進行解耦處理,本章節將對執行完查詢的結果進行封裝處理。而不是像我們前面章節那樣粗魯的判斷封裝,因為這樣的方式既不能滿足不同類型的優雅擴展,也不以為維護迭代。如圖 11-1 所示
圖 11-1 簡單的結果集處理
- 對于結果集的封裝處理,其實核心在于我們拿到了 Mapper XML 中所配置的返回類型,解析后把從數據庫查詢到的結果,反射到類型實例化的對象上。
- 那么這個過程中,我們需要滿足不同返回類型的處理,比如Long、Double、String、Date等,都要一一與數據庫的類型匹配,與此同時,返回的結果可能是一個普通的基本類型,也可能是我們封裝后的對象類型。并這個結果查詢也不一定只是一條記錄,還可能是多條記錄。那么為了更好的處理這些不同情況下的問題,就需要對流程進行分治和實現,以及在過程中進行抽象化的解耦,這樣才能滿足于我們把不同的返回信息訴求,封裝到對象里去。分治、抽象和知識,來自于人月神話中的康威定律,它是系統設計的第一原則。
三、設計
在我們使用 JDBC 獲取到查詢結果 ResultSet#getObject 可以獲取返回屬性值,但其實 ResultSet 是可以按照不同的屬性類型進行返回結果的,而不是都返回 Object 對象(如圖11-2 所示)。那么其實我們在上一章節中處理屬性信息時候,所開發的 TypeHandler 接口的實現類,就可以擴充返回結果的方法,例如:LongTypeHandler#getResult、StringTypeHandler#getResult 等,這樣我們就可以使用策略模式非常明確的定位到返回的結果,而不需要進行if判斷處理。
圖 11-2 返回類型
再有了這個目標的前提下,就可以通過解析 XML 信息時封裝返回類型到映射器語句類中,MappedStatement#resultMaps 直到執行完 SQL 語句,按照我們的返回結果參數類型,創建對象和使用 MetaObject 反射工具類填充屬性信息。詳細設計如圖 11-3 所示
圖 11-3 封裝結果集處理器
- 首先我們在解析 XML 語句解析構建器中,添加一個 MapperBuilderAssistant 映射器的助手類,方便我們對參數的統一包裝處理,按照職責歸屬的方式進行細分解耦。通過這樣的方式在 MapperBuilderAssistant#setStatementResultMap 中封裝返回結果信息,一般來說我們使用 Mybatis 配置返回對象的時候 ResultType 就能解決大部分問題,而不需要都是配置一個 ResultMap 映射結果。但這里的設計其實是把 ResultType 也按照一個 ResultMap 的方式進行封裝處理,這樣統一一個標準的方式進行包裝,做了到適配的效果,也更加方便后面對這樣的參數進行統一使用。
- 接下來就是執行 JDBC 操作查詢到數據以后,對結果的封裝。那么在 DefaultResultSetHandler 返回結果處理中,首先會按照我們已經解析的到的 ResultType 進行對象的實例化。實例化對象以后再根據解析出來對象中參數的名稱獲取對應的類型,在根據類型找到 TypeHandler 接口實現類,也就是我們前面提到的 LongTypeHandler、StringTypeHandler,因為通過這樣的方式,可以避免 if···else 的判斷,而是直接O(1)時間復雜度定位到對應的類型處理器,在不同的類型處理器中返回結果信息。最終拿到結果再通過前面章節已經開發過的 MetaObject 反射工具類進行屬性信息的設置。metaObject.setValue(property, value)最終填充實例化并設置了屬性內容的結果對象到上下文中,直至處理完成返回最終的結果數據,以此處理完成。
四、實現
1. 工程結構
mybatis-step-10
└── src
├── main
│ └── java
│ └── cn.bugstack.mybatis
│ ├── binding
│ ├── builder
│ │ ├── xml
│ │ │ ├── XMLConfigBuilder.java
│ │ │ ├── XMLMapperBuilder.java
│ │ │ └── XMLStatementBuilder.java
│ │ ├── BaseBuilder.java
│ │ ├── MapperBuilderAssistant.java
│ │ ├── ParameterExpression.java
│ │ ├── SqlSourceBuilder.java
│ │ └── StaticSqlSource.java
│ ├── datasource
│ ├── executor
│ │ ├── resultset
│ │ │ └── ParameterHandler.java
│ │ ├── resultset
│ │ │ ├── DefaultResultContext.java
│ │ │ └── DefaultResultHandler.java
│ │ ├── resultset
│ │ │ ├── DefaultResultSetHandler.java
│ │ │ └── ResultSetHandler.java
│ │ │ └── ResultSetWrapper.java
│ │ ├── statement
│ │ │ ├── BaseStatementHandler.java
│ │ │ ├── PreparedStatementHandler.java
│ │ │ ├── SimpleStatementHandler.java
│ │ │ └── StatementHandler.java
│ │ ├── BaseExecutor.java
│ │ ├── Executor.java
│ │ └── SimpleExecutor.java
│ ├── io
│ ├── mapping
│ │ ├── BoundSql.java
│ │ ├── Environment.java
│ │ ├── MappedStatement.java
│ │ ├── ParameterMapping.java
│ │ ├── ResultMap.java
│ │ ├── ResultMapping.java
│ │ ├── SqlCommandType.java
│ │ └── SqlSource.java
│ ├── parsing
│ ├── reflection
│ ├── scripting
│ ├── session
│ │ ├── defaults
│ │ │ ├── DefaultSqlSession.java
│ │ │ └── DefaultSqlSessionFactory.java
│ │ ├── Configuration.java
│ │ ├── ResultContext.java
│ │ ├── ResultHandler.java
│ │ ├── RowBounds.java
│ │ ├── SqlSession.java
│ │ ├── SqlSessionFactory.java
│ │ ├── SqlSessionFactoryBuilder.java
│ │ └── TransactionIsolationLevel.java
│ ├── transaction
│ └── type
│ ├── BaseTypeHandler.java
│ ├── JdbcType.java
│ ├── LongTypeHandler.java
│ ├── StringTypeHandler.java
│ ├── TypeAliasRegistry.java
│ ├── TypeHandler.java
│ └── TypeHandlerRegistry.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
流程解耦,封裝結果集處理器核心類關系,如圖 11-4 所示
圖 11-4 封裝結果集處理器核心類關系
- 在 XML 語句構建器中使用映射構建器助手,包裝映射器語句入參、出參的封裝處理。通過此處功能職責的切割,滿足不同邏輯單元的擴展。MapperBuilderAssistant#setStatementResultMap 處理 ResultType/ResultMap 的封裝信息。
- 入參信息的解析會存放到映射語句 MappedStatement 類中,這樣隨著 DefaultSqlSession#selectOne 具體方法的執行時,就可以通過 statement 從配置項中獲取到對應的 MappedStatement 信息,所以這里的設計是符合一個充血模型結構的領域功能聚合。
- 最后就是實現了 ResultSetHandler 結果集處理器接口的 DefaultResultSetHandler 實現類,對查詢結果的封裝處理,這里主要分為按照解析出來的 resultType 類型進行實例化對象,之后根據對象的屬性信息尋找對應的處理策略,避免if···else判斷的方式獲取對應的結果,當對象和屬性都準備完畢后,就可以使用 MetaObject 元對象反射工具類進行屬性填充,形成一個完整的結果對象,并寫入到結果上下文中 DefaultResultContext 返回。
2. 出參參數處理
鑒于對 XML 語句構建器中解析語句后的信息封裝會逐步增多,所以這里需要引入映射構建器助手對類中方法的職責進行劃分,降低一個方法塊內的邏輯復雜度。這樣的方式也更加利于代碼的維護和擴展。
2.1 結果映射封裝
熟悉使用 Mybatis 的讀者都清楚的知道,在一條語句配置中需要有包括一個返回類型的配置,這個返回類型可以是通過 resultType 配置,也可以使用 resultMap 進行處理,而無論使用哪種方式其實最終都會被封裝成統一的 ResultMap 結果映射類。
那么一般我們配置 ResultMap 都是配置了字段的映射,所以實際的代碼開發中 ResultMap 還會包含 ResultMapping 也就是每一個字段的映射信息,包括:colum、javaType、jdbcType 等。由于本章節暫時還不涉及到 ResultMap 的使用,所以這里我們先只是建好基本的地基結構就可以。
源碼詳見:cn.bugstack.mybatis.mapping.ResultMap
public class ResultMap {
private String id;
private Class<?> type;
private List<ResultMapping> resultMappings;
private Set<String> mappedColumns;
//...
}
ResultMap 就是一個簡單的返回結果信息映射類,并提供了建造者方法,方便外部使用。沒有太多的邏輯行為,具體可以參照源碼。
2.2 構建器助手
MapperBuilderAssistant 構建器助手專門為創建 MappedStatement 映射語句類而服務的,在這個類中封裝了入參和出參的映射、以及把這些配置信息寫入到 Configuration 配置項中。
源碼詳見:cn.bugstack.mybatis.builder.MapperBuilderAssistant
public class MapperBuilderAssistant extends BaseBuilder {
/**
* 添加映射器語句
*/
public MappedStatement addMappedStatement(
String id,
SqlSource sqlSource,
SqlCommandType sqlCommandType,
Class<?> parameterType,
String resultMap,
Class<?> resultType,
LanguageDriver lang
) {
// 給id加上namespace前綴:cn.bugstack.mybatis.test.dao.IUserDao.queryUserInfoById
id = applyCurrentNamespace(id, false);
MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlCommandType, sqlSource, resultType);
// 結果映射,給 MappedStatement#resultMaps
setStatementResultMap(resultMap, resultType, statementBuilder);
MappedStatement statement = statementBuilder.build();
// 映射語句信息,建造完存放到配置項中
configuration.addMappedStatement(statement);
return statement;
}
private void setStatementResultMap(
String resultMap,
Class<?> resultType,
MappedStatement.Builder statementBuilder) {
List<ResultMap> resultMaps = new ArrayList<>();
/*
* 通常使用 resultType 即可滿足大部分場景
* <select id="queryUserInfoById" resultType="cn.bugstack.mybatis.test.po.User">
* 使用 resultType 的情況下,Mybatis 會自動創建一個 ResultMap,基于屬性名稱映射列到 JavaBean 的屬性上。
*/
ResultMap.Builder inlineResultMapBuilder = new ResultMap.Builder(
configuration,
statementBuilder.id() + "-Inline",
resultType,
new ArrayList<>());
resultMaps.add(inlineResultMapBuilder.build());
statementBuilder.resultMaps(resultMaps);
}
}
- 在映射構建器助手中,提供了添加映射器語句的方法,在這個方法中更加標準的封裝了入參和出參信息。如果這些內容全部都堆砌到 XMLStatementBuilder 語句構建器的解析中,就會顯得非常臃腫不易于維護了
- 在 MapperBuilderAssistant#setStatementResultMap 方法中,其實它只是一個非常簡單的結果映射建造的過程,無論是否為 ResultMap 都會進行這樣的封裝處理。并最終把創建的信息寫入到 MappedStatement 映射語句類中。
2.3 調用助手類
接下來我們就可以清理 XMLStatementBuilder 語句構建器中解析后,映射語句類的構建和存放處理流程。通過使用助手類,統一封裝參數信息。
源碼詳見:cn.bugstack.mybatis.builder.xml.XMLStatementBuilder
- 與上一章節相比,對于這部分的解析后的結果處理的職責內容,劃分到了新增加的助手類中,這種實現方式在 Mybatis 的源碼中還是非常多的,大部分的內容處理,都會提供一個助手類進行操作。
3. 查詢結果封裝
從 DefaultSqlSession 調用 Executor 語句執行器,一直到 PreparedStatementHandler 預處理語句處理,最后就是 DefaultResultSetHandler 結果信息的封裝。
前面章節對此處的封裝處理,并沒有解耦的操作,只是簡單的 JDBC 使用通過查詢結果,反射處理返回信息就結束了。如果是使用這樣的一個簡單的 if···else 面向過程方式進行開發,那么后續所需要滿足 Mybatis 的全部封裝對象功能,就會變得特別吃力,一個方法塊也會越來越大。
所以這一部分的內容處理是需要被解耦,分為;對象的實例化、結果信息的封裝、策略模式的處理、寫入上下文返回等操作,只有通過這樣的解耦流程,才能更加方便的擴展流程不同節點中的各類需求。
源碼詳見:cn.bugstack.mybatis.executor.resultset.DefaultResultSetHandler#handleResultSet
這是一套結果封裝的核心處理流程,包括創建處理器、封裝數據和保存結果,接下來就分別介紹下這塊代碼的具體實現。
3.1 結果集收集器
源碼詳見:cn.bugstack.mybatis.executor.result.DefaultResultHandler
public class DefaultResultHandler implements ResultHandler
private final List<Object> list;
/**
* 通過 ObjectFactory 反射工具類,產生特定的 List
*/
@SuppressWarnings("unchecked")
public DefaultResultHandler(ObjectFactory objectFactory)
this.list = objectFactory.create(List.class);
@Override
public void handleResult(ResultContext context)
list.add(context.getResultObject());
這里封裝了一個非常簡單的結果集對象,默認情況下都會寫入到這個對象的 list 集合中。
3.2 對象創建
在處理封裝數據的過程中,包括根據 resultType 使用反射工具類 ObjectFactory#create 方法創建出 Bean 對象。這個過程會根據不同的類型進行創建,不過暫時我們這里只是普通對象,所以不會填充太多的代碼,避免擾亂讀者的重點核心內容的學習
調用鏈路:handleResultSet->handleRowValuesForSimpleResultMap->getRowValue->createResultObject
源碼詳見:cn.bugstack.mybatis.executor.resultset.DefaultResultSetHandler#createResultObject
private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, List<Class<?>> constructorArgTypes, List<Object> constructorArgs, String columnPrefix) throws SQLException
final Class<?> resultType = resultMap.getType();
final MetaClass metaType = MetaClass.forClass(resultType);
if (resultType.isInterface() || metaType.hasDefaultConstructor())
// 普通的Bean對象類型
return objectFactory.create(resultType);
throw new RuntimeException("Do not know how to create an instance of " + resultType);
- 對于這樣的普通對象,只需要使用反射工具類就可以實例化對象了,不過這個時候屬性信息還沒有填充。其實和我們使用的 clazz.newInstance(); 也是一樣的效果
3.3 屬性填充
對象實例化完成后,就是根據 ResultSet 獲取出對應的值填充到對象的屬性中,但這里需要注意,這個結果的獲取來自于 TypeHandler#getResult 接口新增的方法,由不同的類型處理器實現,通過這樣的策略模式設計方式就可以巧妙的避免 if···else 的判斷處理。
圖 11-7 使用策略模式,獲取返回結果
源碼詳見:cn.bugstack.mybatis.executor.resultset.DefaultResultSetHandler#applyAutomaticMappings
- columnName 是屬性名稱,根據屬性名稱,按照反射工具類從對象中獲取對應的 properyType 屬性類型,之后再根據類型獲取到 TypeHandler 類型處理器。有了具體的類型處理器,在獲取每一個類型處理器下的結果內容就更加方便了。
- 獲取屬性值后,再使用 MetaObject 反射工具類設置屬性值,一次循環設置完成以后,這樣一個完整的結果信息 Bean 對象就可以返回了。返回后寫入到 DefaultResultContext#nextResultObject 上下文中
五、測試
1. 事先準備
1.1 創建庫表
創建一個數據庫名稱為 mybatis 并在庫中創建表 user 以及添加測試數據,如下:
CREATE TABLE
USER
(
id bigint NOT NULL AUTO_INCREMENT COMMENT '自增ID',
userId VARCHAR(9) COMMENT '用戶ID',
userHead VARCHAR(16) COMMENT '用戶頭像',
createTime TIMESTAMP NULL COMMENT '創建時間',
updateTime TIMESTAMP NULL COMMENT '更新時間',
userName VARCHAR(64),
PRIMARY KEY (id)
)
ENGINE=InnoDB DEFAULT CHARSET=utf8;
insert into user (id, userId, userHead, createTime, updateTime, userName) values (1, '10001', '1_04', '2022-04-13 00:00:00', '2022-04-13 00:00:00', '小傅哥');
1.2 配置數據源
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://127.0.0.1:3306/mybatis?useUnicode=true"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
- 通過mybatis-config-datasource.xml 配置數據源信息,包括:driver、url、username、password
- 在這里 dataSource 可以按需配置成 DRUID、UNPOOLED 和 POOLED 進行測試驗證。
1.3 配置Mapper
<select id="queryUserInfoById" parameterType="java.lang.Long" resultType="cn.bugstack.mybatis.test.po.User">
SELECT id, userId, userName, userHead
FROM user
where id = #{id}
</select>
- 這部分暫時不需要調整,目前還只是一個入參的類型的參數,后續我們全部完善這部分內容以后,則再提供更多的其他參數進行驗證。
2. 單元測試
@Before
public void init() throws IOException
// 1. 從SqlSessionFactory中獲取SqlSession
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsReader("mybatis-config-datasource.xml"));
sqlSession = sqlSessionFactory.openSession();
@Test
public void test_queryUserInfoById()
// 1. 獲取映射器對象
IUserDao userDao = sqlSession.getMapper(IUserDao.class);
// 2. 測試驗證:基本參數
User user = userDao.queryUserInfoById(1L);
logger.info("測試結果:{}" JSON.toJSONString(user));
- 這里我們只測試一個查詢結果即可,返回的類型是一個自定義的對象類型。
測試結果
12:39:17.321main INFO c.b.mybatis.builder.SqlSourceBuilder - 構建參數映射 property:id propertyType:class java.lang.Long
12:39:17.321 main INFO c.b.mybatis.builder.SqlSourceBuilder - 構建參數映射 property:userId propertyType:class java.lang.String
12:39:17.382 main INFO c.b.m.s.defaults.DefaultSqlSession - 執行查詢 statement:cn.bugstack.mybatis.test.dao.IUserDao.queryUserInfoById parameter:1
12:39:17.684 main INFO c.b.m.s.d.DefaultParameterHandler - 根據每個ParameterMapping中的TypeHandler設置對應的參數信息 value:1
12:39:17.728 main INFO cn.bugstack.mybatis.test.ApiTest - 測試結果:"id":1"userHead":"1_04""userId":"10001""userName":"小傅哥"
Process finished with exit code 0
- 通過 DefaultResultSetHandler 結果處理器的功能解耦和實現,已經可以正常查詢和返回對應的對象信息了,后續其他內容的擴展也可以基于這個基座進行處理。
六、總結
- 這一章節的整個功能實現,都在圍繞流程的解耦進行處理,將對象的參數解析和結果封裝都進行拆解,通過這樣的方式來分配各個模塊的單一職責,不讓一個類的方法承擔過多的交叉功能。
- 那么我們在結合這樣的思想和設計,反復閱讀和動手實踐中,來學習這樣的代碼設計和開發過程,都能為我們以后實際開發業務代碼時候帶來參考建議,避免總是把所有的流程都寫到一個類或者方法中。
- 到本章節全核心流程基本就串聯清楚了,再有的就是一些功能的拓展,比如支持更多的參數類型,以及添加除了 Select 以外的其他操作,還有一些緩存數據的使用等,后面章節將在這些內容中,摘取一些核心的設計和實現進行講解,讓讀者吸收更多的設計技巧。