mybatis-plus的sql加载顺序源码解析
作者:codecraft
序
本文主要研究一下如果mybatis mapper定义了多个同名方法会不会有问题
MybatisConfiguration
com/baomidou/mybatisplus/core/MybatisConfiguration.java
/** * MybatisPlus 加载 SQL 顺序: * <p> 1、加载 XML中的 SQL </p> * <p> 2、加载 SqlProvider 中的 SQL </p> * <p> 3、XmlSql 与 SqlProvider不能包含相同的 SQL </p> * <p>调整后的 SQL优先级:XmlSql > sqlProvider > CurdSql </p> */ @Override public void addMappedStatement(MappedStatement ms) { if (mappedStatements.containsKey(ms.getId())) { /* * 说明已加载了xml中的节点; 忽略mapper中的 SqlProvider 数据 */ logger.error("mapper[" + ms.getId() + "] is ignored, because it exists, maybe from xml file"); return; } mappedStatements.put(ms.getId(), ms); }
MybatisSqlSessionFactoryBean
com/baomidou/mybatisplus/extension/spring/MybatisSqlSessionFactoryBean.java
/** * Build a {@code SqlSessionFactory} instance. * <p> * The default implementation uses the standard MyBatis {@code XMLConfigBuilder} API to build a * {@code SqlSessionFactory} instance based on an Reader. Since 1.3.0, it can be specified a * {@link Configuration} instance directly(without config file). * </p> * * @return SqlSessionFactory * @throws IOException if loading the config file failed */ protected SqlSessionFactory buildSqlSessionFactory() throws Exception { //...... if (this.mapperLocations != null) { if (this.mapperLocations.length == 0) { LOGGER.warn(() -> "Property 'mapperLocations' was specified but matching resources are not found."); } else { for (Resource mapperLocation : this.mapperLocations) { if (mapperLocation == null) { continue; } try { XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(), targetConfiguration, mapperLocation.toString(), targetConfiguration.getSqlFragments()); xmlMapperBuilder.parse(); } catch (Exception e) { throw new IOException("Failed to parse mapping resource: '" + mapperLocation + "'", e); } finally { ErrorContext.instance().reset(); } LOGGER.debug(() -> "Parsed mapper file: '" + mapperLocation + "'"); } } } }
MybatisSqlSessionFactoryBean的buildSqlSessionFactory方法会根据mapperLocations的配置取加载xml配置,即加载xml的mapper信息
XMLMapperBuilder
org/apache/ibatis/builder/xml/XMLMapperBuilder.java
public void parse() { if (!configuration.isResourceLoaded(resource)) { configurationElement(parser.evalNode("/mapper")); configuration.addLoadedResource(resource); bindMapperForNamespace(); } parsePendingResultMaps(); parsePendingCacheRefs(); parsePendingStatements(); } 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 && !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); configuration.addMapper(boundType); } } }
XMLMapperBuilder的parse方法会执行configurationElement,即加载xml的mapper方法,之后执行bindMapperForNamespace,加载对应java mapper的方法
MybatisMapperRegistry
com/baomidou/mybatisplus/core/MybatisMapperRegistry.java
@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); } } } }
MybatisMapperRegistry通过MybatisMapperAnnotationBuilder进行parse
MybatisMapperAnnotationBuilder
com/baomidou/mybatisplus/core/MybatisMapperAnnotationBuilder.java
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(); }
这里通过反射获取对应java mapper的方法(这里的顺序是先接口本身定义的方法,然后是逐层继承的接口定义的方法),然后挨个执行parseStatement,接着执行parserInjector来处理内置的通过SqlMethod提供的内置方法
parseStatement
private static final Set<Class<? extends Annotation>> statementAnnotationTypes = Stream .of(Select.class, Update.class, Insert.class, Delete.class, SelectProvider.class, UpdateProvider.class, InsertProvider.class, DeleteProvider.class) .collect(Collectors.toSet()); void parseStatement(Method method) { final Class<?> parameterTypeClass = getParameterType(method); final LanguageDriver languageDriver = getLanguageDriver(method); getAnnotationWrapper(method, true, statementAnnotationTypes).ifPresent(statementAnnotation -> { final SqlSource sqlSource = buildSqlSource(statementAnnotation.getAnnotation(), parameterTypeClass, languageDriver, method); final SqlCommandType sqlCommandType = statementAnnotation.getSqlCommandType(); final Options options = getAnnotationWrapper(method, false, Options.class).map(x -> (Options) x.getAnnotation()).orElse(null); final String mappedStatementId = type.getName() + StringPool.DOT + method.getName(); final KeyGenerator keyGenerator; String keyProperty = null; String keyColumn = null; if (SqlCommandType.INSERT.equals(sqlCommandType) || SqlCommandType.UPDATE.equals(sqlCommandType)) { // first check for SelectKey annotation - that overrides everything else SelectKey selectKey = getAnnotationWrapper(method, false, SelectKey.class).map(x -> (SelectKey) x.getAnnotation()).orElse(null); if (selectKey != null) { keyGenerator = handleSelectKeyAnnotation(selectKey, mappedStatementId, getParameterType(method), languageDriver); keyProperty = selectKey.keyProperty(); } else if (options == null) { keyGenerator = configuration.isUseGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE; } else { keyGenerator = options.useGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE; keyProperty = options.keyProperty(); keyColumn = options.keyColumn(); } } else { keyGenerator = NoKeyGenerator.INSTANCE; } Integer fetchSize = null; Integer timeout = null; StatementType statementType = StatementType.PREPARED; ResultSetType resultSetType = configuration.getDefaultResultSetType(); boolean isSelect = sqlCommandType == SqlCommandType.SELECT; boolean flushCache = !isSelect; boolean useCache = isSelect; if (options != null) { if (FlushCachePolicy.TRUE.equals(options.flushCache())) { flushCache = true; } else if (FlushCachePolicy.FALSE.equals(options.flushCache())) { flushCache = false; } useCache = options.useCache(); fetchSize = options.fetchSize() > -1 || options.fetchSize() == Integer.MIN_VALUE ? options.fetchSize() : null; //issue #348 timeout = options.timeout() > -1 ? options.timeout() : null; statementType = options.statementType(); if (options.resultSetType() != ResultSetType.DEFAULT) { resultSetType = options.resultSetType(); } } String resultMapId = null; if (isSelect) { ResultMap resultMapAnnotation = method.getAnnotation(ResultMap.class); if (resultMapAnnotation != null) { resultMapId = String.join(StringPool.COMMA, resultMapAnnotation.value()); } else { resultMapId = generateResultMapName(method); } } assistant.addMappedStatement( mappedStatementId, sqlSource, statementType, sqlCommandType, fetchSize, timeout, // ParameterMapID null, parameterTypeClass, resultMapId, getReturnType(method), resultSetType, flushCache, useCache, // TODO gcode issue #577 false, keyGenerator, keyProperty, keyColumn, statementAnnotation.getDatabaseId(), languageDriver, // ResultSets options != null ? nullOrEmpty(options.resultSets()) : null); }); }
parseStatement这里解析带有Select.class, Update.class, Insert.class, Delete.class, SelectProvider.class, UpdateProvider.class, InsertProvider.class, DeleteProvider.class注解的方法,然后通过assistant.addMappedStatement注册到configuration的mappedStatements中,key为statementId(type.getName() + StringPool.DOT + method.getName())
parserInjector
com/baomidou/mybatisplus/core/MybatisMapperAnnotationBuilder.java
void parserInjector() { GlobalConfigUtils.getSqlInjector(configuration).inspectInject(assistant, type); }
com/baomidou/mybatisplus/core/injector/AbstractSqlInjector.java
@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); } } }
com/baomidou/mybatisplus/core/injector/AbstractMethod.java
public void inject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) { this.configuration = builderAssistant.getConfiguration(); this.builderAssistant = builderAssistant; this.languageDriver = configuration.getDefaultScriptingLanguageInstance(); /* 注入自定义方法 */ injectMappedStatement(mapperClass, modelClass, tableInfo); } protected MappedStatement addInsertMappedStatement(Class<?> mapperClass, Class<?> parameterType, String id, SqlSource sqlSource, KeyGenerator keyGenerator, String keyProperty, String keyColumn) { return addMappedStatement(mapperClass, id, sqlSource, SqlCommandType.INSERT, parameterType, null, Integer.class, keyGenerator, keyProperty, keyColumn); } protected MappedStatement addMappedStatement(Class<?> mapperClass, String id, SqlSource sqlSource, SqlCommandType sqlCommandType, Class<?> parameterType, String resultMap, Class<?> resultType, KeyGenerator keyGenerator, String keyProperty, String keyColumn) { String statementName = mapperClass.getName() + DOT + id; if (hasMappedStatement(statementName)) { logger.warn(LEFT_SQ_BRACKET + statementName + "] Has been loaded by XML or SqlProvider or Mybatis's Annotation, so ignoring this injection for [" + getClass() + RIGHT_SQ_BRACKET); return null; } /* 缓存逻辑处理 */ boolean isSelect = sqlCommandType == SqlCommandType.SELECT; return builderAssistant.addMappedStatement(id, sqlSource, StatementType.PREPARED, sqlCommandType, null, null, null, parameterType, resultMap, resultType, null, !isSelect, isSelect, false, keyGenerator, keyProperty, keyColumn, configuration.getDatabaseId(), languageDriver, null); }
这里会通过statementName(mapperClass.getName() + DOT + id
)j检测是否存在,如果不存在则添加
org/apache/ibatis/builder/MapperBuilderAssistant.java
public MappedStatement addMappedStatement( String id, SqlSource sqlSource, StatementType statementType, SqlCommandType sqlCommandType, Integer fetchSize, Integer timeout, String parameterMap, Class<?> parameterType, String resultMap, Class<?> resultType, ResultSetType resultSetType, boolean flushCache, boolean useCache, boolean resultOrdered, KeyGenerator keyGenerator, String keyProperty, String keyColumn, String databaseId, LanguageDriver lang, String resultSets) { if (unresolvedCacheRef) { throw new IncompleteElementException("Cache-ref not yet resolved"); } id = applyCurrentNamespace(id, false); boolean isSelect = sqlCommandType == SqlCommandType.SELECT; MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType) .resource(resource) .fetchSize(fetchSize) .timeout(timeout) .statementType(statementType) .keyGenerator(keyGenerator) .keyProperty(keyProperty) .keyColumn(keyColumn) .databaseId(databaseId) .lang(lang) .resultOrdered(resultOrdered) .resultSets(resultSets) .resultMaps(getStatementResultMaps(resultMap, resultType, id)) .resultSetType(resultSetType) .flushCacheRequired(valueOrDefault(flushCache, !isSelect)) .useCache(valueOrDefault(useCache, isSelect)) .cache(currentCache); ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id); if (statementParameterMap != null) { statementBuilder.parameterMap(statementParameterMap); } MappedStatement statement = statementBuilder.build(); configuration.addMappedStatement(statement); return statement; } public String applyCurrentNamespace(String base, boolean isReference) { if (base == null) { return null; } if (isReference) { // is it qualified with any namespace yet? if (base.contains(".")) { return base; } } else { // is it qualified with this namespace yet? if (base.startsWith(currentNamespace + ".")) { return base; } if (base.contains(".")) { throw new BuilderException("Dots are not allowed in element names, please remove it from " + base); } } return currentNamespace + "." + base; }
添加的话,最后的id会拼接上当前的namespace
小结
如果mybatis mapper定义了多个同名方法,则启动时不会报错,但是会有error日志告知同名方法被忽略。整体加载顺序是xml的方法优先于java mapper定义的方法,优先于自定义的SqlMethod;而xml或者java mapper方法都是以最先出现的为准。
以上就是mybatis-plus的sql加载顺序源码解析的详细内容,更多关于mybatis-plus sql加载顺序的资料请关注脚本之家其它相关文章!