Автоматическая регистрация bean-компонентов в Guava EventBus с помощью Spring IoC

Допустим, у меня есть интерфейс для события смены языка в моем приложении (он основан на Vaadin):

public interface ILanguageChangeListener{
    @Subscribe onLanguageChange(LanguageChangeEvent event);
}

И у меня есть много bean-компонентов, которые реализуют этот интерфейс, аннотированный @Component, поэтому они доступны в Spring IoC. У меня также есть компонент EventBus:

<bean id="languageSwitcher" class="com.google.common.eventbus" scope="session" />

Теперь, после получения экземпляра любого bean-компонента из IoC, я также должен получить экземпляр languageSwitcher и зарегистрировать в нем вновь созданный bean-компонент с помощью:

languageSwitcher.register(myNewBean);

для получения этих событий. Можно ли каким-то образом сообщить IoC, что я хочу вызывать метод register компонента languageSwitcher для каждого нового компонента, реализующего ILanguageChangeListener?


person fracz    schedule 10.12.2012    source источник
comment
Хорошо, почему бы не создать фабричный компонент для вашего EventBus, который получает введенный список ILanguageChangeListeners и просто регистрирует их в цикле... это руководство, но вам не нужно помещать код для регистрации компонента в каждый экземпляр ILanguageChangeListener .   -  person ElderMael    schedule 10.12.2012
comment
Потому что у меня нет всех объектов, которые должны слушать это событие в начале — см. мой комментарий к ответу @mael.   -  person fracz    schedule 10.12.2012
comment
Затем, возможно, BeanPostProcessor к вашим реализациям ILanguageChangeListener. Я так понимаю, это прототип?   -  person ElderMael    schedule 10.12.2012
comment
О да, я думаю, это то, что я ищу. Позвольте мне попробовать реализовать это, и я вернусь с результатом.   -  person fracz    schedule 10.12.2012
comment
Я тоже попробую, но, конечно, мои ответы развенчиваются D:   -  person ElderMael    schedule 10.12.2012
comment
О боже, наконец-то получилось. Ржу не могу.   -  person ElderMael    schedule 11.12.2012


Ответы (4)


ОК, используя BeanPostProcessor, зарегистрируйте каждый bean-компонент вашего интерфейса:

public class EventBusRegisterBeanPostProcessor implements BeanPostProcessor,
        ApplicationContextAware {

    private ApplicationContext context;

    @Autowired
    private EventBus eventBus; // The only event bus i assume...

    public Object postProcessBeforeInitialization(Object bean, String beanName)
            throws BeansException {

        return bean;
    }

    public Object postProcessAfterInitialization(Object bean, String beanName)
            throws BeansException {

        if (bean instanceof ILanguageChangeListener) {
            registerToEventBus(bean);
        }

        return bean;
    }

    private void registerToEventBus(Object bean) {
        this.eventBus.register(bean);
    }

    public void setApplicationContext(ApplicationContext applicationContext)
            throws BeansException {
        this.context = applicationContext;
    }

}

Обратите внимание, что если у вас много компонентов EventBus, вы должны использовать ApplicationContext.getBean(String) для получения нужного EventBus.

Я цитирую из javadoc:

В случае FactoryBean этот обратный вызов будет вызываться как для экземпляра FactoryBean, так и для объектов, созданных FactoryBean (начиная с Spring 2.0). Постпроцессор может решить, применять ли его либо к FactoryBean, либо к созданным объектам, либо и к тому, и к другому через соответствующие проверки FactoryBean instanceof.

person ElderMael    schedule 10.12.2012
comment
Большое спасибо, работает как шарм. Также обратите внимание, что при использовании EventBus для этой цели вы должны знать, что Guava не использует слабые ссылки на слушателей, поэтому они не будут собирать мусор, пока они не зарегистрированы. Это немного сложно, когда дело доходит до масштаба прототипа Spring. Проблема описана здесь code.google.com/p/guava- library/issues/detail?id=807 и в моем случае я представил решение из комментария №14. - person fracz; 11.12.2012
comment
Довольно интересно, у вас есть репо или что-то с вашей реализацией? - person ElderMael; 11.12.2012
comment
У меня нет публичного репозитория для этого проекта, но то, что я реализовал на основе вашего ответа, выглядит так: pastebin. com/tX55ih4C - person fracz; 11.12.2012
comment
Ок, спасибо, попробую сделать универсальную версию своих утилит. - person ElderMael; 11.12.2012

