I'm using Spring Boot 3.x, and the @Cacheable
annotation is working on methods that are not public. This is strange because the documentation clearly states:
Method visibility and cache annotations
When you use proxies, you should apply the cache annotations only to methods with public visibility. If you do annotate protected, private, or package-visible methods with these annotations, no error is raised, but the annotated method does not exhibit the configured caching settings. Consider using AspectJ (see the rest of this section) if you need to annotate non-public methods, as it changes the bytecode itself.
Here is my sample service:
@Service
public class CacheService {
@Cacheable("cache1")
String foo() {
System.out.println("string");
return "string";
}
}
And a very simple test:
@SpringBootTest
class ApplicationTests {
@Autowired
private CacheService cacheService;
@Test
void test2() {
cacheService.foo();
cacheService.foo();
cacheService.foo();
cacheService.foo();
cacheService.foo();
cacheService.foo();
}
}
In Spring Boot 2.7, the console output shows 6 "string" messages, but in 3.0.0 (and above), it only shows 1 "string".
Do you have any idea what changed, and where in the documentation this behavior is described?
This came up during a company presentation, and I was confident I understood the behavior of this annotation, so I'm looking for arguments to justify it :)
EDIT
Here's the example with branch spring-2.7
and spring-3.0
.
You are not using Spring's default JDK interface proxies but are directly proxying a class, i.e. the result will be a CGLIB proxy, which basically is a subclass of the original class. I.e., non-public methods can and will be proxied and delegated to their original target methods, as long as they are not private. This is the expected behaviour for Spring proxies. For reference:
NOTE
Due to the proxy-based nature of Spring's AOP framework, calls within the target object are, by definition, not intercepted. For JDK proxies, only public interface method calls on the proxy can be intercepted. With CGLIB, public and protected method calls on the proxy are intercepted (and even package-visible methods, if necessary). However, common interactions through proxies should always be designed through public signatures.
Given the following modification of your service and test:
@Service
public class CacheService {
@Cacheable("cache1")
public String publicMethod() {
System.out.println("public");
return "public";
}
@Cacheable("cache1")
String nonPublicMethod() {
System.out.println("non-public");
return "non-public";
}
}
@SpringBootTest
class ApplicationTests {
@Autowired
private CacheService cacheService;
@Test
void test2() {
cacheService.publicMethod();
cacheService.publicMethod();
cacheService.publicMethod();
cacheService.nonPublicMethod();
cacheService.nonPublicMethod();
cacheService.nonPublicMethod();
}
}
I see the following behaviour, depending on Spring Boot version and whether I comment out the @Cacheable
annotation from either of the two methods:
Spring Boot version | Has @Cacheable public |
Has @Cacheable non-public |
Behaviour |
---|---|---|---|
2.7 | yes | no | Only public method is cached |
2.7 | no | yes | No method is cached |
2.7 | yes | yes | Only public method is cached |
3.0 | yes | no | Only public method is cached |
3.0 | no | yes | Only non-public method is cached |
3.0 | yes | yes | Both public and non-public methods are cached |
In a nutshell: In Spring 2.7, caching consequently is only applied to public methods. In Spring 3.0, caching also works for non-public (but non-private) methods, despite the recommendation - it really is nothing more than that - to use the annotation only on public APIs.
Some research in the Spring Core repository revealed issue #25582, "@Transactional does not work on package protected methods of CGLib proxies". It was closed for Spring 6.0 by commit 37bebeaaaf with the comment: "Accept protected @Transactional/Cacheable methods on CGLIB proxies". I.e., it was changed on purpose, which IMO makes sense, because it makes Spring more versatile in special situations, and it works similarly in other Spring proxy contexts, e.g. for Spring AOP.