当前使用版本(必填,否则不予处理)

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