
1. 前言
今天繼續搭建我們的kono Spring Boot腳手架,上一文把國內最流行的ORM框架Mybatis也集成了進去,但是很多時候我們希望有一些開箱即用的通用Mapper來簡化我們的開發,我自己嘗試實作了一個,接下來我分享一下思路,昨天晚上才寫的,謹慎用于實際生產開發,但是可以借鑒思路,
Gitee: https://gitee.com/felord/kono day03 分支
GitHub: https://github.com/NotFound403/kono day03 分支
2. 思路來源
最近在看一些關于Spring Data JDBC的東西,發現它很不錯,其中CrudRepository非常神奇,只要ORM介面繼承了它就被自動加入Spring IoC,同時也具有了一些基礎的資料庫操作介面,我就在想能不能把它跟Mybatis結合一下,
其實Spring Data JDBC本身是支持Mybatis的,但是我嘗試整合它們之后發現,要做的事情很多,而且需要遵守很多規約,比如MybatisContext的引數背景關系,介面名稱前綴都有比較嚴格的約定,學習使用成本比較高,不如單獨使用Spring Data JDBC爽,但是我還是想要那種通用的CRUD功能啊,所以就開始嘗試自己簡單搞一個,
3. 一些嘗試
最開始能想到的有幾個思路但是最終都沒有成功,這里也分享一下,有時候失敗也是非常值得借鑒的,
3.1 Mybatis plugin
使用Mybatis的插件功能開發插件,但是研究了半天發現不可行,最大的問題就是Mapper生命周期的問題,
在專案啟動的時候Mapper注冊到配置中,同時對應的SQL也會被注冊到MappedStatement物件中,當執行Mapper的方法時會通過代理來根據名稱空間(Namespace)來加載對應的MappedStatement來獲取SQL并執行,
而插件的生命周期是在MappedStatement已經注冊的前提下才開始,根本銜接不上,
3.2 代碼生成器
這個完全可行,但是造輪子的成本高了一些,而且成熟的很多,實際生產開發中我們找一個就是了,個人造輪子時間精力成本比較高,也沒有必要,
3.3 模擬MappedStatement注冊
最后還是按照這個方向走,找一個合適的切入點把對應通用Mapper的MappedStatement注冊進去,接下來會詳細介紹我是如何實作的,
4. Spring 注冊Mapper的機制
在最開始沒有Spring Boot的時候,大都是這么注冊Mapper的,
<bean id="baseMapper" abstract="true" lazy-init="true">
<property name="sqlSessionFactory" ref="sqlSessionFactory" />
</bean>
<bean id="oneMapper" parent="baseMapper">
<property name="mapperInterface" value="https://www.cnblogs.com/felordcn/p/my.package.MyMapperInterface" />
</bean>
<bean id="anotherMapper" parent="baseMapper">
<property name="mapperInterface" value="https://www.cnblogs.com/felordcn/p/my.package.MyAnotherMapperInterface" />
</bean>
通過MapperFactoryBean每一個Mybatis Mapper被初始化并注入了Spring IoC容器,所以這個地方來進行通用Mapper的注入是可行的,而且侵入性更小一些,那么它是如何生效的呢?我在大家熟悉的@MapperScan中找到了它的身影,下面摘自其原始碼:
/**
* Specifies a custom MapperFactoryBean to return a mybatis proxy as spring bean.
*
* @return the class of {@code MapperFactoryBean}
*/
Class<? extends MapperFactoryBean> factoryBean() default MapperFactoryBean.class;
也就是說通常@MapperScan會將特定包下的所有Mapper使用MapperFactoryBean批量初始化并注入Spring IoC,
5. 實作通用Mapper
明白了Spring 注冊Mapper的機制之后就可以開始實作通用Mapper了,
5.1 通用Mapper介面
這里借鑒Spring Data專案中的CrudRepository<T,ID>的風格,撰寫了一個Mapper的父介面CrudMapper<T, PK>,包含了四種基本的單表操作,
/**
* 所有的Mapper介面都會繼承{@code CrudMapper<T, PK>}.
*
* @param <T> 物體類泛型
* @param <PK> 主鍵泛型
* @author felord.cn
* @since 14 :00
*/
public interface CrudMapper<T, PK> {
int insert(T entity);
int updateById(T entity);
int deleteById(PK id);
T findById(PK id);
}
后面的邏輯都會圍繞這個介面展開,當具體的Mapper繼承這個介面后,物體類泛型 T 和主鍵泛型PK就已經確定了,我們需要拿到T的具體型別并把其成員屬性封裝為SQL,并定制MappedStatement,
5.2 Mapper的元資料決議封裝
為了簡化代碼,物體類做了一些常見的規約:
- 物體類名稱的下劃線風格就是對應的表名,例如
UserInfo的資料庫表名就是user_info, - 物體類屬性的下劃線風格就是對應資料庫表的欄位名稱,而且物體內所有的屬性都有對應的資料庫欄位,其實可以實作忽略,
- 如果對應Mapper.xml存在對應的SQL,該配置忽略,
因為主鍵屬性必須有顯式的標識才能獲得,所以宣告了一個主鍵標記注解:
/**
* Demarcates an identifier.
*
* @author felord.cn
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(value = https://www.cnblogs.com/felordcn/p/{ FIELD, METHOD, ANNOTATION_TYPE })
public @interface PrimaryKey {
}
然后我們宣告一個資料庫物體時這樣就行了:
/**
* @author felord.cn
* @since 15:43
**/
@Data
public class UserInfo implements Serializable {
private static final long serialVersionUID = -8938650956516110149L;
@PrimaryKey
private Long userId;
private String name;
private Integer age;
}
然后就可以這樣撰寫對用的Mapper了,
public interface UserInfoMapper extends CrudMapper<UserInfo,String> {}
下面就要封裝一個決議這個介面的工具類CrudMapperProvider了,它的作用就是決議UserInfoMapper這些Mapper,封裝MappedStatement,為了便于理解我通過舉例對決議Mapper的程序進行說明,
public CrudMapperProvider(Class<? extends CrudMapper<?, ?>> mapperInterface) {
// 拿到 具體的Mapper 介面 如 UserInfoMapper
this.mapperInterface = mapperInterface;
Type[] genericInterfaces = mapperInterface.getGenericInterfaces();
// 從Mapper 介面中獲取 CrudMapper<UserInfo,String>
Type mapperGenericInterface = genericInterfaces[0];
// 引數化型別
ParameterizedType genericType = (ParameterizedType) mapperGenericInterface;
// 引數化型別的目的是為了決議出 [UserInfo,String]
Type[] actualTypeArguments = genericType.getActualTypeArguments();
// 這樣就拿到物體型別 UserInfo
this.entityType = (Class<?>) actualTypeArguments[0];
// 拿到主鍵型別 String
this.primaryKeyType = (Class<?>) actualTypeArguments[1];
// 獲取所有物體類屬性 本來打算采用內省方式獲取
Field[] declaredFields = this.entityType.getDeclaredFields();
// 決議主鍵
this.identifer = Stream.of(declaredFields)
.filter(field -> field.isAnnotationPresent(PrimaryKey.class))
.findAny()
.map(Field::getName)
.orElseThrow(() -> new IllegalArgumentException(String.format("no @PrimaryKey found in %s", this.entityType.getName())));
// 決議屬性名并封裝為下劃線欄位 排除了靜態屬性 其它沒有深入 后續有需要可宣告一個忽略注解用來忽略欄位
this.columnFields = Stream.of(declaredFields)
.filter(field -> !Modifier.isStatic(field.getModifiers()))
.collect(Collectors.toList());
// 決議表名
this.table = camelCaseToMapUnderscore(entityType.getSimpleName()).replaceFirst("_", "");
}
拿到這些元資料之后就是生成四種SQL了,我們期望的SQL,以UserInfoMapper為例是這樣的:
# findById
SELECT user_id, name, age FROM user_info WHERE (user_id = #{userId})
# insert
INSERT INTO user_info (user_id, name, age) VALUES (#{userId}, #{name}, #{age})
# deleteById
DELETE FROM user_info WHERE (user_id = #{userId})
# updateById
UPDATE user_info SET name = #{name}, age = #{age} WHERE (user_id = #{userId})
Mybatis提供了很好的SQL工具類來生成這些SQL:
String findSQL = new SQL()
.SELECT(COLUMNS)
.FROM(table)
.WHERE(CONDITION)
.toString();
String insertSQL = new SQL()
.INSERT_INTO(table)
.INTO_COLUMNS(COLUMNS)
.INTO_VALUES(VALUES)
.toString();
String deleteSQL = new SQL()
.DELETE_FROM(table)
.WHERE(CONDITION).toString();
String updateSQL = new SQL().UPDATE(table)
.SET(SETS)
.WHERE(CONDITION).toString();
我們只需要把前面通過反射獲取的元資料來實作SQL的動態創建就可以了,以insert方法為例:
/**
* Insert.
*
* @param configuration the configuration
*/
private void insert(Configuration configuration) {
String insertId = mapperInterface.getName().concat(".").concat("insert");
// xml配置中已經注冊就跳過 xml中的優先級最高
if (existStatement(configuration,insertId)){
return;
}
// 生成資料庫的欄位串列
String[] COLUMNS = columnFields.stream()
.map(Field::getName)
.map(CrudMapperProvider::camelCaseToMapUnderscore)
.toArray(String[]::new);
// 對應的值 用 #{} 包裹
String[] VALUES = columnFields.stream()
.map(Field::getName)
.map(name -> String.format("#{%s}", name))
.toArray(String[]::new);
String insertSQL = new SQL()
.INSERT_INTO(table)
.INTO_COLUMNS(COLUMNS)
.INTO_VALUES(VALUES)
.toString();
Map<String, Object> additionalParameters = new HashMap<>();
// 注冊
doAddMappedStatement(configuration, insertId, insertSQL, SqlCommandType.INSERT, entityType, additionalParameters);
}
這里還有一個很重要的東西,每一個MappedStatement都有一個全域唯一的標識,Mybatis的默認規則是Mapper的全限定名用標點符號 . 拼接上對應的方法名稱,例如 cn.felord.kono.mapperClientUserRoleMapper.findById,這些實作之后就是定義自己的MapperFactoryBean了,
5.3 自定義MapperFactoryBean
一個最佳的切入點是在Mapper注冊后進行MappedStatement的注冊,我們可以繼承MapperFactoryBean重寫其checkDaoConfig方法利用CrudMapperProvider來注冊MappedStatement,
@Override
protected void checkDaoConfig() {
notNull(super.getSqlSessionTemplate(), "Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required");
Class<T> mapperInterface = super.getMapperInterface();
notNull(mapperInterface, "Property 'mapperInterface' is required");
Configuration configuration = getSqlSession().getConfiguration();
if (isAddToConfig()) {
try {
// 判斷Mapper 是否注冊
if (!configuration.hasMapper(mapperInterface)) {
configuration.addMapper(mapperInterface);
}
// 只有繼承了CrudMapper 再進行切入
if (CrudMapper.class.isAssignableFrom(mapperInterface)) {
// 一個注冊SQL映射的時機
CrudMapperProvider crudMapperProvider = new CrudMapperProvider(mapperInterface);
// 注冊 MappedStatement
crudMapperProvider.addMappedStatements(configuration);
}
} catch (Exception e) {
logger.error("Error while adding the mapper '" + mapperInterface + "' to configuration.", e);
throw new IllegalArgumentException(e);
} finally {
ErrorContext.instance().reset();
}
}
}
5.4 啟用通用Mapper
因為我們覆寫了默認的MapperFactoryBean所以我們要顯式宣告啟用自定義的MybatisMapperFactoryBean,如下:
@MapperScan(basePackages = {"cn.felord.kono.mapper"},factoryBean = MybatisMapperFactoryBean.class)
然后一個通用Mapper功能就實作了,
5.5 專案位置
這只是自己的一次小嘗試,我已經單獨把這個功能抽出來了,有興趣可自行參考研究,
- GitHub: https://github.com/NotFound403/mybatis-mapper-extension.git
- Gitee: https://gitee.com/felord/mybatis-mapper-extension.git
6. 總結
成功的關鍵在于對Mybatis中一些概念生命周期的把控,其實大多數框架如果需要魔改時都遵循了這一個思路:把流程搞清楚,找一個合適的切入點把自定義邏輯嵌進去,本次DEMO不會合并的主分支,因為這只是一次嘗試,還不足以運用于實踐,你可以選擇其它知名的框架來做這些事情,多多關注并支持:碼農小胖哥 分享更多開發中的事情,
關注公眾號:Felordcn 獲取更多資訊
個人博客:https://felord.cn
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/98207.html
標籤:Java
上一篇:MySQL索引結構原理分析
