MybatisPlus源码解析2:mybatis-plus启动流程之Sql注入器-SqlInjector

文章详细介绍了MybatisPlus中SqlInjector的职责,包括MapperFactoryBean初始化过程中的Mapper接口添加和解析,以及如何根据接口生成MappedStatement并存储在全局配置。重点阐述了Mapper接口的注册、解析Mapper注解和自动注入SQL的过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

@[TOC](MybatisPlus源码解析2:mybatis-plus启动流程之Sql注入器-SqlInjector

职责
如果UserMapper继承了com.baomidou.mybatisplus.core.mapper.Mapper接口,那么就会生成对应的MappedStatement,然后保存到全局配置里(MybatisConfiguration.mappedStatements),以供Sql执行。
在这里插入图片描述

1.MapperFactoryBean初始化

MapperFactoryBean创建完毕之后会调用DaoSupport#afterPropertiesSet

@Override
public final void afterPropertiesSet() throws IllegalArgumentException, BeanInitializationException {
	// Let abstract subclasses check their configuration.
	checkDaoConfig();

	// Let concrete implementations initialize themselves.
	try {
		initDao();
	}
	catch (Exception ex) {
		throw new BeanInitializationException("Initialization of DAO failed", ex);
	}
}

 @Override
 protected void checkDaoConfig() {
   super.checkDaoConfig();

   notNull(this.mapperInterface, "Property 'mapperInterface' is required");

   Configuration configuration = getSqlSession().getConfiguration();
   if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) {
     try {
       configuration.addMapper(this.mapperInterface);
     } catch (Exception e) {
       logger.error("Error while adding the mapper '" + this.mapperInterface + "' to configuration.", e);
       throw new IllegalArgumentException(e);
     } finally {
       ErrorContext.instance().reset();
     }
   }
 }

