码迷,mamicode.com
首页 > 其他好文 > 详细

苞米豆源码解析一: 动态注入

时间:2020-01-03 10:30:00      阅读:260      评论:0      收藏:0      [点我收藏+]

标签:span   cep   非接口   start   不用   sqli   configure   assign   col   

启动过程分析: 与绝大部分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);
    }

苞米豆源码解析一: 动态注入

标签:span   cep   非接口   start   不用   sqli   configure   assign   col   

原文地址:https://www.cnblogs.com/xieyanke/p/12143501.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!