Статьи

JSF Событийное общение: подход новой школы

В последнем посте мы изучали основанное на событиях взаимодействие на основе шаблонов Observer / Event Listener и Mediator. В связи с их недостатками я хотел бы показать более эффективные способы общения на основе событий. Мы начнем с Google Guava EventBus и закончим CDI (контексты и внедрение зависимостей для платформы Java EE).

Гуава EventBus

В библиотеке Google Guava есть полезный пакет eventbus . Класс EventBus позволяет осуществлять связь между компонентами в стиле публикации-подписки, не требуя, чтобы компоненты явно регистрировались друг с другом. Поскольку мы разрабатываем веб-приложения, мы должны инкапсулировать экземпляр этого класса в bean-объект.

Давайте напишем бин EventBusProvider.

01
02
03
04
05
06
07
08
09
10
11
12
13
public class EventBusProvider implements Serializable {
 
    private EventBus eventBus = new EventBus("scopedEventBus");
 
    public static EventBus getEventBus() {
        // access EventBusProvider bean
        ELContext elContext = FacesContext.getCurrentInstance().getELContext();
        EventBusProvider eventBusProvider =
            (EventBusProvider) elContext.getELResolver().getValue(elContext, null, "eventBusProvider");
 
        return eventBusProvider.eventBus;
    }
}

Я хотел бы продемонстрировать все основные функции Guava EventBus только в одном примере. Давайте напишем следующую иерархию событий:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
public class SettingsChangeEvent {
 
}
 
public class LocaleChangeEvent extends SettingsChangeEvent {
 
    public LocaleChangeEvent(Object newLocale) {
        ...
    }
}
 
public class TimeZoneChangeEvent extends SettingsChangeEvent {
 
    public TimeZoneChangeEvent(Object newTimeZone) {
        ...
    }
}
Следующие шаги просты. Чтобы получать события, объект (бин) должен предоставлять открытый метод, аннотированный аннотацией @Subscribe, который принимает один аргумент с желаемым типом события. Объект должен передать себя в метод register () экземпляра EventBus. Давайте создадим два компонента:
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
public MyBean1 implements Serializable {
 
    @PostConstruct
    public void initialize() throws Exception {
        EventBusProvider.getEventBus().register(this);
    }
 
    @Subscribe
    public void handleLocaleChange(LocaleChangeEvent event) {
        // do something
    }
 
    @Subscribe
    public void handleTimeZoneChange(TimeZoneChangeEvent event) {
        // do something
    }
}
 
public MyBean2 implements Serializable {
 
    @PostConstruct
    public void initialize() throws Exception {
        EventBusProvider.getEventBus().register(this);
    }
 
    @Subscribe
    public void handleSettingsChange(SettingsChangeEvent event) {
        // do something
    }
}

Чтобы опубликовать событие, просто предоставьте объект события методу post () экземпляра EventBus. Экземпляр EventBus определит тип события и направит его всем зарегистрированным слушателям.

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
public class UserSettingsForm implements Serializable {
 
    private boolean changed;
 
    public void localeChangeListener(ValueChangeEvent e) {
        changed = true;       
  
        // notify subscribers
        EventBusProvider.getEventBus().post(new LocaleChangeEvent(e.getNewValue()));
    }
 
    public void timeZoneChangeListener(ValueChangeEvent e) {
        changed = true;       
  
        // notify subscribers
        EventBusProvider.getEventBus().post(new TimeZoneChangeEvent(e.getNewValue()));
    }
 
    public String saveUserSettings() {
        ...
 
        if (changed) {
            // notify subscribers
            EventBusProvider.getEventBus().post(new SettingsChangeEvent());
 
            return "home";
        }
    }
}
Guava EventBus позволяет создавать любого слушателя, который реагирует на множество различных событий — просто аннотируйте множество методов с помощью @Subscribe, и все. Слушатели могут использовать существующую иерархию событий. Поэтому, если прослушиватель A ожидает события A, а событие A имеет подкласс с именем B, этот прослушиватель будет получать события обоих типов: A и B. В нашем примере мы опубликовали три события: SettingsChangeEvent, LocaleChangeEvent и TimeZoneChangeEvent. Метод handleLocaleChange () в MyBean1 будет получать только LocaleChangeEvent. Метод handleTimeZoneChange () будет получать только TimeZoneChangeEvent. Но посмотрите на метод handleSettingsChange () в MyBean2. Он получит все три события!
Как вы можете видеть, регистрация вручную по-прежнему необходима (EventBusProvider.getEventBus (). Register (this)), и проблема с bean-объектами с ограничениями, о которых я упоминал в предыдущем посте , все еще существует. Мы должны знать о области видимости EventBusProvider и области действия бобов публикации / подписчика. Но, как вы также можете видеть, у нас есть некоторые улучшения по сравнению с шаблоном Mediator: никаких специальных интерфейсов не требуется, имена методов подписчика не определены фиксированно, возможны также множественные прослушиватели, никаких усилий по управлению зарегистрированными экземплярами и т. Д. Последнее но не в последнюю очередь — асинхронный AsyncEventBus и подписка на DeadEvent (для прослушивания любых событий, отправленных без прослушивателей — удобно для отладки). Следуйте этому руководству, чтобы преобразовать существующую систему на основе EventListener в EventBus.
CDI (контексты и внедрение зависимостей)
Каждый совместимый с JEE 6 сервер приложений поддерживает CDI (спецификация JSR-299). Он определяет набор дополнительных услуг, которые помогают улучшить структуру кода приложения. Наиболее известными реализациями CDI являются OpenWebBeans и JBoss Weld . События в CDI позволяют бинам взаимодействовать вообще без зависимости. Производители событий генерируют события, которые доставляются наблюдателям событий контейнером. Эта базовая схема может звучать как знакомый шаблон Observer / Observable, но есть несколько преимуществ.
  • Производители событий и наблюдатели событий отделены друг от друга.
  • Наблюдатели могут указать комбинацию «селекторов», чтобы сузить набор уведомлений о событиях, которые они будут получать.
  • Наблюдатели могут быть уведомлены немедленно или с задержкой до конца текущей транзакции.
  • Отсутствие головной боли при определении объема с помощью методов условного наблюдателя (помните проблему с областями действия и Mediator / EventBus?)
