Spring common mistake : bypassing the proxy features
Or "why does the Spring magic stopped working?"
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() {
// Do some resources intensive request here
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) {
// Let's pretend we need the collection of existing books :
Collection<Book> existingCollection = this.fetchAllBooks();
// do something with the new book and the collection
}
@Cacheable("books")
public Collection<Book> fetchAllBooks() {
// Do some resources intensive request here
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 {
// The class name would be 'MyLibrary$$SpringCGLIB$$0' in reality
// A reference to your original MyLibrary class
MyLibrary target;
public void createBook(Book Book) {
// Your code is called here
target.createBook(Book);
}
public Collection<Book> fetchAllBooks() {
// fetch an existing value in cache
// if found, return it
// else, call target.fetchAllBooks() (i.e. your code)
// then store the result in cache
// and return it
}
}
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) {
// Note that we don't use 'this' but 'self' here
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!