12

While implementing, I came across a problem with Spring Cache Abstraction VS interfaces. Lets say I have the following interface:

package com.example.cache;

public interface IAddItMethod 
{   
    Integer addIt(String key);
}

And the two following implementations:

package com.example.cache;

import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Component;

@Component
public class MethodImplOne implements IAddItMethod 
{
    @Override
    @Cacheable(value="integersPlusOne", key="#keyOne")
    public Integer addIt(String keyOne) 
    {
        return new Integer(Integer.parseInt(keyOne) + 1);
    }
}

.

package com.example.cache;

import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Component;

@Component
public class MethodImplTwo implements IAddItMethod 
{
    @Override
    @Cacheable(value="integersPlusTwo", key="#keyTwo")
    public Integer addIt(String keyTwo)
    {
        return new Integer(Integer.parseInt(keyTwo) + 2);
    }
}

Note that the IAddItMethod is not the one specifying @Cacheable. We could have other implementation (ex MethodImplThree) without the @Cacheable annotation.

We’ve got a simple beans.xml with:

context:component-scan base-package="com.example.cache"

Adding to that, two jUnit test cases:

package com.example.cache;

import static org.junit.Assert.assertEquals;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class) 
@ContextConfiguration(locations = {"classpath:beans.xml"})  
public class MethodImplOneTest 
{

    @Autowired
    @Qualifier("methodImplOne")
    private IAddItMethod classUnderTest;

    @Test
    public void testInit() 
    {
        int number = 1;
        assertEquals(new Integer(number + 1), classUnderTest.addIt("" + number));
    }

}

.

package com.example.cache;

import static org.junit.Assert.assertEquals;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class) 
@ContextConfiguration(locations = {"classpath:beans.xml"})  
public class MethodImplTwoTest 
{

    @Autowired
    @Qualifier("methodImplTwo")
    private IAddItMethod classUnderTest;

    @Test
    public void testInit() 
    {
        int number = 1;
        assertEquals(new Integer(number + 2), classUnderTest.addIt("" + number));
    }

}

When I run the tests individually, they succeed. However, if I run them both together (selecting the package, right-click, run as), the second one (not necessarily MethodImplTwoTest, just the second one running) will fail with the following exception:

java.lang.IllegalArgumentException: Null key returned for cache operation (maybe you are using named params on classes without debug info?) CacheableOperation[public java.lang.Integer com.example.cache.MethodImplOne.addIt(java.lang.String)] caches=[integersPlusOne] | condition='' | key='#keyOne'
    at org.springframework.cache.interceptor.CacheAspectSupport.inspectCacheables(CacheAspectSupport.java:297)
    at org.springframework.cache.interceptor.CacheAspectSupport.execute(CacheAspectSupport.java:198)
    at org.springframework.cache.interceptor.CacheInterceptor.invoke(CacheInterceptor.java:66)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:202)
    at $Proxy16.addIt(Unknown Source)
    at com.example.cache.ITMethodImplOneIntegrationTest.testInit(ITMethodImplOneIntegrationTest.java:26)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
    at java.lang.reflect.Method.invoke(Method.java:597)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:45)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:42)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:20)
    at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:74)
    at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:83)
    at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:72)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:231)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:47)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:231)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:60)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:229)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:50)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:222)
    at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
    at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:71)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:300)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:174)
    at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:50)
    at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:467)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:683)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:390)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:197)

note: I'm using Eclipse STS 3.0 and the "Add variable attributes to generated class files" is enabled.

IMPORTANT: If I don't specify the "key" in the @Cacheable annotations, it works.

Is there anything I forgot to specify? config? annotations?

Thanks in advance!

dostiguy
  • 123
  • 1
  • 1
  • 4

1 Answers1

13

My guess is that for jdk proxy the parameter name is fetched from the interface method so it's key and not keyTwo.

update: You can try to use parameter indexes instead

If for some reason the names are not available (ex: no debug information), the parameter names are also available under the p<#arg> where #arg stands for the parameter index (starting from 0).

see http://static.springsource.org/spring/docs/3.1.0.M1/spring-framework-reference/html/cache.html#cache-spel-context

Boris Treukhov
  • 17,493
  • 9
  • 70
  • 91
  • It works thanks! However, I consider this to be restrictive and it is complicated to enforce the implementations to respect that. I'll add complete javadoc in the interface and current implementations but another developer might stumble on the same problem for a new implementation. Thanks again Boris! – dostiguy Jan 07 '13 at 15:17
  • @dostiguy I added a link to the documentation regarding accessing parameters by indexes, I haven't tried it myself though – Boris Treukhov Jan 07 '13 at 16:04
  • Good thinking, I'd forgotten we could do that. I tried it and ran my tests (real implementation has around 100 test cases, and the keys are more complicated than the example) and it works. Now I'm just not sure which way I prefer. Thanks again. – dostiguy Jan 07 '13 at 19:34
  • 2
    Found this issue when adding `@Cachable` to a `@FeignClient` interface. Thanks!!!!! – Nick Mar 30 '18 at 15:03