I want to share with you a common mistake I find in codebases using Spring when it comes to features auto-magically added by annotations.
Let's start with this piece of code :
@Service
public class MyLibrary {
BookRepository bookRepository;
@Cacheable("books")
public Collection<Book> fetchAllBooks() {
return bookRepository.fetchAllBooks();
}
}
Pretty straightforward: in our bean MyLibrary, the method fetchAllBooks() fetches some books and the annotation @Cacheable from Spring Cache enables some caching of the result to avoid doing the query at every call. So far so good.
What if I add another method :
@Service
public class MyLibrary {
BookRepository bookRepository;
public void createBook(Book Book) {
Collection<Book> existingCollection = this.fetchAllBooks();
}
@Cacheable("books")
public Collection<Book> fetchAllBooks() {
return bookRepository.fetchAllBooks();
}
}
Here the method createBook() needs to fetch some books too (bear with me for the sake of the example), so it makes sense to call the one already there, right? We expect the method createBook() to get the collection of existing books from the cache.
Unfortunately, it won't work as expected: the cache will never be used, and here is why.
All the nice features of Spring enabled by those annotations are implemented using proxies (and a lot of AOP). As you may know, when you inject the MyLibrary bean, you don't get exactly the instance of the MyLibrary class, instead, you get a proxy.
To implement the caching mechanism around the method fetchAllBooks(), Spring creates a proxy that will do the caching for you, and only call your method fetchAllBooks() if there is nothing in the cache. All your public methods are in fact proxied, even if no extra behavior is implemented.
public class ProxyOfMyLibrary {
MyLibrary target;
public void createBook(Book Book) {
target.createBook(Book);
}
public Collection<Book> fetchAllBooks() {
}
}
This is of course simplified but the important info is this:
ProxyOfMyLibrary.createBook() will call the real MyLibrary.createBook() that you wrote, and your method calls MyLibrary.fetchAllBooks(), not ProxyOfMyLibrary.fetchAllBooks() in which the caching mechanism is.
I often see this with @Cacheable but keep in mind that this would apply to any other annotation that adds behavior to your code like @Transactional,@RolesAllowed or @Secured, to name a few.
How to fix this?
The simple solution would be to put the annotation @Cacheable on another bean, the BookRepository in my example.
Another solution is to inject the bean into itself: you will get a pointer to the proxy, on which you call the method.
@Service
public class MyLibrary implements ApplicationContextAware {
MyLibrary self;
BookRepository bookRepository;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.self = applicationContext.getBean(MyLibrary.class);
}
public void createBook(Book Book) {
Collection<Book> existingCollection = self.fetchAllBooks();
}
@Cacheable("books")
public Collection<Book> fetchAllBooks() {
return bookRepository.fetchAllBooks();
}
}
However, I would advise against doing this if you can avoid it, as it clutters your code and is a smell of a class trying to do too much.
I hope this can explain why, sometimes, Spring's magic doesn't seem to work.
Let me know if this helped you avoid a headache!