当前使用版本(必填,否则不予处理)
3.4.0
该问题是如何引起的?(确定最新版也有问题再提!!!)
在Mapper中使用@CacheNamespaceRef注解引用其他Mapper的二级缓存,但是有一个问题,在MybatisPlus加载Mapper的时候,如果当前正在加载的Mapper上注解@CacheNamespaceRef所引用的其他Mapper的缓存还未被创建时,那么就会引发异常:throw new IncompleteElementException("Cache-ref not yet resolved")
重现步骤(如果有就写完整)
我研究了一下MybatisPlus的相关源码,MapperBuilderAssistant.useCacheRef()方法会从缓存Map中去拿对应namespace的缓存,而且该类每一个Mapper对应一个,且有一个标识字段unresolvedCacheRef用来标识缓存是否被解析。那么当所引用的缓存还没加载时,自然是找不到的,然后会抛出一个异常throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.", e),但是比较幸运的是调用useCacheRef方法的类MybatisMapperAnnotationBuilder.parseCacheRef方法捕获了该异常,同时添加了延迟处理:
private void parseCacheRef() {
CacheNamespaceRef cacheDomainRef = type.getAnnotation(CacheNamespaceRef.class);
if (cacheDomainRef != null) {
Class<?> refType = cacheDomainRef.value();
String refName = cacheDomainRef.name();
if (refType == void.class && refName.isEmpty()) {
throw new BuilderException("Should be specified either value() or name() attribute in the @CacheNamespaceRef");
}
if (refType != void.class && !refName.isEmpty()) {
throw new BuilderException("Cannot use both value() and name() attribute in the @CacheNamespaceRef");
}
String namespace = (refType != void.class) ? refType.getName() : refName;
try {
assistant.useCacheRef(namespace);
} catch (IncompleteElementException e) {
// 重点在这里,捕获了异常,添加延迟处理了,这是非常合理的一步,可以解决缓存依赖问题
configuration.addIncompleteCacheRef(new CacheRefResolver(assistant, namespace));
}
}
}
但是这里还没结束,继续看调用parseCacheRef方法的依旧是类MybatisMapperAnnotationBuilder.parse方法:
public void parse() {
String resource = type.toString();
if (!configuration.isResourceLoaded(resource)) {
loadXmlResource();
configuration.addLoadedResource(resource);
String mapperName = type.getName();
assistant.setCurrentNamespace(mapperName);
parseCache();
parseCacheRef();
// 注意啊,这里上一步可能没有解析到正确的缓存,添加了延迟处理,但是后面的逻辑并没有做延迟处理,而且照走不误
// 那么问题出现的关键就在后面的代码调用注入方法的逻辑:builderAssistant.addMappedStatement()
InterceptorIgnoreHelper.InterceptorIgnoreCache cache = 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 {
parseStatement(method);
// TODO 加入 SqlParser 注解过滤缓存
InterceptorIgnoreHelper.initSqlParserInfoCache(cache, mapperName, method);
SqlParserHelper.initSqlParserInfoCache(mapperName, method);
} catch (IncompleteElementException e) {
// TODO 使用 MybatisMethodResolver 而不是 MethodResolver
configuration.addIncompleteMethod(new MybatisMethodResolver(this, method));
}
}
// TODO 注入 CURD 动态 SQL , 放在在最后, because 可能会有人会用注解重写sql
if (GlobalConfigUtils.isSupperMapperChildren(configuration, type)) {
GlobalConfigUtils.getSqlInjector(configuration).inspectInject(assistant, type);
}
}
parsePendingMethods();
}
那么再看:builderAssistant.addMappedStatement()的逻辑:
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) {
// 关键代码在这里,后面不要看,此时unresolvedCacheRef变量必然是true呀,肯定没解析到缓存,所以必然抛出此异常
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;
}
现在问题的整体脉络清晰了,原因也找到了,在没有解析到正确的二级缓存Cache对象之前,后面的动态方法应该也跟缓存的延迟处理一样,做类似的延迟处理。 扫一眼延迟处理的过程:
protected void buildAllStatements() {
parsePendingResultMaps();
if (!incompleteCacheRefs.isEmpty()) {
synchronized (incompleteCacheRefs) {
incompleteCacheRefs.removeIf(x -> x.resolveCacheRef() != null);
}
}
if (!incompleteStatements.isEmpty()) {
synchronized (incompleteStatements) {
incompleteStatements.removeIf(x -> {
x.parseStatementNode();
return true;
});
}
}
if (!incompleteMethods.isEmpty()) {
synchronized (incompleteMethods) {
incompleteMethods.removeIf(x -> {
x.resolve();
return true;
});
}
}
}
此方法存在于mybatis官方的Configuration类中,MybatisPlus并没有重写它,但是MybatisPlus调用它了:
public Collection<String> getMappedStatementNames() {
buildAllStatements();
return mappedStatements.keySet();
}
public Collection<MappedStatement> getMappedStatements() {
buildAllStatements();
return mappedStatements.values();
}
public MappedStatement getMappedStatement(String id) {
return this.getMappedStatement(id, true);
}
public MappedStatement getMappedStatement(String id, boolean validateIncompleteStatements) {
if (validateIncompleteStatements) {
buildAllStatements();
}
return mappedStatements.get(id);
}
public boolean hasStatement(String statementName, boolean validateIncompleteStatements) {
if (validateIncompleteStatements) {
buildAllStatements();
}
return mappedStatements.containsKey(statementName);
}
可见很多方法都调用过延迟加载的方法。
报错信息
Caused by: org.apache.ibatis.builder.IncompleteElementException: Cache-ref not yet resolved
at org.apache.ibatis.builder.MapperBuilderAssistant.addMappedStatement(MapperBuilderAssistant.java:267) ~[mybatis-3.5.5.jar:3.5.5]
at com.baomidou.mybatisplus.core.injector.AbstractMethod.addMappedStatement(AbstractMethod.java:331) ~[mybatis-plus-core-3.4.0.jar:3.4.0]
at com.baomidou.mybatisplus.core.injector.AbstractMethod.addInsertMappedStatement(AbstractMethod.java:293) ~[mybatis-plus-core-3.4.0.jar:3.4.0]
at com.baomidou.mybatisplus.core.injector.methods.Insert.injectMappedStatement(Insert.java:67) ~[mybatis-plus-core-3.4.0.jar:3.4.0]
at com.baomidou.mybatisplus.core.injector.AbstractMethod.inject(AbstractMethod.java:63) ~[mybatis-plus-core-3.4.0.jar:3.4.0]
at com.baomidou.mybatisplus.core.injector.AbstractSqlInjector.lambda$inspectInject$0(AbstractSqlInjector.java:55) ~[mybatis-plus-core-3.4.0.jar:3.4.0]
at com.baomidou.mybatisplus.core.injector.AbstractSqlInjector$$Lambda$579/00000000E2C0F620.accept(Unknown Source) ~[na:na]
at java.base/java.util.ArrayList.forEach(ArrayList.java:1541) ~[na:na]
at com.baomidou.mybatisplus.core.injector.AbstractSqlInjector.inspectInject(AbstractSqlInjector.java:55) ~[mybatis-plus-core-3.4.0.jar:3.4.0]
at com.baomidou.mybatisplus.core.MybatisMapperAnnotationBuilder.parse(MybatisMapperAnnotationBuilder.java:121) ~[mybatis-plus-core-3.4.0.jar:3.4.0]
at com.baomidou.mybatisplus.core.MybatisMapperRegistry.addMapper(MybatisMapperRegistry.java:83) ~[mybatis-plus-core-3.4.0.jar:3.4.0]
at com.baomidou.mybatisplus.core.MybatisConfiguration.addMapper(MybatisConfiguration.java:152) ~[mybatis-plus-core-3.4.0.jar:3.4.0]
at org.apache.ibatis.builder.xml.XMLMapperBuilder.bindMapperForNamespace(XMLMapperBuilder.java:432) ~[mybatis-3.5.5.jar:3.5.5]
at org.apache.ibatis.builder.xml.XMLMapperBuilder.parse(XMLMapperBuilder.java:97) ~[mybatis-3.5.5.jar:3.5.5]
at com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean.buildSqlSessionFactory(MybatisSqlSessionFactoryBean.java:593) ~[mybatis-plus-extension-3.4.0.jar:3.4.0]
... 124 common frames omitted
附加一句
我也尝试通过继承覆盖部分核心方法来解决此问题,但是可惜MybatisPlus大部分类的方法是私有的,没法覆盖,虽然可以通过复制过来重写部分代码解决,但是这对升级极不友好,万一官方改了少许代码,对我而言我不可能每天对比我重写的文件做比对升级,太麻烦了。最后还是希望官方可以解决这个问题,在此表示衷心的感谢!~~~~😁我还是喜欢做个胜手党,哈哈
Comment From: huanghaifeng98
牛逼
Comment From: forzyh
牛