While trying to add caching to a bean in my client’s project, I ran into some strange behaviour. The project already used caching on several components and used the @Cacheable
annotation with an EhCache backing store. I added a cache definition to the ehcache.xml
file and put the @Cacheable
annotation on the method, recompiled, tested… No caching.
Update 2015-01-26: I added a little trick to mitigate this situation if you run into it. See below for the fix.
Confused, I cleaned, recompiled again, restarted, tested. Still no caching. My first instinct was: maybe caching is improperly configured and the other beans also have no active caching, even though we think they do. So I started debugging, to see if the calls to the other beans did use caching. To my surprise, they did. Refreshing the page did not cause those methods to be called again, but mine were.
Some further debugging showed that the SpringCacheAnnotationParser
did process my bean and was able to see the @Cacheable
annotation. However, when debugging the method that was supposed to be cached, I noticed there were no references to any CacheInterceptor
instances in the stack trace. Even though during startup my bean was replaced by a generated proxy.
This raised a suspicion that maybe I was looking at two instances of my bean. A breakpoint on my bean’s constructor revealed that indeed, it was called twice. The cause became pretty obvious when I examined the Spring configuration files:
applicationContext.xml
<beans ...>
...
<context:component-scan base-package="com.our.app"/>
...
</beans>
servlet-context.xml
<beans ...>
...
<context:component-scan base-package="com.our.app"/>
...
</beans>
Aha! It appears our Spring context is loading all classes annotated with @Component
, @Service
, etc. twice! Once on the startup of the application and again when the first request comes in for the dispatcher servlet. Now, the reason that caching worked on some, but not all beans, lied in the fact that caching was configured in the applicationContext.xml
file, which applied caching wrappers to all beans in that context. There was no caching configured in servlet-context.xml
, causing beans loaded there to not have caching wrappers.
Our Java EE Servlet Filters were loaded from the application-context.xml
, which contained caching. Those were fine. But any requests coming from the dispatcher servlet would reach beans in the servlet-context.xml
context, which did not have caching.
The solution was quite a lot of work, since we had to re-evaluate all annotated classes and decide whether they should live in the application context, or the dispatcher servlet context. The rule of thumb we used here was:
Classes annotated with @Component
and @Service
will be loaded in the application context. Classes annotated with @Controller
and its derivatives will be loaded in the dispatcher servlet context.
I realize there may be more subtle cases where this rule does not fully apply, but it’s a good starting point. Generally you want any potentially shared beans in the application context and anything specifically tied to the dispatcher servlet in its context. At least take this advice to heart:
Never, ever use component-scan
on the same package tree from different contexts
Update 2015-01-26: If you have this same problem and are unable to move your controllers to a separate package tree, you can use a little configuration option of Spring’s component scan. You can use include and exclude filters to filter the @Controller
annotations out of the application context and only include them in the servlet context. The snippet below filters out the controllers, you should use this in you application context:
<beans ...>
...
<context:component-scan base-package="com.our.app">
<:context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
...
</beans>
The same with annotations:
@ComponentScan(excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, value = Controller.class)})
@Configuration
public class MyConfiguration {}
Then reverse the filter on the servlet-config and you have separated your stereotypes. Note that I believe this is more of a hack than a sound application architecture, but in legacy applications you may not have much of a choice…
–JH
PS: This post talks about @Cacheable
, but the same behaviour might be triggered by things like @Transaction
and other bean post processing features in a context.