Spring @Configuration and FactoryBean
01 02 03 04 05 06 07 08 09 10 11 12 13 14 | < cache:annotation-driven /> < context:component-scan base-package = 'org.bk.samples.cachexml' ></ context:component-scan > < bean id = 'cacheManager' class = 'org.springframework.cache.support.SimpleCacheManager' > < property name = 'caches' > < set > < ref bean = 'defaultCache' /> </ set > </ property > </ bean > < bean name = 'defaultCache' class = 'org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean' > < property name = 'name' value = 'default' /> </ bean > |
The factory bean ConcurrentMapCacheFactoryBean is a bean which is in turn responsible for creating a Cache bean.
My first attempt at translating this setup to a @Configuration style was the following:
01 02 03 04 05 06 07 08 09 10 | @Bean public SimpleCacheManager cacheManager(){ SimpleCacheManager cacheManager = new SimpleCacheManager(); List<Cache> caches = new ArrayList<Cache>(); ConcurrentMapCacheFactoryBean cacheFactoryBean = new ConcurrentMapCacheFactoryBean(); cacheFactoryBean.setName( 'default' ); caches.add(cacheFactoryBean.getObject()); cacheManager.setCaches(caches ); return cacheManager; } |
This did not work however, the reason is that here I have bypassed some Spring bean lifecycle mechanisms altogether. It turns out that ConcurrentMapCacheFactoryBean also implements the InitializingBean interface and does a eager initialization of the cache in the ‘afterPropertiesSet’ method of InitializingBean. Now by directly calling factoryBean.getObject() , I was completely bypassing the afterPropertiesSet method.
There are two possible solutions:
1. Define the FactoryBean the same way it is defined in the XML:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 | @Bean public SimpleCacheManager cacheManager(){ SimpleCacheManager cacheManager = new SimpleCacheManager(); List<Cache> caches = new ArrayList<Cache>(); caches.add(cacheBean().getObject()); cacheManager.setCaches(caches ); return cacheManager; } @Bean public ConcurrentMapCacheFactoryBean cacheBean(){ ConcurrentMapCacheFactoryBean cacheFactoryBean = new ConcurrentMapCacheFactoryBean(); cacheFactoryBean.setName( 'default' ); return cacheFactoryBean; } |
In this case, there is an explicit FactoryBean being returned from a @Bean method, and Spring will take care of calling the lifecycle methods on this bean.
2. Replicate the behavior in the relevant lifecycle methods, in this specific instance I know that the FactoryBean instantiates the ConcurrentMapCache in the afterPropertiesSet method, I can replicate this behavior directly this way:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 | @Bean public SimpleCacheManager cacheManager(){ SimpleCacheManager cacheManager = new SimpleCacheManager(); List<Cache> caches = new ArrayList<Cache>(); caches.add(cacheBean()); cacheManager.setCaches(caches ); return cacheManager; } @Bean public Cache cacheBean(){ Cache cache = new ConcurrentMapCache( 'default' ); return cache; } |
Something to keep in mind when translating a FactoryBean from xml to @Configuration.
Note:
A working one page test as a gist is available here:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | package org.bk.samples.cache; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import java.util.ArrayList; import java.util.List; import java.util.Random; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.Cache; import org.springframework.cache.annotation.Cacheable; import org.springframework.cache.annotation.EnableCaching; import org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean; import org.springframework.cache.support.SimpleCacheManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.stereotype.Component; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; @RunWith (SpringJUnit4ClassRunner. class ) @ContextConfiguration (classes={TestSpringCache.TestConfiguration. class }) public class TestSpringCache { @Autowired TestService testService; @Test public void testCache() { String response1 = testService.cachedMethod( 'param1' , 'param2' ); String response2 = testService.cachedMethod( 'param1' , 'param2' ); assertThat(response2, equalTo(response1)); } @Configuration @EnableCaching @ComponentScan ( 'org.bk.samples.cache' ) public static class TestConfiguration{ @Bean public SimpleCacheManager cacheManager(){ SimpleCacheManager cacheManager = new SimpleCacheManager(); List<Cache> caches = new ArrayList<Cache>(); caches.add(cacheBean().getObject()); cacheManager.setCaches(caches ); return cacheManager; } @Bean public ConcurrentMapCacheFactoryBean cacheBean(){ ConcurrentMapCacheFactoryBean cacheFactoryBean = new ConcurrentMapCacheFactoryBean(); cacheFactoryBean.setName( 'default' ); return cacheFactoryBean; } } } interface TestService{ String cachedMethod(String param1,String param2); } @Component class TestServiceImpl implements TestService{ @Cacheable (value= 'default' , key= '#p0.concat(' - ').concat(#p1)' ) public String cachedMethod(String param1, String param2){ return 'response ' + new Random().nextInt(); } } |
Reference: Spring @Configuration and FactoryBean from our JCG partner Biju Kunjummen at the all and sundry blog.