How to use Spring Boot Cacheable on self-invocation

How to use Spring Boot Cacheable on self-invocation

Spring Boot has a handful of caching annotations that make working with caching easier. However, these annotations have limitations and do not cater to all cases. For instance, the @Cacheable annotation is ignored on self-invocation. In this article, we focus on this single issue and go through ways how to use Spring Boot Cacheable on self-invocation.

Introduction

If you poke around with Spring Boot long enough, by now you should know that Spring annotations do not work on self-invocations regardless of the method signature. That is because of how Spring dynamic proxy works. 

By default, Spring relies on Spring Proxy AOP to intercept calls to annotated methods, and on self-call, all dynamic proxy logics are bypassed even though no error raises. If you are curious about how dynamic proxies work, we recommend you reading our getting started with dynamic proxies article.

Naturally, Spring Boot @Cacheable is no exception either. However, there are three ways to tackle this problem.

  1. Changing the proxy mode
  2. Using self-instantiation
  3. Implementation change

Changing the proxy mode

As said earlier Spring by default proxies classes using Spring Proxy AOP (based on JDK Dynamic Proxy). The dynamic proxy does not work on self-invocation. To tackle that, we can use another proxying mechanism. AspectJ, for example. However, that requires some changes in how we wire a Spring Boot application. To know more about it, refer to our comprehensive guide about configuring Spring Boot with AspectJ.

Using AspectJ not only solves self-invocation issues but also allows us to apply @Cacheable annotation on private, protected, and package-private methods.

As stated, the change is impactful and may break your entire project depending on how the project is scaffolded. For example, if you use interceptors with DI, switching to AspectJ breaks them since Spring will no longer manage Aspects. Therefore, you must find a different way to inject the Spring Beans into them. Additionally, Jackson classes that don’t have default constructors will break too.

If you are unfamiliar with AspectJ thoroughly, we recommend against using it. It is not worth the complications of adding it to an already mature project.

Using self-instantiation

Another approach to using the @Cacheable annotation on self-invocation is to create an instance of the class within itself. That is known as self-autowiring/self-injection/self-instantiation and was introduced in Spring 4.3. For more, see here and here.

For example,

@Service
public class DefaultUserService implements UserService {
    
    private final UserRepository userRepository;
    
    @Autowired
    private UserService self;

    public DefaultUserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
    
    @Cacheable(USER_BY_COUNTRY_CACHE)
    @Override
    public List<User> getUsersByCountryCode(CountryCode countryCode) {
        return getAllUsers().stream().filter(user -> user.getCountryCode == countryCode).collect(Collectors.toList());
    }

    @Cacheable(USER_CACHE)
    @Override
    public List<User> getAllUsers() {
        return userRepository.findAll();
    }
}

As you can see in the above code, we created an instance of the UserService class using the @Autowired annotation. That makes any self-invocation looks as if it is called from outside. Hence, Spring proxies the calls. However, keep in mind that if we have a private method, this approach will not work.

Using this approach is much easier than the AspectJ one. However, the main drawback of this approach is testability. It is impossible to thoroughly unit test a class that has a self-instance.

We recommend against using self-injection as much as possible unless you have no better option.

Implementation change

The last option, the author’s favorite approach, is to change the implementation AKA refactoring, ironic.

When the default @Cacheable annotation does not work for a use case, that signals anti-patterns and code smells most of the time. For instance, if you are trying to annotate a private method with @Cacheable that indicates that you are doing something wrong, not to mention that your code will be difficult to test. Hence, bugs can crip in easily.

It is better not to waste your time trying to hack the framework. Instead, spend the time refactoring the code. The other maintainers will also thank you.

However, not all issues will necessarily go away with code refactoring. Some require architectural changes. For instance, the situation for self-invocation implies that it’s time to have a caching service and manage your cache in a separate service rather than relying on the default annotations only.

Keep in mind that while refactoring and rearchitecting are the best options, it takes a longer time and that could be a preventive factor why many developers avoid doing it, especially in legacy codebases or when the deadline is approaching rapidly.

Conclusion

In this article, we discussed how to use Spring Boot Cacheable on self-invocation. As discussed, there are three approaches to how to use Spring Boot Cacheable on self-invocation. Each has pros and cons. There’s not a single best solution for all use cases. Based on multiple factors, the best selection may vary. However, we must add that, if you don’t want to spend much time on refactoring and at the same time don’t want to implement a hacky solution, the best approach probably is to use AspectJ proxy mode, as long as get yourself fully familiar with it.

References

Inline/featured images credits