Spring Cacheable 使用 varArgs 产生 ArrayIndexOutOfBoundsException

Spring Cacheable produces ArrayIndexOutOfBoundsException with varArgs

我们刚刚从 spring 3.2.6 升级到 4.2.6。 迁移后出现异常。

java.lang.ArrayIndexOutOfBoundsException: 1
at org.springframework.context.expression.MethodBasedEvaluationContext.lazyLoadArguments(MethodBasedEvaluationContext.java:93) ~[spring-context-4.2.6.RELEASE.jar:4.2.6.RELEASE]
at org.springframework.context.expression.MethodBasedEvaluationContext.lookupVariable(MethodBasedEvaluationContext.java:67) ~[spring-context-4.2.6.RELEASE.jar:4.2.6.RELEASE]
at org.springframework.cache.interceptor.CacheEvaluationContext.lookupVariable(CacheEvaluationContext.java:74) ~[spring-context-4.2.6.RELEASE.jar:4.2.6.RELEASE]
at org.springframework.expression.spel.ExpressionState.lookupVariable(ExpressionState.java:144) ~[spring-expression-4.2.6.RELEASE.jar:4.2.6.RELEASE]
at org.springframework.expression.spel.ast.VariableReference.getValueInternal(VariableReference.java:75) ~[spring-expression-4.2.6.RELEASE.jar:4.2.6.RELEASE]
at org.springframework.expression.spel.ast.CompoundExpression.getValueRef(CompoundExpression.java:51) ~[spring-expression-4.2.6.RELEASE.jar:4.2.6.RELEASE]
at org.springframework.expression.spel.ast.CompoundExpression.getValueInternal(CompoundExpression.java:87) ~[spring-expression-4.2.6.RELEASE.jar:4.2.6.RELEASE]
at org.springframework.expression.spel.ast.SpelNodeImpl.getValue(SpelNodeImpl.java:120) ~[spring-expression-4.2.6.RELEASE.jar:4.2.6.RELEASE]
at org.springframework.expression.spel.standard.SpelExpression.getValue(SpelExpression.java:242) ~[spring-expression-4.2.6.RELEASE.jar:4.2.6.RELEASE]
at org.springframework.cache.interceptor.ExpressionEvaluator.key(ExpressionEvaluator.java:115) ~[spring-context-4.2.6.RELEASE.jar:4.2.6.RELEASE]
at org.springframework.cache.interceptor.CacheAspectSupport$CacheOperationContext.generateKey(CacheAspectSupport.java:632) ~[spring-context-4.2.6.RELEASE.jar:4.2.6.RELEASE]
at org.springframework.cache.interceptor.CacheAspectSupport.generateKey(CacheAspectSupport.java:487) ~[spring-context-4.2.6.RELEASE.jar:4.2.6.RELEASE]
at org.springframework.cache.interceptor.CacheAspectSupport.findCachedItem(CacheAspectSupport.java:431) ~[spring-context-4.2.6.RELEASE.jar:4.2.6.RELEASE]
at org.springframework.cache.interceptor.CacheAspectSupport.execute(CacheAspectSupport.java:336) ~[spring-context-4.2.6.RELEASE.jar:4.2.6.RELEASE]
at org.springframework.cache.interceptor.CacheAspectSupport.execute(CacheAspectSupport.java:302) ~[spring-context-4.2.6.RELEASE.jar:4.2.6.RELEASE]
at org.springframework.cache.interceptor.CacheInterceptor.invoke(CacheInterceptor.java:61) ~[spring-context-4.2.6.RELEASE.jar:4.2.6.RELEASE]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.2.6.RELEASE.jar:4.2.6.RELEASE]
at org.springframework.transaction.interceptor.TransactionInterceptor.proceedWithInvocation(TransactionInterceptor.java:99) ~[spring-tx-4.2.6.RELEASE.jar:4.2.6.RELEASE]
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:281) ~[spring-tx-4.2.6.RELEASE.jar:4.2.6.RELEASE]
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96) ~[spring-tx-4.2.6.RELEASE.jar:4.2.6.RELEASE]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.2.6.RELEASE.jar:4.2.6.RELEASE]
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:208) ~[spring-aop-4.2.6.RELEASE.jar:4.2.6.RELEASE]
at com.sun.proxy.$Proxy122.getUserBylId(Unknown Source) ~[na:na]

当我们调用带有空变量参数的服务方法时会发生这种情况。是这样定义的

@Transactional(readOnly = true)
@Override
@Cacheable(value = "users", key = "#id")
public final User getUserBylId(String id, EntityFilter<User>... filters) {}

这个方法的调用是这样的

userService.getUserById(ID)

所以问题是我没有将任何参数作为过滤器传递。 当我传递任何东西时它工作正常。

对我来说这似乎是一个错误,但我在 spring 资源中找不到任何相关的线程或问题。

我已将问题追踪到两个地方。

此处,spring 将调用参数提取到一个数组中。它有一个看起来正确的 varArgs 特例。 运行 这段代码为我的案例生成了一个带有一个 ID 元素 [id] 的数组,这看起来不错。

protected class CacheOperationContext ...{
    .....
    private Object[] extractArgs(Method method, Object[] args) {
        if (!method.isVarArgs()) {
            return args;
        }
        Object[] varArgs = ObjectUtils.toObjectArray(args[args.length - 1]);
        Object[] combinedArgs = new Object[args.length - 1 + varArgs.length];
        System.arraycopy(args, 0, combinedArgs, 0, args.length - 1);
        System.arraycopy(varArgs, 0, combinedArgs, args.length - 1, varArgs.length);
        return combinedArgs;
    }
    ....
}

第二名是

public class MethodBasedEvaluationContext ...{
    ....
    protected void lazyLoadArguments() {
        // shortcut if no args need to be loaded
        if (ObjectUtils.isEmpty(this.args)) {
            return;
        }

       // save arguments as indexed variables
       for (int i = 0; i < this.args.length; i++) {
           setVariable("a" + i, this.args[i]);
           setVariable("p" + i, this.args[i]);
       }

       String[] parameterNames = this.paramDiscoverer.getParameterNames(this.method);
       // save parameter names (if discovered)
       if (parameterNames != null) {
           for (int i = 0; i < parameterNames.length; i++) {
                setVariable(parameterNames[i], this.args[i]);
           }
       }
    }
}

这里 spring 正在遍历参数名称并尝试在参数数组中找到相应的属性。而且这个数组只有 1 个元素而不是 2 个。

这似乎是一个错误。我试图回退到 spring 4.1.7,它有同样的问题。 我主要担心的是这段代码在 spring 内部如此之深,以至于没有 'clean' 方法可以覆盖此行为。 我能想到的唯一方法是分叉 spring-context,我不太喜欢这样做。 你怎么看?请指教=)

或者,如果您确定问题的根本原因并包含一个可重现的测试用例,那么您可以修复它并向 Spring 框架项目提交 PR

乍一看,这个问题似乎很容易重现。但是,我只是写了一个非常简单的 quick/contrived test class with a couple of test cases to test this particular issue,它使用核心 Spring Framework 4.2.6.RELEASE.

可以正常工作

请注意,我将可能影响缓存交互成功的变量数量降至最低,因此 my SUT 不是 @Transactional

@Transactional 注释可能有影响,但是,这不太可能,而且我还没有听说过事务划分与缓存混合导致任何问题的任何情况。

我可能会在测试用例中将 2 分开,这与不 "enabling" 事务管理一样简单,看看是否有任何改变。但是,据我所知,@Cacheable var args (service) 方法按预期工作。

请报告您的任何其他发现或信息。

谢谢!