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 を用いる必要がある点にも注意してください。