@[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);
}
}
}
}
- 如果当前的UserMapper不是接口,那么就什么都不执行。
- 如果当前的接口保存在knownMappers的key里面,那么就直接返回。
- 先将当前的接口和它对应的代理工厂保存到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接口没有被加载:
- 那么就先去加载当前mapper对应的xml文件
- canHaveStatement(method) 如果当前接口不是桥接方法也不是default方法,那么才会对注解进行解析
- 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);
}
}
}
- 获取Mapper接口的泛型,也就是User.class
- 获取mapperRegistryCache, mapperRegistryCache缓存已注入CRUD的Mapper信息
- 初始化table信息
- 获取modelClass(也就是User.class)的所有方法,然后调用inject方法注入sql
- 将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:
- sql为SELECT %s FROM %s WHERE %s=#{%s} %s
- 第一个%s为tableInfo的查询字段
- 第二个%s为表的名称
- 第三个%s为数据库主键列名
- 第四个%s为实体类名称主键的字段名称
- 第五个%s为逻辑删除的条件
- 最终解析出的sql为SELECT id,name,age FROM user WHERE id=#{id}
最终this.addSelectMappedStatementForTable方法注入到了MybatisConfiguration.mappedStatements,完成了Sql的自动注入BaseMapper#selectById