ИМО, даже лучше (меньше связи) вместо реализации интерфейса маркера использовать аннотацию уровня класса для маркировки bean-компонентов, которые должны быть зарегистрированы. Вот модифицированный код постпроцессора бина:

public class EventBusListenersRegistererBeanPostProcessor implements BeanPostProcessor{

    Logger log = LoggerFactory.getLogger(this.getClass());

    @Inject
    private EventBus bus;

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if(bean.getClass().isAnnotationPresent(RegisterWithEventBus.class)){
            log.info("Event Bus is registering bean named \"{}\" of class {}.", beanName, bean.getClass().getCanonicalName());
            bus.register(bean);
        }

        return bean;
    }
}

И аннотация:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Inherited // important when working with dynamically generated proxies i.e. CGLib
public @interface RegisterWithEventBus {}

Обратите внимание, что интерфейс аннотаций имеет метааннотацию @Inherited. Это необходимо в приложении Spring, которое использует прокси-серверы CGLIB, поскольку аннотация будет относиться не к фактическому (динамическому) классу объекта, а к родительскому классу.

person Pierre Henry    schedule 20.03.2016

Используйте фабричный компонент для шины событий и введите список всех компонентов ILanguageChangeListener в вашем контексте.

public class EventBusFactoryBean implements FactoryBean<EventBus> {

    @Autowired
    private List<ILanguageChangeListener> languageChangeListeners;

    private EventBus instance;

    @PostConstruct
    public void init() {

        this.instance = new EventBus();

        for (ILanguageChangeListener listener : this.languageChangeListeners) {
            this.instance.register(listener);
        }
    }

    public EventBusFactoryBean() {

    }

    public EventBus getObject() throws Exception {
        return this.instance;
    }

    public Class<?> getObjectType() {
        return EventBus.class;
    }

    public boolean isSingleton() {
        return true;
    }

    public List<ILanguageChangeListener> getLanguageChangeListeners() {
        return languageChangeListeners;
    }

    public void setLanguageChangeListeners(
            List<ILanguageChangeListener> languageChangeListeners) {
        this.languageChangeListeners = languageChangeListeners;
    }

}

А затем определите свой компонент в файле определения Spring Bean или аннотируйте его с помощью @Component

person ElderMael    schedule 10.12.2012
comment
Но вы предполагаете, что в жизни приложения есть момент, когда я знаю все экземпляры классов, реализующих ILanguageChangeListener. Но это не так - например, каждое представление в моем приложении реализует его, но не все они создаются во время запуска приложения. Они создаются (извлекаются из IoC), когда пользователь просматривает и открывает новые страницы (представления). Итак, что я ищу, так это каким-то образом сообщить IoC, что, прежде чем он введет какой-либо из ILanguageChangeListener, он должен быть зарегистрирован в EventBus. Надеюсь, теперь я ясно выразился. - person fracz; 10.12.2012

Одним из способов может быть использование интерфейса InitializingBean и внедрить переключатель языка в ваш bean-компонент. Что-то вроде этого:

public class NotVeryUsefulLanguageListener implements ILanguageChangeListener,
        InitializingBean {

    @Autowired
    private EventBus languageSwitcher;

    public void afterPropertiesSet() throws Exception {

        this.languageSwitcher.register(this);
    }

    //... getters, setters, etc

}

Если NotVeryUsefulLanguageLisetener является синглтоном, это произойдет только один раз... если прототип, то каждый раз, когда вы получаете экземпляр из Spring

Если вы используете XML, вы можете использовать что-то вроде этого:

<bean id="languageListenerA" init-method="afterPropertiesSet" class="org.company.whatever.NotVeryUsefulLanguageListener">
        <!-- stuff -->
    </bean>
person ElderMael    schedule 10.12.2012
comment
Я могу использовать @PostConstruct, чтобы получить тот же эффект. Это далеко не автоматизировано, так как в каждой реализации мне приходится вызывать register() вручную. - person fracz; 10.12.2012
comment
Итак, вы хотите, чтобы они были зарегистрированы один раз экземпляром этого компонента после их загрузки Spring? - person ElderMael; 10.12.2012