springspring-bootspring-aopspring-cache

Why @Cachable works on not public method in Spring Boot 3.0


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.


Solution

  • 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.


    Update after MCVE availability

    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.