今天繼續搭建咱們的kono Spring Boot腳手架,上一文把國內最流行的ORM框架Mybatis也集成了進去。可是不少時候咱們但願有一些開箱即用的通用Mapper來簡化咱們的開發。我本身嘗試實現了一個,接下來我分享一下思路。昨天晚上才寫的,謹慎用於實際生產開發,可是能夠借鑑思路。html
Gitee: https://gitee.com/felord/kono day03 分支GitHub: https://github.com/NotFound40... day03 分支java
最近在看一些關於Spring Data JDBC的東西,發現它很不錯。其中CrudRepository
很是神奇,只要ORM接口繼承了它就被自動加入Spring IoC,同時也具備了一些基礎的數據庫操做接口。我就在想能不能把它跟Mybatis結合一下。git
其實Spring Data JDBC自己是支持Mybatis的。可是我嘗試整合它們以後發現,要作的事情不少,並且須要遵照不少規約,好比MybatisContext
的參數上下文,接口名稱前綴都有比較嚴格的約定,學習使用成本比較高,不如單獨使用Spring Data JDBC爽。可是我仍是想要那種通用的CRUD功能啊,因此就開始嘗試本身簡單搞一個。github
最開始能想到的有幾個思路可是最終都沒有成功。這裏也分享一下,有時候失敗也是很是值得借鑑的。spring
使用Mybatis的插件功能開發插件,可是研究了半天發現不可行,最大的問題就是Mapper生命週期的問題。sql
在項目啓動的時候Mapper註冊到配置中,同時對應的SQL也會被註冊到MappedStatement
對象中。當執行Mapper的方法時會經過代理來根據名稱空間(Namespace)來加載對應的MappedStatement
來獲取SQL並執行。數據庫
而插件的生命週期是在MappedStatement
已經註冊的前提下才開始,根本銜接不上。mybatis
這個徹底可行,可是造輪子的成本高了一些,並且成熟的不少,實際生產開發中咱們找一個就是了,我的造輪子時間精力成本比較高,也沒有必要。app
最後仍是按照這個方向走,找一個合適的切入點把對應通用Mapper的MappedStatement
註冊進去。接下來會詳細介紹我是如何實現的。框架
在最開始沒有Spring Boot的時候,大都是這麼註冊Mapper的。
<bean id="baseMapper" class="org.mybatis.spring.mapper.MapperFactoryBean" abstract="true" lazy-init="true"> <property name="sqlSessionFactory" ref="sqlSessionFactory" /> </bean> <bean id="oneMapper" parent="baseMapper"> <property name="mapperInterface" value="my.package.MyMapperInterface" /> </bean> <bean id="anotherMapper" parent="baseMapper"> <property name="mapperInterface" value="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。
明白了Spring 註冊Mapper的機制以後就能夠開始實現通用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
。
爲了簡化代碼,實體類作了一些常見的規約:
UserInfo
的數據庫表名就是user_info
。由於主鍵屬性必須有顯式的標識才能得到,因此聲明瞭一個主鍵標記註解:
/** * Demarcates an identifier. * * @author felord.cn */ @Retention(RetentionPolicy.RUNTIME) @Target(value = { 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
了。
一個最佳的切入點是在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(); } } }
由於咱們覆蓋了默認的MapperFactoryBean
因此咱們要顯式聲明啓用自定義的MybatisMapperFactoryBean
,以下:
@MapperScan(basePackages = {"cn.felord.kono.mapper"},factoryBean = MybatisMapperFactoryBean.class)
而後一個通用Mapper功能就實現了。
這只是本身的一次小嚐試,我已經單獨把這個功能抽出來了,有興趣可自行參考研究。
成功的關鍵在於對Mybatis中一些概念生命週期的把控。其實大多數框架若是須要魔改時都遵循了這一個思路:把流程搞清楚,找一個合適的切入點把自定義邏輯嵌進去。本次DEMO不會合並的主分支,由於這只是一次嘗試,還不足以運用於實踐,你能夠選擇其它知名的框架來作這些事情。多多關注並支持:碼農小胖哥 分享更多開發中的事情。
關注公衆號:Felordcn 獲取更多資訊