微信公衆號「後端進階」,專一後端技術分享:Java、Golang、WEB框架、分佈式中間件、服務治理等等。
老司機傾囊相授,帶你一路進階,來不及解釋了快上車!
mybatis-plus是徹底基於mybatis開發的一個加強工具,它的設計理念是在mybatis的基礎上只作加強不作改變,爲簡化開發、提升效率而生,它在mybatis的基礎上增長了不少實用性的功能,好比增長了樂觀鎖插件、字段自動填充功能、分頁插件、條件構造器、sql注入器等等,這些在開發過程當中都是很是實用的功能,mybatis-plus可謂是站在巨人的肩膀上進行了一系列的創新,我我的極力推薦。下面我會詳細地從源碼的角度分析mybatis-plus(下文簡寫成mp)是如何實現sql自動注入的原理。java
咱們回顧一下mybatis的Mapper的註冊與綁定過程,我以前也寫過一篇「Mybatis源碼分析之Mapper註冊與綁定」,在這篇文章中,我詳細地講解了Mapper綁定的最終目的是將xml或者註解上的sql信息與其對應Mapper類註冊到MappedStatement中,既然mybatis-plus的設計理念是在mybatis的基礎上只作加強不作改變,那麼sql注入器必然也是在將咱們預先定義好的sql和預先定義好的Mapper註冊到MappedStatement中。程序員
如今我將Mapper的註冊與綁定過程用時序圖再梳理一遍:spring
解析一下這幾個類的做用:sql
從時序圖可知,Configuration配置類存儲了全部Mapper註冊與綁定的信息,而後建立SqlSessionFactory時再將Configuration注入進去,最後通過SqlSessionFactory建立出來的SqlSession會話,就能夠根據Configuration信息進行數據庫交互,而MapperProxyFactory會爲每一個Mapper建立一個MapperProxy代理類,MapperProxy包含了Mapper操做SqlSession全部的細節,所以咱們就能夠直接使用Mapper的方法就能夠跟SqlSession進行交互。數據庫
饒了一圈,發現我如今還沒講sql注入器的源碼分析,你不用慌,你得體現出老司機的成熟穩定,以前我也跟你說了sql注入器的原理了,只剩下源碼分析,這時候咱們應該在源碼分析以前作足前戲,前戲作足就剩下撕、拉、扯、剝開源碼的外衣了,來不及解釋了快上車!後端
從Mapper的註冊與綁定過程的時序圖看,要想將sql注入器無縫連接地添加到mybatis裏面,那就得從Mapper註冊步驟添加,果真,mp很雞賊地繼承了MapperRegistry這個類而後重寫了addMapper方法:緩存
com.baomidou.mybatisplus.MybatisMapperRegistry#addMapper:微信
public <T> void addMapper(Class<T> type) { if (type.isInterface()) { if (hasMapper(type)) { // TODO 若是以前注入 直接返回 return; // throw new BindingException("Type " + type + // " is already known to the MybatisPlusMapperRegistry."); } boolean loadCompleted = false; try { knownMappers.put(type, new MapperProxyFactory<>(type)); // It's important that the type is added before the parser is run // otherwise the binding may automatically be attempted by the // mapper parser. If the type is already known, it won't try. // TODO 自定義無 XML 注入 MybatisMapperAnnotationBuilder parser = new MybatisMapperAnnotationBuilder(config, type); parser.parse(); loadCompleted = true; } finally { if (!loadCompleted) { knownMappers.remove(type); } } } }
方法中將MapperAnnotationBuilder替換成了自家的MybatisMapperAnnotationBuilder,在這裏特別說明一下,mp爲了避免更改mybatis原有的邏輯,會用繼承或者直接粗暴地將其複製過來,而後在原有的類名上加上前綴「Mybatis」。mybatis
com.baomidou.mybatisplus.MybatisMapperAnnotationBuilder#parse:架構
public void parse() { String resource = type.toString(); if (!configuration.isResourceLoaded(resource)) { loadXmlResource(); configuration.addLoadedResource(resource); assistant.setCurrentNamespace(type.getName()); parseCache(); parseCacheRef(); Method[] methods = type.getMethods(); // TODO 注入 CURD 動態 SQL (應該在註解以前注入) if (BaseMapper.class.isAssignableFrom(type)) { GlobalConfigUtils.getSqlInjector(configuration).inspectInject(assistant, type); } for (Method method : methods) { try { // issue #237 if (!method.isBridge()) { parseStatement(method); } } catch (IncompleteElementException e) { configuration.addIncompleteMethod(new MethodResolver(this, method)); } } } parsePendingMethods(); }
sql注入器就是從這個方法裏面添加上去的,首先判斷Mapper是不是BaseMapper的超類或者超接口,BaseMapper是mp的基礎Mapper,裏面定義了不少默認的基礎方法,意味着咱們一旦使用上mp,經過sql注入器,不少基礎的數據庫操做均可以直接繼承BaseMapper實現了,開發效率爆棚有木有!
com.baomidou.mybatisplus.toolkit.GlobalConfigUtils#getSqlInjector:
public static ISqlInjector getSqlInjector(Configuration configuration) { // fix #140 GlobalConfiguration globalConfiguration = getGlobalConfig(configuration); ISqlInjector sqlInjector = globalConfiguration.getSqlInjector(); if (sqlInjector == null) { sqlInjector = new AutoSqlInjector(); globalConfiguration.setSqlInjector(sqlInjector); } return sqlInjector; }
GlobalConfiguration是mp的全局緩存類,用於存放mp自帶的一些功能,很明顯,sql注入器就存放在GlobalConfiguration中。
這個方法是先從全局緩存類中獲取自定義的sql注入器,若是在GlobalConfiguration中沒有找到自定義sql注入器,就會設置一個mp默認的sql注入器AutoSqlInjector。
sql注入器接口:
// SQL 自動注入器接口 public interface ISqlInjector { // 根據mapperClass注入SQL void inject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass); // 檢查SQL是否注入(已經注入過再也不注入) void inspectInject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass); // 注入SqlRunner相關 void injectSqlRunner(Configuration configuration); }
全部自定義的sql注入器都須要實現ISqlInjector接口,mp已經爲咱們默認實現了一些基礎的注入器:
其中AutoSqlInjector提供了最基本的sql注入,以及一些通用的sql注入與拼裝的邏輯,LogicSqlInjector在AutoSqlInjector的基礎上覆寫了刪除邏輯,由於咱們的數據庫的數據刪除實質上是軟刪除,並非真正的刪除。
com.baomidou.mybatisplus.mapper.AutoSqlInjector#inspectInject:
public void inspectInject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass) { String className = mapperClass.toString(); Set<String> mapperRegistryCache = GlobalConfigUtils.getMapperRegistryCache(builderAssistant.getConfiguration()); if (!mapperRegistryCache.contains(className)) { inject(builderAssistant, mapperClass); mapperRegistryCache.add(className); } }
該方法是sql注入器的入口,在入口處添加了注入事後再也不注入的判斷功能。
// 注入單點 crudSql @Override public void inject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass) { this.configuration = builderAssistant.getConfiguration(); this.builderAssistant = builderAssistant; this.languageDriver = configuration.getDefaultScriptingLanguageInstance(); // 駝峯設置 PLUS 配置 > 原始配置 GlobalConfiguration globalCache = this.getGlobalConfig(); if (!globalCache.isDbColumnUnderline()) { globalCache.setDbColumnUnderline(configuration.isMapUnderscoreToCamelCase()); } Class<?> modelClass = extractModelClass(mapperClass); if (null != modelClass) { // 初始化 SQL 解析 if (globalCache.isSqlParserCache()) { PluginUtils.initSqlParserInfoCache(mapperClass); } TableInfo table = TableInfoHelper.initTableInfo(builderAssistant, modelClass); injectSql(builderAssistant, mapperClass, modelClass, table); } }
注入以前先將Mapper類提取泛型模型,由於繼承BaseMapper須要將Mapper對應的model添加到泛型裏面,這時候咱們須要將其提取出來,提取出來後還須要將其初始化成一個TableInfo對象,TableInfo存儲了數據庫對應的model全部的信息,包括表主鍵ID類型、表名稱、表字段信息列表等等信息,這些信息經過反射獲取。
com.baomidou.mybatisplus.mapper.AutoSqlInjector#injectSql:
protected void injectSql(MapperBuilderAssistant builderAssistant, Class<?> mapperClass, Class<?> modelClass, TableInfo table) { if (StringUtils.isNotEmpty(table.getKeyProperty())) { /** 刪除 */ this.injectDeleteByIdSql(false, mapperClass, modelClass, table); /** 修改 */ this.injectUpdateByIdSql(true, mapperClass, modelClass, table); /** 查詢 */ this.injectSelectByIdSql(false, mapperClass, modelClass, table); } /** 自定義方法 */ this.inject(configuration, builderAssistant, mapperClass, modelClass, table); }
全部須要注入的sql都是經過該方法進行調用,AutoSqlInjector還提供了一個inject方法,自定義sql注入器時,繼承AutoSqlInjector,實現該方法就好了。
com.baomidou.mybatisplus.mapper.AutoSqlInjector#injectDeleteByIdSql:
protected void injectSelectByIdSql(boolean batch, Class<?> mapperClass, Class<?> modelClass, TableInfo table) { SqlMethod sqlMethod = SqlMethod.SELECT_BY_ID; SqlSource sqlSource; if (batch) { sqlMethod = SqlMethod.SELECT_BATCH_BY_IDS; StringBuilder ids = new StringBuilder(); ids.append("\n<foreach item=\"item\" index=\"index\" collection=\"coll\" separator=\",\">"); ids.append("#{item}"); ids.append("\n</foreach>"); sqlSource = languageDriver.createSqlSource(configuration, String.format(sqlMethod.getSql(), sqlSelectColumns(table, false), table.getTableName(), table.getKeyColumn(), ids.toString()), modelClass); } else { sqlSource = new RawSqlSource(configuration, String.format(sqlMethod.getSql(), sqlSelectColumns(table, false), table.getTableName(), table.getKeyColumn(), table.getKeyProperty()), Object.class); } this.addSelectMappedStatement(mapperClass, sqlMethod.getMethod(), sqlSource, modelClass, table); }
我隨機選擇一個刪除sql的注入,其它sql注入都是相似這麼寫,SqlMethod是一個枚舉類,裏面存儲了全部自動注入的sql與方法名,若是是批量操做,SqlMethod的定義的sql語句在添加批量操做的語句。再根據table和sql信息建立一個SqlSource對象。
com.baomidou.mybatisplus.mapper.AutoSqlInjector#addMappedStatement:
public MappedStatement addMappedStatement(Class<?> mapperClass, String id, SqlSource sqlSource, SqlCommandType sqlCommandType, Class<?> parameterClass, String resultMap, Class<?> resultType, KeyGenerator keyGenerator, String keyProperty, String keyColumn) { // MappedStatement是否存在 String statementName = mapperClass.getName() + "." + id; if (hasMappedStatement(statementName)) { System.err.println("{" + statementName + "} Has been loaded by XML or SqlProvider, ignoring the injection of the SQL."); return null; } /** 緩存邏輯處理 */ boolean isSelect = false; if (sqlCommandType == SqlCommandType.SELECT) { isSelect = true; } return builderAssistant.addMappedStatement(id, sqlSource, StatementType.PREPARED, sqlCommandType, null, null, null, parameterClass, resultMap, resultType, null, !isSelect, isSelect, false, keyGenerator, keyProperty, keyColumn, configuration.getDatabaseId(), languageDriver, null); }
sql注入器的最終操做,這裏會判斷MappedStatement是否存在,這個判斷是有緣由的,它會防止重複注入,若是你的Mapper方法已經在Mybatis的邏輯裏面註冊了,mp不會再次注入。最後調用MapperBuilderAssistant助手類的addMappedStatement方法執行註冊操做。
到這裏,一個sql自動注入器的源碼就分析完了,其實實現起來很簡單,由於它利用了Mybatis的機制,站在巨人的肩膀上進行創新。
我但願在大家從此的職業生涯裏,不要只作一個只會調用API的crud程序員,咱們要有一種刨根問底的精神。閱讀源碼很枯燥,但閱讀源碼不只會讓你知道API底層的實現原理,讓你知其然也知其因此然,還能夠開闊你的思惟,提高你的架構設計能力,經過閱讀源碼,能夠看到大佬們是如何設計一個框架的,爲何會這麼設計。