2.添加Mapper

	@Override
    public <T> void addMapper(Class<T> type) {
        if (type.isInterface()) {
            if (hasMapper(type)) {
                // TODO 如果之前注入 直接返回
                return;
                // TODO 这里就不抛异常了
//                throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
            }
            boolean loadCompleted = false;
            try {
                // TODO 这里也换成 MybatisMapperProxyFactory 而不是 MapperProxyFactory
                knownMappers.put(type, new MybatisMapperProxyFactory<>(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 这里也换成 MybatisMapperAnnotationBuilder 而不是 MapperAnnotationBuilder
                MybatisMapperAnnotationBuilder parser = new MybatisMapperAnnotationBuilder(config, type);
                parser.parse();
                loadCompleted = true;
            } finally {
                if (!loadCompleted) {
                    knownMappers.remove(type);
                }
            }
        }
    }
  1. 如果当前的UserMapper不是接口,那么就什么都不执行。
  2. 如果当前的接口保存在knownMappers的key里面,那么就直接返回。
  3. 先将当前的接口和它对应的代理工厂保存到knownMappers,然后再通过Mapper注解解析器对当前的接口进行解析

3.解析Mapper接口 MybatisMapperAnnotationBuilder#parse

	@Override
    public void parse() {
        String resource = type.toString();
        if (!configuration.isResourceLoaded(resource)) {
            loadXmlResource();
            configuration.addLoadedResource(resource);
            String mapperName = type.getName();
            assistant.setCurrentNamespace(mapperName);
            parseCache();
            parseCacheRef();
            IgnoreStrategy ignoreStrategy = InterceptorIgnoreHelper.initSqlParserInfoCache(type);
            for (Method method : type.getMethods()) {
                if (!canHaveStatement(method)) {
                    continue;
                }
                if (getAnnotationWrapper(method, false, Select.class, SelectProvider.class).isPresent()
                    && method.getAnnotation(ResultMap.class) == null) {
                    parseResultMap(method);
                }
                try {
                    // TODO 加入 注解过滤缓存
                    InterceptorIgnoreHelper.initSqlParserInfoCache(ignoreStrategy, mapperName, method);
                    parseStatement(method);
                } catch (IncompleteElementException e) {
                    // TODO 使用 MybatisMethodResolver 而不是 MethodResolver
                    configuration.addIncompleteMethod(new MybatisMethodResolver(this, method));
                }
            }
            // TODO 注入 CURD 动态 SQL , 放在在最后, because 可能会有人会用注解重写sql
            try {
                // https://github.com/baomidou/mybatis-plus/issues/3038
                if (GlobalConfigUtils.isSupperMapperChildren(configuration, type)) {
                    parserInjector();
                }
            } catch (IncompleteElementException e) {
                configuration.addIncompleteMethod(new InjectorResolver(this));
            }
        }
        parsePendingMethods();
    }

如果当前的mapper接口没有被加载:

  1. 那么就先去加载当前mapper对应的xml文件
  2. canHaveStatement(method) 如果当前接口不是桥接方法也不是default方法,那么才会对注解进行解析
  3. parserInjector()注入sql,是由DefaultSqlInjector完成的

最终解析未加载完的方法parsePendingMethods

3.1 AbstractSqlInjector#inspectInject

负责注入sql的核心代码

	@Override
    public void inspectInject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass) {
        Class<?> modelClass = ReflectionKit.getSuperClassGenericType(mapperClass, Mapper.class, 0);
        if (modelClass != null) {
            String className = mapperClass.toString();
            Set<String> mapperRegistryCache = GlobalConfigUtils.getMapperRegistryCache(builderAssistant.getConfiguration());
            if (!mapperRegistryCache.contains(className)) {
                TableInfo tableInfo = TableInfoHelper.initTableInfo(builderAssistant, modelClass);
                List<AbstractMethod> methodList = this.getMethodList(mapperClass, tableInfo);
                if (CollectionUtils.isNotEmpty(methodList)) {
                    // 循环注入自定义方法
                    methodList.forEach(m -> m.inject(builderAssistant, mapperClass, modelClass, tableInfo));
                } else {
                    logger.debug(mapperClass.toString() + ", No effective injection method was found.");
                }
                mapperRegistryCache.add(className);
            }
        }
    }
  1. 获取Mapper接口的泛型,也就是User.class
  2. 获取mapperRegistryCache, mapperRegistryCache缓存已注入CRUD的Mapper信息
  3. 初始化table信息
  4. 获取modelClass(也就是User.class)的所有方法,然后调用inject方法注入sql
  5. 将Mapper接口放入到缓存mapperRegistryCache

3.2 TableInfoHelper#initTableInfo

初始化表的信息,简单的来说就是初始化表名称、表的列名clomn、java属性

private synchronized static TableInfo initTableInfo(Configuration configuration, String currentNamespace, Class<?> clazz) {
    /* 没有获取到缓存信息,则初始化 */
    TableInfo tableInfo = new TableInfo(configuration, clazz);
    tableInfo.setCurrentNamespace(currentNamespace);
    GlobalConfig globalConfig = GlobalConfigUtils.getGlobalConfig(configuration);

    /* 初始化表名相关 */
    final String[] excludeProperty = initTableName(clazz, globalConfig, tableInfo);

    List<String> excludePropertyList = excludeProperty != null && excludeProperty.length > 0 ? Arrays.asList(excludeProperty) : Collections.emptyList();

    /* 初始化字段相关 */
    initTableFields(configuration, clazz, globalConfig, tableInfo, excludePropertyList);

    /* 自动构建 resultMap */
    tableInfo.initResultMapIfNeed();
    globalConfig.getPostInitTableInfoHandler().postTableInfo(tableInfo, configuration);
    TABLE_INFO_CACHE.put(clazz, tableInfo);
    TABLE_NAME_INFO_CACHE.put(tableInfo.getTableName(), tableInfo);

    /* 缓存 lambda */
    LambdaUtils.installCache(tableInfo);
    return tableInfo;
}
3.2.1 初始化表名称 initTableName
private static String[] initTableName(Class<?> clazz, GlobalConfig globalConfig, TableInfo tableInfo) {
        /* 数据库全局配置 */
        GlobalConfig.DbConfig dbConfig = globalConfig.getDbConfig();
        TableName table = clazz.getAnnotation(TableName.class);

        String tableName = clazz.getSimpleName();
        String tablePrefix = dbConfig.getTablePrefix();
        String schema = dbConfig.getSchema();
        boolean tablePrefixEffect = true;
        String[] excludeProperty = null;

        if (table != null) {
            if (StringUtils.isNotBlank(table.value())) {
                tableName = table.value();
                if (StringUtils.isNotBlank(tablePrefix) && !table.keepGlobalPrefix()) {
                    tablePrefixEffect = false;
                }
            } else {
                tableName = initTableNameWithDbConfig(tableName, dbConfig);
            }
            if (StringUtils.isNotBlank(table.schema())) {
                schema = table.schema();
            }
            /* 表结果集映射 */
            if (StringUtils.isNotBlank(table.resultMap())) {
                tableInfo.setResultMap(table.resultMap());
            }
            tableInfo.setAutoInitResultMap(table.autoResultMap());
            excludeProperty = table.excludeProperty();
        } else {
            tableName = initTableNameWithDbConfig(tableName, dbConfig);
        }

        String targetTableName = tableName;
        if (StringUtils.isNotBlank(tablePrefix) && tablePrefixEffect) {
            targetTableName = tablePrefix + targetTableName;
        }
        if (StringUtils.isNotBlank(schema)) {
            targetTableName = schema + StringPool.DOT + targetTableName;
        }

        tableInfo.setTableName(targetTableName);

        /* 开启了自定义 KEY 生成器 */
        if (CollectionUtils.isNotEmpty(dbConfig.getKeyGenerators())) {
            tableInfo.setKeySequence(clazz.getAnnotation(KeySequence.class));
        }
        return excludeProperty;
    }

获取实体类User上面的TableName注解。如果有该注解,那么就取 TableName#value属性;如果没有TableName注解,就取实体类User的类名(默认是下划线转驼峰);最终返回需要排除的字段(com.baomidou.mybatisplus.annotation.TableName#excludeProperty)。
通过@KeySequence注解自定义sequence名称,一般在Oracle中使用。

3.2.2 初始化执行字段
/**
     * <p>
     * 初始化 表主键,表字段
     * </p>
     *
     * @param clazz        实体类
     * @param globalConfig 全局配置
     * @param tableInfo    数据库表反射信息
     */
    private static void initTableFields(Configuration configuration, Class<?> clazz, GlobalConfig globalConfig, TableInfo tableInfo, List<String> excludeProperty) {
        /* 数据库全局配置 */
        GlobalConfig.DbConfig dbConfig = globalConfig.getDbConfig();
        PostInitTableInfoHandler postInitTableInfoHandler = globalConfig.getPostInitTableInfoHandler();
        Reflector reflector = tableInfo.getReflector();
        List<Field> list = getAllFields(clazz);
        // 标记是否读取到主键
        boolean isReadPK = false;
        // 是否存在 @TableId 注解
        boolean existTableId = isExistTableId(list);
        // 是否存在 @TableLogic 注解
        boolean existTableLogic = isExistTableLogic(list);

        List<TableFieldInfo> fieldList = new ArrayList<>(list.size());
        for (Field field : list) {
            if (excludeProperty.contains(field.getName())) {
                continue;
            }

            boolean isPK = false;
            boolean isOrderBy = field.getAnnotation(OrderBy.class) != null;

            /* 主键ID 初始化 */
            if (existTableId) {
                TableId tableId = field.getAnnotation(TableId.class);
                if (tableId != null) {
                    if (isReadPK) {
                        throw ExceptionUtils.mpe("@TableId can't more than one in Class: \"%s\".", clazz.getName());
                    }

                    initTableIdWithAnnotation(dbConfig, tableInfo, field, tableId);
                    isPK = isReadPK = true;
                }
            } else if (!isReadPK) {
                isPK = isReadPK = initTableIdWithoutAnnotation(dbConfig, tableInfo, field);

            }

            if (isPK) {
                if (isOrderBy) {
                    tableInfo.getOrderByFields().add(new TableFieldInfo(dbConfig, tableInfo, field, reflector, existTableLogic, true));
                }
                continue;
            }

            final TableField tableField = field.getAnnotation(TableField.class);

            /* 有 @TableField 注解的字段初始化 */
            if (tableField != null) {
                TableFieldInfo tableFieldInfo = new TableFieldInfo(dbConfig, tableInfo, field, tableField, reflector, existTableLogic, isOrderBy);
                fieldList.add(tableFieldInfo);
                postInitTableInfoHandler.postFieldInfo(tableFieldInfo, configuration);
                continue;
            }

            /* 无 @TableField  注解的字段初始化 */
            TableFieldInfo tableFieldInfo = new TableFieldInfo(dbConfig, tableInfo, field, reflector, existTableLogic, isOrderBy);
            fieldList.add(tableFieldInfo);
            postInitTableInfoHandler.postFieldInfo(tableFieldInfo, configuration);
        }

        /* 字段列表 */
        tableInfo.setFieldList(fieldList);

        /* 未发现主键注解,提示警告信息 */
        if (!isReadPK) {
            logger.warn(String.format("Can not find table primary key in Class: \"%s\".", clazz.getName()));
        }
    }

getAllFields方法获取实体类User(非代理)非static和非transient所有字段。取没有被@TableField注解标注,或者被@TableField标注了但是exist属性为true的字段

1.如果上一步的initTableFields返回的字段里面包含字段的名称,那么就不对字段进行解析
2.主键:如果实体类User中有字段被@TableId标注,那么它就是主键。如果没有的话,那就取属性名称为id的字段作为主键。
最终将主键的的信息保存到了tableInfo的属性中
3.其他字段:直接将字段的属性、数据库字段的column、类型等放入到TableInfo.fieldList属性中,并且初始化了TableInfo的扩展属性,比如:逻辑删除、insert填充、update填充、乐观锁版本等

最后把tableInfo缓存在TableInfoHelper.TABLE_INFO_CACHE(key为实体类)和TABLE_NAME_INFO_CACHE(key为表名)中

3.3 注入Sql SqlInjector

MP默认是由DefaultSqlInjector注入Sql的

	@Override
    public List<AbstractMethod> getMethodList(Class<?> mapperClass, TableInfo tableInfo) {
        Stream.Builder<AbstractMethod> builder = Stream.<AbstractMethod>builder()
            .add(new Insert())
            .add(new Delete())
            .add(new DeleteByMap())
            .add(new Update())
            .add(new SelectByMap())
            .add(new SelectCount())
            .add(new SelectMaps())
            .add(new SelectMapsPage())
            .add(new SelectObjs())
            .add(new SelectList())
            .add(new SelectPage());
        if (tableInfo.havePK()) {
            builder.add(new DeleteById())
                .add(new DeleteBatchByIds())
                .add(new UpdateById())
                .add(new SelectById())
                .add(new SelectBatchByIds());
        } else {
            logger.warn(String.format("%s ,Not found @TableId annotation, Cannot use Mybatis-Plus 'xxById' Method.",
                tableInfo.getEntityType()));
        }
        return builder.build().collect(toList());
    }

注入了很多AbstractMethod,就拿SelectById举例

public class SelectById extends AbstractMethod {

    public SelectById() {
        this(SqlMethod.SELECT_BY_ID.getMethod());
    }

    /**
     * @param name 方法名
     * @since 3.5.0
     */
    public SelectById(String name) {
        super(name);
    }

    @Override
    public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
        SqlMethod sqlMethod = SqlMethod.SELECT_BY_ID;
        SqlSource sqlSource = new RawSqlSource(configuration, String.format(sqlMethod.getSql(),
                sqlSelectColumns(tableInfo, false),
                tableInfo.getTableName(), tableInfo.getKeyColumn(), tableInfo.getKeyProperty(),
                tableInfo.getLogicDeleteSql(true, true)), Object.class);
        return this.addSelectMappedStatementForTable(mapperClass, methodName, sqlSource, tableInfo);
    }
}

其中SqlMethod.SELECT_BY_ID

/**
 * 查询
 */
SELECT_BY_ID("selectById", "根据ID 查询一条数据", "SELECT %s FROM %s WHERE %s=#{%s} %s"),

它对应的BaseMapper接口方法为

/**
 * 根据 ID 查询
 *
 * @param id 主键ID
 */
T selectById(Serializable id);

转换Sql:

  1. sql为SELECT %s FROM %s WHERE %s=#{%s} %s
  2. 第一个%s为tableInfo的查询字段
  3. 第二个%s为表的名称
  4. 第三个%s为数据库主键列名
  5. 第四个%s为实体类名称主键的字段名称
  6. 第五个%s为逻辑删除的条件
  7. 最终解析出的sql为SELECT id,name,age FROM user WHERE id=#{id}

最终this.addSelectMappedStatementForTable方法注入到了MybatisConfiguration.mappedStatements,完成了Sql的自动注入BaseMapper#selectById

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值