Методы условного наблюдателя позволяют получить уже существующий экземпляр компонента, только если область действия компонента, который объявляет метод наблюдателя, в настоящее время активна, без создания нового экземпляра компонента. Если метод наблюдателя не является условным, соответствующий компонент всегда будет создан. Вы гибкий!
Механизм событий CDI, по моему мнению, является наилучшим подходом для событийного общения. Тема сложная. Давайте покажем только основные функции. Метод наблюдателя — это метод bean-компонента с аннотированным параметром @Observed.
1
2
3
4
5
6
public MyBean implements Serializable {
 
    public void onLocaleChangeEvent(@Observes Locale locale) {
        ...
    }
}

Параметр события может также указывать квалификаторы, если метод наблюдателя заинтересован только в квалифицированных событиях — это события, которые имеют эти квалификаторы.

1
2
3
public void onLocaleChangeEvent(@Observes @Updated Locale locale) {
    ...
}

Спецификатор события — это обычный классификатор, определенный с помощью @Qualifier. Вот пример:

1
2
3
4
@Qualifier
@Target({FIELD, PARAMETER})
@Retention(RUNTIME)
public @interface Updated {}

Производители событий запускают события, используя экземпляр параметризованного интерфейса Event. Экземпляр этого интерфейса получается путем инъекции. Производитель вызывает события, вызывая метод fire () интерфейса Event, передавая объект события.

1
2
3
4
5
6
7
8
9
public class UserSettingsForm implements Serializable {
 
    @Inject @Any Event<Locale> localeEvent;
 
    public void localeChangeListener(ValueChangeEvent e) {
        // notify all observers
        localeEvent.fire((Locale)e.getNewValue());
    }
}
Контейнер вызывает все методы наблюдателя, передавая объект события в качестве значения параметра события. Если какой-либо метод-наблюдатель генерирует исключение, контейнер перестает вызывать методы-наблюдатели, и исключение повторно вызывается методом fire (). @ Любая аннотация выше действует как псевдоним для всех и всех классификаторов. Видите ли, регистрация наблюдателей вручную не требуется. Легко? Задать другие квалификаторы в точке внедрения также просто:
1
2
// this will raise events to observers having parameter @Observes @Updated Locale
@Inject @Updated Event<Locale> localeEvent;
Вы также можете иметь несколько классификаторов событий. Событие доставляется каждому методу-наблюдателю, у которого есть параметр события, которому может быть назначен объект события, и не имеет никакого квалификатора события, кроме описателей события, соответствующих тем, которые указаны в точке внедрения события. Метод наблюдателя может иметь дополнительные параметры, которые являются точками ввода. Пример:
1
2
3
public void onLocaleChangeEvent(@Observes @Updated Locale locale, User user) {
    ...
}

Как насчет динамического определения спецификатора? CDI позволяет получить правильный экземпляр квалификатора с помощью AnnotationLiteral. Таким образом, мы можем передать классификатор методу select () класса Event. Пример:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
public class DocumentController implements Serializable {
 
    Document document;
 
    @Inject @Updated @Deleted Event<Document> documentEvent;
 
    public void updateDocument() {
        ...
        // notify observers with @Updated annotation
        documentEvent.select(new AnnotationLiteral<Updated>(){}).fire(document);
    }
 
    public void deleteDocument() {
        ...
        // notify observers with @Deleted annotation
        documentEvent.select(new AnnotationLiteral<Deleted>(){}).fire(document);
    }
}
Давайте поговорим о «методах условного наблюдателя». По умолчанию, если в текущем контексте нет экземпляра наблюдателя, контейнер создает экземпляр наблюдателя, чтобы доставить ему событие. Такое поведение не всегда желательно. Мы можем захотеть доставлять события только тем экземплярам наблюдателя, которые уже существуют в текущем контексте. Условный наблюдатель задается добавлением receive = IF_EXISTS к аннотации @Observe.
1
2
3
public void onLocaleChangeEvent(@Observes(receive = IF_EXISTS) @Updated Locale locale) {
    ...
}
Узнайте больше о Scopes и Contexts здесь . В этом коротком посте мы не можем больше говорить о дополнительных функциях, таких как «квалификаторы событий с участниками» и «наблюдатели транзакций». Я хотел бы призвать всех начать изучать CDI. Веселитесь!