啟動過程分析: 與絕大部分starter一樣, 使用spring.factories作為入口
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.baomidou.mybatisplus.spring.boot.starter.MybatisPlusAutoConfiguration
簡要說明動態SQL注入的流程:
- 先對XML進行解析, 基於原生的mybatis解析XML方式, 解析成statement並存入configuration中
- 根據第一步的解析可以獲取當前XML的namespace,也即 mapper類判斷當前Mapper接口是否繼承 BaseMapper(只有繼承了BaseMapper方法才需要動態注入SQL),
然后動態注入BaseMapper中方法(有多個注入器, 文章最末尾代碼段) - 最后再對所有Mapper方法進行篩選, 判斷方法是否使用注解動態注入SQL方式, 若使用了注解則覆蓋前兩步驟生成的statement(SelectProvider, InsertProvider, UpdateProvider)
配置構造
public MybatisPlusAutoConfiguration(MybatisPlusProperties properties, ObjectProvider<Interceptor[]> interceptorsProvider, //spring注入 可以理解為@AutoWare ResourceLoader resourceLoader, ObjectProvider<DatabaseIdProvider> databaseIdProvider, ObjectProvider<List<ConfigurationCustomizer>> configurationCustomizersProvider, ApplicationContext applicationContext) { this.properties = properties; this.interceptors = interceptorsProvider.getIfAvailable(); this.resourceLoader = resourceLoader; this.databaseIdProvider = databaseIdProvider.getIfAvailable(); this.configurationCustomizers = configurationCustomizersProvider.getIfAvailable(); this.applicationContext = applicationContext; }
初始化核心類SqlSessionFactory MybatisPlusAutoConfiguration
//注入 SqlSessionFactory @Bean @ConditionalOnMissingBean public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception { //SqlSessionFactory生成 MybatisSqlSessionFactoryBean factory = new MybatisSqlSessionFactoryBean(); //...忽略若干行MybatisSqlSessionFactoryBean的屬性設置 //注入填充器 針對比較通用的字段 舉例:插入數據是自動填充 valid gmt_create gmt_modify 修改數據時自動填充gmt_modify if (this.applicationContext.getBeanNamesForType(MetaObjectHandler.class, false, false).length > 0) { MetaObjectHandler metaObjectHandler = this.applicationContext.getBean(MetaObjectHandler.class); globalConfig.setMetaObjectHandler(metaObjectHandler); } //注入主鍵生成器 做insert操作的時候 自動填充ID 不過由於大部分情況下主鍵需要用來承上啟下, 不建議使用 if (this.applicationContext.getBeanNamesForType(IKeyGenerator.class, false, false).length > 0) { IKeyGenerator keyGenerator = this.applicationContext.getBean(IKeyGenerator.class); globalConfig.setKeyGenerator(keyGenerator); } //注入sql注入器 這個比較重要 可以在這里注入自定義的SQL注入器, 苞米豆自帶一個邏輯處理器LogicSqlInjector,注入到Spring容器后,能在此處拿到 (執行delete方法的時候 變成update邏輯字段) if (this.applicationContext.getBeanNamesForType(ISqlInjector.class, false, false).length > 0) { //從容器中取 ISqlInjector iSqlInjector = this.applicationContext.getBean(ISqlInjector.class); globalConfig.setSqlInjector(iSqlInjector); } //重點關注的是 SqlSessionFactory對象創建過程 return factory.getObject(); }
創建SqlSessionFactory對象 MybatisSqlSessionFactoryBean
@Override public SqlSessionFactory getObject() throws Exception { if (this.sqlSessionFactory == null) { //重點關注方法 初始化操作 afterPropertiesSet(); } return this.sqlSessionFactory; } @Override public void afterPropertiesSet() throws Exception { //前置條件判斷 notNull(dataSource, "Property 'dataSource' is required"); notNull(sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required"); state((configuration == null && configLocation == null) || !(configuration != null && configLocation != null), "Property 'configuration' and 'configLocation' can not specified with together"); //重點關注 構建sqlSessionFactory this.sqlSessionFactory = buildSqlSessionFactory(); } protected SqlSessionFactory buildSqlSessionFactory() throws Exception { Configuration configuration; //加載自定義 MybatisXmlConfigBuilder 較少使用 忽略 MybatisXMLConfigBuilder xmlConfigBuilder = null; ....... // 自定義枚舉類掃描處理 類型轉換器注冊(jdbc和java轉換) 較少使用 非關心重點 忽略 ........ // 自定義類別名 if (!isEmpty(this.typeAliases)) { for (Class<?> typeAlias : this.typeAliases) { configuration.getTypeAliasRegistry().registerAlias(typeAlias); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Registered type alias: '" + typeAlias + "'"); } } } // 重點關心 mybatis 攔截器注冊, 幾乎絕大部分mybatis插件都是使用攔截器方式實現, 將攔截器注冊到configuration中 if (!isEmpty(this.plugins)) { for (Interceptor plugin : this.plugins) { configuration.addInterceptor(plugin); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Registered plugin: '" + plugin + "'"); } } } //忽略其他自定義實現 ..... //設置spring事務管理工廠 其作用為新建Spring事務org.mybatis.spring.transaction.SpringManagedTransaction 非重點不關注 if (this.transactionFactory == null) { this.transactionFactory = new SpringManagedTransactionFactory(); } configuration.setEnvironment(new Environment(this.environment, this.transactionFactory, this.dataSource)); // 設置元數據相關 這里並非通過反射對Object對屬性賦值 只是簡單的將dataSource屬性值賦給globalConfig 不要被名字誤解 GlobalConfigUtils.setMetaData(dataSource, globalConfig); SqlSessionFactory sqlSessionFactory = this.sqlSessionFactoryBuilder.build(configuration); // 各種sqlSessionFactory的屬性賦值 忽略 ....... if (!isEmpty(this.mapperLocations)) { if (globalConfig.isRefresh()) { //TODO 設置自動刷新配置 減少配置 new MybatisMapperRefresh(this.mapperLocations, sqlSessionFactory, 2, 2, true); } for (Resource mapperLocation : this.mapperLocations) { if (mapperLocation == null) { continue; } try { // TODO 這里也換了噢噢噢噢 這句話是官方原話 // mapperLocation可以理解為一個mybatis的Xml文件 作用為創建xml解析器 XMLMapperBuilder XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(), configuration, mapperLocation.toString(), configuration.getSqlFragments()); //對xml進行解析 重點關注 xmlMapperBuilder.parse(); } catch (Exception e) { throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e); } finally { ErrorContext.instance().reset(); } if (LOGGER.isDebugEnabled()) { LOGGER.debug("Parsed mapper file: '" + mapperLocation + "'"); } } } else { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Property 'mapperLocations' was not specified or no matching resources found"); } } //返回此sqlSessionFactory return sqlSessionFactory; }
對mybatis xml進行解析
XMLMapperBuilder
//總體邏輯分為兩步 // 1 靜態加載, 加載xml文件 注冊xml文件中sql為 statement到 configration中 // 2 動態加載, 判斷方法是否在上一步已經注冊為statement 若未注冊則使用動態注冊類進行 SQL動態注冊statement到 configration中, 這取決於BaseMapper的基礎方法數 // 注: 由於第二步動態加載只對方法名進性判斷 未對注解@Param中的參數進性容錯處理 若進性自定義SQL覆蓋BaseMapper中的方法,可能會導致報錯 public void parse() { if (!configuration.isResourceLoaded(resource)) { //重點關注 注冊自定義xml的SQL方法 configurationElement(parser.evalNode("/mapper")); configuration.addLoadedResource(resource); //重點關注 動態注冊xml的sql方法 bindMapperForNamespace(); } parsePendingResultMaps(); parsePendingChacheRefs(); parsePendingStatements(); } //解析xml private void bindMapperForNamespace() { String namespace = builderAssistant.getCurrentNamespace(); if (namespace != null) { Class<?> boundType = null; try { boundType = Resources.classForName(namespace); } catch (ClassNotFoundException e) { //ignore, bound type is not required } if (boundType != null) { if (!configuration.hasMapper(boundType)) { // Spring may not know the real resource name so we set a flag // to prevent loading again this resource from the mapper interface // look at MapperAnnotationBuilder#loadXmlResource configuration.addLoadedResource("namespace:" + namespace); //重點關注 注冊mapper 此處 configuration.addMapper 被苞米豆重新實現 使用的是苞米豆的添加方法 繼續往下看苞米豆的具體實現 configuration.addMapper(boundType); } } } }
注冊 mapper
MybatisPlusAutoConfiguration
@Override public <T> void addMapper(Class<T> type) { //此注冊器為苞米豆的mapper注冊器 原生的為MapperRegistry不需要特意關注 重點關注苞米豆注冊器MybatisMapperRegistry mybatisMapperRegistry.addMapper(type); }
真正的注冊類 儲存mapper
MybatisMapperRegistry
@Override public <T> void addMapper(Class<T> type) { //若mapper類是接口則往下進行 若非接口也不報錯 這點無法理解的 if (type.isInterface()) { //判斷是否已經注冊了 if (hasMapper(type)) { // TODO 如果之前注入 直接返回 return; // throw new BindingException("Type " + type + // " is already known to the MybatisPlusMapperRegistry."); } boolean loadCompleted = false; try { //此處是為了防止后面同樣mybatis xml使用用一個mapper作為namespace 可以不用重復創建 見hasMapper(type)方法 knownMappers.put(type, new MapperProxyFactory<>(type)); //終於到了終點代碼 xml的解析實際是交給 MybatisMapperAnnotationBuilder來做的 MybatisMapperAnnotationBuilder parser = new MybatisMapperAnnotationBuilder(config, type); parser.parse(); loadCompleted = true; } finally { if (!loadCompleted) { knownMappers.remove(type); } } } }
苞米豆的動態statement從這里開始
MybatisMapperAnnotationBuilder
@Override public void parse() { //獲取mapper全路徑 String resource = type.toString(); if (!configuration.isResourceLoaded(resource)) { //解析xml loadXmlResource(); //設置當前mapper已經加載 configuration.addLoadedResource(resource); assistant.setCurrentNamespace(type.getName()); //緩存配置 忽略 parseCache(); parseCacheRef(); //獲取mapper所有方法 重點 Method[] methods = type.getMethods(); // TODO 注入 CURD 動態 SQL (應該在注解之前注入) 注入器見 // 判斷BaseMapper是否是當前mapper的接口或者父類 一個native方法 if (BaseMapper.class.isAssignableFrom(type)) { //利用SQL注入器 根據方法名動態住處sql 相當於在xml寫了一段sql, 然后解析成statemanet //最終實現在AutoSqlInjector的injectSql方法 直接看下一段代碼 GlobalConfigUtils.getSqlInjector(configuration).inspectInject(assistant, type); } for (Method method : methods) { try { // 判斷此方法是否為橋接方法 只處理非橋接方法 簡單解釋: 父類申明泛型但是不指定 而實現類指定具體的泛型 編譯時確定了具體泛型 if (!method.isBridge()) { //最后進行注解覆蓋 舉例org.apache.ibatis.annotations.SelectProvider parseStatement(method); } } catch (IncompleteElementException e) { configuration.addIncompleteMethod(new MethodResolver(this, method)); } } } parsePendingMethods(); }
動態SQL注入器處理方法 AutoSqlInjector
// 以下每個自動注入器注入動態SQL之前會判斷是否已人為實現在mybatis xml中 若不存在才使用動態注入器 // 舉例: 在分庫分表時 若使用自動注入器則會連org_id一並修改, 此時需要人為實現updateById, 苞米豆檢測要人為實現則不會進行動態注入 protected void injectSql(MapperBuilderAssistant builderAssistant, Class<?> mapperClass, Class<?> modelClass, TableInfo table) { /** * #148 表信息包含主鍵,注入主鍵相關方法 */ if (StringUtils.isNotEmpty(table.getKeyProperty())) { /** 刪除 */ this.injectDeleteByIdSql(false, mapperClass, modelClass, table); this.injectDeleteByIdSql(true, mapperClass, modelClass, table); /** 修改 */ this.injectUpdateByIdSql(true, mapperClass, modelClass, table); this.injectUpdateByIdSql(false, mapperClass, modelClass, table); /** 查詢 */ this.injectSelectByIdSql(false, mapperClass, modelClass, table); this.injectSelectByIdSql(true, mapperClass, modelClass, table); } else { // 表不包含主鍵時 給予警告 logger.warn(String.format("%s ,Not found @TableId annotation, Cannot use Mybatis-Plus 'xxById' Method.", modelClass.toString())); } /** * 正常注入無需主鍵方法 */ /** 插入 */ this.injectInsertOneSql(true, mapperClass, modelClass, table); this.injectInsertOneSql(false, mapperClass, modelClass, table); /** 刪除 */ this.injectDeleteSql(mapperClass, modelClass, table); this.injectDeleteByMapSql(mapperClass, table); /** 修改 */ this.injectUpdateSql(mapperClass, modelClass, table); /** 修改 (自定義 set 屬性) */ this.injectUpdateForSetSql(mapperClass, modelClass, table); /** 查詢 */ this.injectSelectByMapSql(mapperClass, modelClass, table); this.injectSelectOneSql(mapperClass, modelClass, table); this.injectSelectCountSql(mapperClass, modelClass, table); this.injectSelectListSql(SqlMethod.SELECT_LIST, mapperClass, modelClass, table); this.injectSelectListSql(SqlMethod.SELECT_PAGE, mapperClass, modelClass, table); this.injectSelectMapsSql(SqlMethod.SELECT_MAPS, mapperClass, modelClass, table); this.injectSelectMapsSql(SqlMethod.SELECT_MAPS_PAGE, mapperClass, modelClass, table); this.injectSelectObjsSql(SqlMethod.SELECT_OBJS, mapperClass, modelClass, table); /** 自定義方法 */ this.inject(configuration, builderAssistant, mapperClass, modelClass, table); }