Mybatis苞米豆源碼分析一: 動態注入


啟動過程分析: 與絕大部分starter一樣, 使用spring.factories作為入口

org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.baomidou.mybatisplus.spring.boot.starter.MybatisPlusAutoConfiguration

簡要說明動態SQL注入的流程:

  1. 先對XML進行解析, 基於原生的mybatis解析XML方式, 解析成statement並存入configuration中
  2. 根據第一步的解析可以獲取當前XML的namespace,也即 mapper類判斷當前Mapper接口是否繼承 BaseMapper(只有繼承了BaseMapper方法才需要動態注入SQL),
    然后動態注入BaseMapper中方法(有多個注入器, 文章最末尾代碼段)
  3. 最后再對所有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);
    }


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM