SpringFramework で特定の Bean のみ再読み込みする
以下のような Service クラスがあるとします。Dao へのアクセスは大変コストがかかるため、Bean の初期化時に afterPropertiesSet メソッド内で1度だけアクセスし、その結果を Map に保持するような仕掛けになっています。
@Service public class FooService implements InitializingBean { @Autowired private BarDao dao; private Map<Integer, Bar> resultCache; public void afterPropertiesSet() { List<Bar> records = dao.findAll(); resultCacheMap = new HashMap<String, Bar>(); for (Bar record : records) { resultCache.put(record.getId(), record); } } // ... 通常の実装コード ... }
この Service クラスのキャッシュを再読み込みするにはどうするか、というのが事の発端です。素朴なのは、キャッシュ構築を独立したメソッドとして定義し、それを再度呼び出すという手法です。
@Service public class FooService implements InitializingBean { @Autowired private BarDao dao; private Map<Integer, Bar> resultCache; public void afterPropertiesSet() { initializeCache(); } public synchronized void initializeCache() { List<Bar> records = dao.findAll(); resultCacheMap = new HashMap<String, Bar>(); for (Bar record : records) { resultCache.put(record.getId(), record); } } // ... 通常の実装コード ... }
他のアプローチはないでしょうか? Bean を再構築するには、ConfigurableApplicationContext#refresh を使用する方法もあります。
@Service public class ApplicationContextReloader implements ApplicationContextAware { ConfigurableApplicationContext context; public void setApplicationContext(ApplicationContext context) { this.context = (ConfigurableApplicationContext) context; } public void refresh() { context.refresh(); } }
ただ、Bean の数が多かったり、生成コストの大きい Bean が含まれていたりすると context のリフレッシュに時間がかかりそうです。そこで、特定の Bean のみリフレッシュする方法を検討してみます。@Refreshable アノテーションを定義し、このアノテーションがついている Bean のみをリフレッシュするようにします。
/** * 対象の Bean がリフレッシュ可能であることを示します。 * キャッシュを持つような Bean で、アプリケーション側からの * リフレッシュ通知を受け取りたい場合に * @Refreshable アノテーションを付与してください。 */ @Target(ElementType.TYPE) @Documented @Inherited @Retention(RetentionPolicy.RUNTIME) public @interface Refreshable { }
@Service public class SingletonBeanRefresher implements BeanFactoryAware { private static final Log LOG = LogFactory.getLog(SingletonBeanRefresher.class); private DefaultListableBeanFactory beanFactory; public void setBeanFactory(BeanFactory beanFactory) throws BeansException { this.beanFactory = (DefaultListableBeanFactory) beanFactory; } public void refresh() { if (LOG.isInfoEnabled()) { LOG.info("Refresh beans annotated with @" + Refreshable.class.getSimpleName()); } String[] beanDefinitionNames = beanFactory.getBeanDefinitionNames(); ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); for (String beanName : beanDefinitionNames) { BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName); String beanClassName = beanDefinition.getBeanClassName(); Class<?> beanClass = ClassUtils.resolveClassName(beanClassName, contextClassLoader); Refreshable annotation = AnnotationUtils.findAnnotation(beanClass, Refreshable.class); if (annotation != null) { synchronized (beanFactory) { beanFactory.removeBeanDefinition(beanName); beanFactory.registerBeanDefinition(beanName, beanDefinition); beanFactory.getBean(beanName); } } } } }
registerBeanDefinition メソッドを呼び出した時点では Bean のインスタンスが生成されないため、あえて getBean メソッドを呼び出しています。なお、コンテナから Bean 定義を取り出すには ApplicationContext ではダメで、 BeanFactory を用いる必要がある点にも注意してください。