Spring common mistake : bypassing the proxy features

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!