Начинающий
- Статус
- Оффлайн
- Регистрация
- 5 Июн 2025
- Сообщения
- 515
- Реакции
- 26
В этом гайде мы рассмотрим что такое шина событий(Event Bus), рассмотрим примеры использования шин событий, как её можно реализовать.
Раздел 1. Что такое шина событий?
Само по себе понятие "Event Bus" представляет из себя паттерн проектирования, который обеспечивает взаимодействие между слабо-связанными компонентами программы по принципу "публикация-подписка", что позволяет компонентам коммуницировать между собой.
Что-бы было проще понять, можно представить какой-нибудь сервис который рассылает спам на вашу почту, в этом представлении сам сервис будет являться шиной событий, а спам приходящий на десятки сотен электронных почт - событиями.
Раздел 2. Примеры использования шин событий (Google Guava EventBus)
За неплохой пример мы можем взять пример реализации шин событий в Forge API (Minecraft 1.6), где есть 2-3 шины событий, но рассмотрим мы пока только одну конкретную - привязанную к главному классу мода помеченного аннотацией
Для тех кто не знает - небольшое пояснение: Моды Forge API имеют несколько стадий загрузки, и каждая из стадий проходит через отдельную шину событий для запуска модов и выполнения разных операции на разных стадиях.
Давайте теперь попробуем представить что мы разработчик Forge, и мы реализуем главный класс мода. У нас есть задача сделать максимально гибкое API для инициализации модов на разных стадиях. И версия Minecraft 1.6 взята не спроста, такая версия игры работала на Java 1.7, что лишает нас возможности использовать лямбды.
Что можно придумать?
У Forge есть достаточно много различных стадий, большинство из которых редко когда нужны. Поэтому приоритетным стоило бы выбрать третий вариант с шиной событий, что дополнительно облегчало бы под-капотную работу Forge из-за того что не нужно везде таскать референсы на главные классы модов, достаточно вызвать
Раздел 3. Пишем свою шину событий с блек-джеком и thread-safety.
Ну а теперь можно и разгуляться. Поскольку большинство участников раздела Minecraft вряд-ли работало с мульти-поточностью чуть более серьёзно чем запускать один случайный Runnable через ExecutorService, я дополнительно еще попытаюсь объяснить зачем нам
Реализовывать это всё дело мы будем на манер ныне устаревшего Google Guava EventBus. Как мне кажется, именно он послужил своеобразной основой для шин событий в Forge. Что мы попробуем реализовать: Шину событий, события которые нельзя отменить(первое отличие от стандартных Forge событий), регистрацию слушателей событий, отправку событий и механизм который обеспечит thread-safety для внутрянки нашей шины событий, аннотацию для слушателей и поиск слушателей событий внутри объектов.
Начнём с простого - само событие:
Невероятно, правда? Поскольку у нас не будет механизма отмены событий, нам достаточно пустого абстрактного класса, от которого будут наследоваться все реальные события. Этот абстрактный класс так-же мы будем использовать при поиске слушателей событий.
Далее по плану аннотация слушателя событий:
Здесь тоже всё довольно просто, 2 другие аннотации на нашу аннотацию и всё готово.
Далее мы создадим объект который будет представлять делегат события, или если проще - функцию которая будет слушателем события:
Вроде-бы всё необходимое у нас есть, предлагаю теперь приступить к написанию нашей шины событий. Разобьём это всё дело по частям и обсудим методы по отдельности:
По сути примерно так будет выглядеть структура нашей шины событий. Последние 2 метода использовать будем только внутри самой шины для регистрации слушателей, поэтому фактически у нас будет всего 3 метода которыми мы будем пользоваться из-вне. Довольно просто!
Начнём с простого - с публикации события по нашим слушателям. У нас уже есть
Далее - можно взяться за более сложную часть шины событий - регистрацию. Для этого мы используем чуть-чуть рефлексии и
Самая сложная часть позади, осталось дописать наши
Полный код без Lock'ов находится в спойлере:
Раздел 4. Thread-safety и EventBus
Теперь давайте поговорим об использовании шин событий в мульти-поточных ситуациях. Томить не буду, давайте сразу рассмотрим проблемную ситуацию когда нам могут понадобится
Представим что у нас есть 2 потока, и они одномоментно начали использовать шину событий. Один - пытается пройтись по событиям, другой - добавить новый список слушателей с новым слушателем. Это потенциальная ситуация с race-condition'ом где потоки не будут останавливаться и будут использовать данные по принципу "кто успел - тот и съел". Для этого мы можем использовать
Работа с
Таким образом вносим изменения в наш код, и теперь наша шина событий выглядит так:
Заключение
Вот как-то так!
Вызвать мы это всё дело можем вот так:
Для тех кому интересно, я написал
Раздел 1. Что такое шина событий?
Само по себе понятие "Event Bus" представляет из себя паттерн проектирования, который обеспечивает взаимодействие между слабо-связанными компонентами программы по принципу "публикация-подписка", что позволяет компонентам коммуницировать между собой.
Что-бы было проще понять, можно представить какой-нибудь сервис который рассылает спам на вашу почту, в этом представлении сам сервис будет являться шиной событий, а спам приходящий на десятки сотен электронных почт - событиями.
Раздел 2. Примеры использования шин событий (Google Guava EventBus)
За неплохой пример мы можем взять пример реализации шин событий в Forge API (Minecraft 1.6), где есть 2-3 шины событий, но рассмотрим мы пока только одну конкретную - привязанную к главному классу мода помеченного аннотацией
@Mod
.Для тех кто не знает - небольшое пояснение: Моды Forge API имеют несколько стадий загрузки, и каждая из стадий проходит через отдельную шину событий для запуска модов и выполнения разных операции на разных стадиях.
Давайте теперь попробуем представить что мы разработчик Forge, и мы реализуем главный класс мода. У нас есть задача сделать максимально гибкое API для инициализации модов на разных стадиях. И версия Minecraft 1.6 взята не спроста, такая версия игры работала на Java 1.7, что лишает нас возможности использовать лямбды.
Что можно придумать?
- Наследовать главные классы мода от абстрактного класса мода с методами на каждую стадию загрузки (минусы - нельзя менять названия методов, нужно всегда наследоваться от уже существующих методов).
- Написать систему которая искала бы методы самостоятельно через рефлексию (минусы - медленно, больше шансов накосячить).
- Использовать шину событий, дав разработчику максимальную гибкость в том, какие методы использовать а какие игнорировать.
У Forge есть достаточно много различных стадий, большинство из которых редко когда нужны. Поэтому приоритетным стоило бы выбрать третий вариант с шиной событий, что дополнительно облегчало бы под-капотную работу Forge из-за того что не нужно везде таскать референсы на главные классы модов, достаточно вызвать
post(event)
и дело в шляпе. Как раз - пример слабо-связанных компонентов.Раздел 3. Пишем свою шину событий с блек-джеком и thread-safety.
Ну а теперь можно и разгуляться. Поскольку большинство участников раздела Minecraft вряд-ли работало с мульти-поточностью чуть более серьёзно чем запускать один случайный Runnable через ExecutorService, я дополнительно еще попытаюсь объяснить зачем нам
Lock
, как с ним работать, и почему он поможет нам достичь thread-safety (конечно-же условно, т.к. для более-менее реальной thread-safety сами слушатели событий должны быть обёрнуты в synchronized
блоки). Писать кстати мы будем на Java 8, так как реализовать это дело с лямбда-выражениями банально проще.Реализовывать это всё дело мы будем на манер ныне устаревшего Google Guava EventBus. Как мне кажется, именно он послужил своеобразной основой для шин событий в Forge. Что мы попробуем реализовать: Шину событий, события которые нельзя отменить(первое отличие от стандартных Forge событий), регистрацию слушателей событий, отправку событий и механизм который обеспечит thread-safety для внутрянки нашей шины событий, аннотацию для слушателей и поиск слушателей событий внутри объектов.
Начнём с простого - само событие:
Event.java:
public abstract class Event {
// Тут пусто! :o
}
Далее по плану аннотация слушателя событий:
EventListener.java:
// Указываем что аннотацию можно использовать только на методах
@Target(ElementType.METHOD)
// Указываем что аннотация должна существовать в рантайме(после компиляции)
@Retention(RetentionPolicy.RUNTIME)
public @interface EventListener {
}
Далее мы создадим объект который будет представлять делегат события, или если проще - функцию которая будет слушателем события:
EventDelegate.java:
// Добавляем @FunctionalInterface аннотацию, что-бы мы могли использовать
// интерфейс в качестве лямбда-выражения (прим.: "(param) -> { expr }")
@FunctionalInterface
// Используем generic тип <T> который обязательно должен наследоваться от события Event
public interface EventDelegate<T extends Event> {
// Собственно наш метод который будет выступать в роли слушателя и принимать событие.
// Так-же необходимо добавить "throws Throwable", т.к. мы будем использовать MethodHandle#invoke в лямбде
void call(T event) throws Throwable;
}
Вроде-бы всё необходимое у нас есть, предлагаю теперь приступить к написанию нашей шины событий. Разобьём это всё дело по частям и обсудим методы по отдельности:
EventBus.java:
public class EventBus {
// В этой Map'е мы будем хранить всех слушателей которые мы зарегистрировали.
// Хранятся они в этой Map'е по логике Map<Тип события, Слушатели>.
// Обратим внимание что EventDelegate нетипизирован, подобное может
// вызвать вопросы у компилятора и он начнёт жаловаться на это.
// В нашем случае это необходимость, да-бы возможно было заносить в список
// объекты EventDelegate с разными типами в качестве generic значений.
protected Map<Class<? extends Event>, List<EventDelegate>> registry;
public EventBus() {
this.registry = new HashMap<>();
}
public void register(Object eventHandler) {
...
}
public void register(Class<?> eventHandler) {
...
}
public <T extends Event> T post(T event) {
...
}
protected void registerAnnotated(Class<?> type, Object instance) {
...
}
protected static boolean checkIsMethodValid(Method method, Object instance) {
...
}
}
Начнём с простого - с публикации события по нашим слушателям. У нас уже есть
Map<...> registry
который хранит всех наших слушателей, поэтому нам останется всего-лишь достать список со слушателями событий определенного типа, и пройтись по всем им и вызвать их:
EventBus.java (EventBus#post(Event)):
// Обратим внимание что у нас есть локальный generic тип который наследуется от Event.
// Это событие которое мы передаём внутрь, и событие того-же типа мы должны будем вернуть.
// Дабы не обезличивать возвращаемый тип - мы и используем generic.
public <T extends Event> T post(T event) {
// Проверяем что объект события - не null
Objects.requireNonNull(event, "event cannot be null.");
// Получаем список всех слушателей события по типу(классу) этого события
final List<EventDelegate> list = this.registry.get(event.getClass());
if (list == null) {
// Если у нас нету списка слушателей - значит нету и самих слушателей.
// В таком случае выходим из метода заранее и возвращаем объект события.
return event;
}
// Вызываем слушателей
for (EventDelegate delegate : list) {
// Оборачиваем в try-catch блок из-за выбрасываемого Throwable делегатом!
try {
delegate.call(event);
} catch (Throwable t) {
// Возникла ошибка! :(
throw new RuntimeException(t);
}
}
// Возвращаем объект события по завершению.
return event;
}
Далее - можно взяться за более сложную часть шины событий - регистрацию. Для этого мы используем чуть-чуть рефлексии и
MethodHandle.Lookup
.
EventBus.java (EventBus#registerAnnotated(...) и EventBus#checkIsMethodValid(...)):
protected void registerAnnotated(Class<?> type, Object instance) {
// Проверяем что класс где будут события - не null
Objects.requireNonNull(type, "type cannot be null.");
// Если мы не предоставили instance, значит нужно регистрировать статические методы
boolean registerStaticMethods = instance == null;
// Создаём Lookup которым будем получать MethodHandle'ы для EventDelegate
final Lookup lookup = MethodHandles.publicLookup();
// Проверяем что Lookup имеет доступ к нужному классу
try {
if (lookup.accessClass(type) == null) {
throw new RuntimeException("Lookup не имеет доступа к классу " + type.getSimpleName());
}
} catch (IllegalAccessException exception) {
// Ошибка доступа :(
throw new RuntimeException(exception);
}
// Ищем методы которые подходят под критерии слушателей и регистрируем их
try {
// Прогоняемся по всем методам нашего класса/объекта
for (Method method : type.getDeclaredMethods()) {
// Используем еще один try-catch блок внутри, для каждого метода по отдельности
try {
// Если метод - статический, но мы не регистрируем статические методы,
// то в таком случае мы просто пропускаем метод и берём следующий.
boolean isStatic = Modifier.isStatic(method.getModifiers());
if (isStatic != registerStaticMethods) {
continue;
}
// Проверяем подпадает ли под нужные критерии наш метод и имеет ли он аннотацию
if (!checkIsMethodValid(method, instance)) { // instance может быть null для статических методов
// Проверка не прошла, пропускаем метод
continue;
}
// Все проверки пройдены, регистрируем слушателя!
final Class eventType = method.getParameterTypes()[0]; // Получаем тип эвента
final MethodHandle handle = lookup.unreflect(method); // Получаем MethodHandle для EventDelegate
// Теперь получаем список делегатов что-бы туда добавить наш новый делегат.
// Стоит обратить внимание, что список может быть null, в таком случае нужно
// будет нам создать новый список, и внести его в registry.
List<EventDelegate> delegates = this.registry.get(eventType);
if (delegates == null) {
delegates = new ArrayList<>();
this.registry.put(eventType, delegates);
}
// Добавляем наш EventDelegate в список делегатов.
// MethodHandle#invoke требует первым аргументом инстанс при виртуальных вызовах,
// поэтому используя тернарный оператор разделяем создание статических и виртуальных делегатов
delegates.add(isStatic ? handle::invoke : (e) -> handle.invoke(instance, e));
} catch (IllegalAccessException exception) {
// Ошибка доступа :(
throw new RuntimeException(exception);
}
}
} catch (SecurityException exception) {
// Если getDeclaredMethods() выкинет ошибку - мы её перевыкидываем
throw new RuntimeException(exception);
}
}
protected static boolean checkIsMethodValid(Method method, Object instance) {
// Проверяем что наш метод - не null
Objects.requireNonNull(method, "method cannot be null.");
// Получаем объект аннотации через рефлексию. Если аннотации над
// методом нет - то getAnnotation(...) вернёт null
final EventListener annotation = method.getAnnotation(EventListener.class);
if (annotation != null && // Смотрим что-бы аннотация была над методом
method.canAccess(instance) && // Проверяем что метод доступен
method.getReturnType() == Void.TYPE && // Проверяем что возвращаемый тип это void
method.getParameterCount() == 1 && // Смотрим что-бы кол-во параметров было 1 (собственно наш единственный Event)
// Важная проверка на то, что наш единственный параметр действительно наследуется от Event
Event.class.isAssignableFrom(method.getParameterTypes()[0])
) {
// Возвращаем true если все проверки прошли
return true;
}
// Возвращаем false если проверки провалились
return false;
}
Самая сложная часть позади, осталось дописать наши
register(...)
методы!
EventBus.java (EventBus#register(...)):
// Регистрация объекта со слушателями событий
public void register(Object eventHandler) {
// Проверяем что eventHandler - не null
Objects.requireNonNull(eventHandler, "eventHandler cannot be null.");
registerAnnotated(eventHandler.getClass(), eventHandler);
}
// Регистрация класса со статичными слушателями событий
public void register(Class<?> eventHandler) {
// Проверяем что eventHandler - не null
Objects.requireNonNull(eventHandler, "eventHandler cannot be null.");
registerAnnotated(eventHandler, null);
}
Полный код без Lock'ов находится в спойлере:
EventBus.java:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodHandles.Lookup;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
public class EventBus {
// В этой Map'е мы будем хранить всех слушателей которые мы зарегистрировали.
// Хранятся они в этой Map'е по логике Map<Тип события, Слушатели>.
// Обратим внимание что EventDelegate нетипизирован, подобное может
// вызвать вопросы у компилятора и он начнёт жаловаться на это.
// В нашем случае это необходимость, да-бы возможно было заносить в список
// объекты EventDelegate с разными типами в качестве generic значений.
protected Map<Class<? extends Event>, List<EventDelegate>> registry;
public EventBus() {
this.registry = new HashMap<>();
}
public void register(Object eventHandler) {
// Проверяем что eventHandler - не null
Objects.requireNonNull(eventHandler, "eventHandler cannot be null.");
registerAnnotated(eventHandler.getClass(), eventHandler);
}
public void register(Class<?> eventHandler) {
// Проверяем что eventHandler - не null
Objects.requireNonNull(eventHandler, "eventHandler cannot be null.");
registerAnnotated(eventHandler, null);
}
// Обратим внимание что у нас есть локальный generic тип который наследуется от Event.
// Это событие которое мы передаём внутрь, и событие того-же типа мы должны будем вернуть.
// Дабы не обезличивать возвращаемый тип - мы и используем generic.
public <T extends Event> T post(T event) {
// Проверяем что объект события - не null
Objects.requireNonNull(event, "event cannot be null.");
// Получаем список всех слушателей события по типу(классу) этого события
final List<EventDelegate> list = this.registry.get(event.getClass());
if (list == null) {
// Если у нас нету списка слушателей - значит нету и самих слушателей.
// В таком случае выходим из метода заранее и возвращаем объект события.
return event;
}
// Вызываем слушателей
for (EventDelegate delegate : list) {
// Оборачиваем в try-catch блок из-за выбрасываемого Throwable делегатом!
try {
delegate.call(event);
} catch (Throwable t) {
// Возникла ошибка! :(
throw new RuntimeException(t);
}
}
// Возвращаем объект события по завершению.
return event;
}
protected void registerAnnotated(Class<?> type, Object instance) {
// Проверяем что класс где будут события - не null
Objects.requireNonNull(type, "type cannot be null.");
// Если мы не предоставили instance, значит нужно регистрировать статические методы
boolean registerStaticMethods = instance == null;
// Создаём Lookup которым будем получать MethodHandle'ы для EventDelegate
final Lookup lookup = MethodHandles.publicLookup();
// Проверяем что Lookup имеет доступ к нужному классу
try {
if (lookup.accessClass(type) == null) {
throw new RuntimeException("Lookup не имеет доступа к классу " + type.getSimpleName());
}
} catch (IllegalAccessException exception) {
// Ошибка доступа :(
throw new RuntimeException(exception);
}
// Ищем методы которые подходят под критерии слушателей и регистрируем их
try {
// Прогоняемся по всем методам нашего класса/объекта
for (Method method : type.getDeclaredMethods()) {
// Используем еще один try-catch блок внутри, для каждого метода по отдельности
try {
// Если метод - статический, но мы не регистрируем статические методы,
// то в таком случае мы просто пропускаем метод и берём следующий.
boolean isStatic = Modifier.isStatic(method.getModifiers());
if (isStatic != registerStaticMethods) {
continue;
}
// Проверяем подпадает ли под нужные критерии наш метод и имеет ли он аннотацию
if (!checkIsMethodValid(method, instance)) { // instance может быть null для статических методов
// Проверка не прошла, пропускаем метод
continue;
}
// Все проверки пройдены, регистрируем слушателя!
final Class eventType = method.getParameterTypes()[0]; // Получаем тип эвента
final MethodHandle handle = lookup.unreflect(method); // Получаем MethodHandle для EventDelegate
// Теперь получаем список делегатов что-бы туда добавить наш новый делегат.
// Стоит обратить внимание, что список может быть null, в таком случае нужно
// будет нам создать новый список, и внести его в registry.
List<EventDelegate> delegates = this.registry.get(eventType);
if (delegates == null) {
delegates = new ArrayList<>();
this.registry.put(eventType, delegates);
}
// Добавляем наш EventDelegate в список делегатов.
// MethodHandle#invoke требует первым аргументом инстанс при виртуальных вызовах,
// поэтому используя тернарный оператор разделяем создание статических и виртуальных делегатов
delegates.add(isStatic ? handle::invoke : (e) -> handle.invoke(instance, e));
} catch (IllegalAccessException exception) {
// Ошибка доступа :(
throw new RuntimeException(exception);
}
}
} catch (SecurityException exception) {
// Если getDeclaredMethods() выкинет ошибку - мы её перевыкидываем
throw new RuntimeException(exception);
}
}
protected static boolean checkIsMethodValid(Method method, Object instance) {
// Проверяем что наш метод - не null
Objects.requireNonNull(method, "method cannot be null.");
// Получаем объект аннотации через рефлексию. Если аннотации над
// методом нет - то getAnnotation(...) вернёт null
final EventListener annotation = method.getAnnotation(EventListener.class);
if (annotation != null && // Смотрим что-бы аннотация была над методом
method.canAccess(instance) && // Проверяем что метод доступен
method.getReturnType() == Void.TYPE && // Проверяем что возвращаемый тип это void
method.getParameterCount() == 1 && // Смотрим что-бы кол-во параметров было 1 (собственно наш единственный Event)
// Важная проверка на то, что наш единственный параметр действительно наследуется от Event
Event.class.isAssignableFrom(method.getParameterTypes()[0])
) {
// Возвращаем true если все проверки прошли
return true;
}
// Возвращаем false если проверки провалились
return false;
}
public static abstract class Event {
// NO-OP
}
// Указываем что аннотацию можно использовать только на методах
@Target(ElementType.METHOD)
// Указываем что аннотация должна существовать в рантайме(после компиляции)
@Retention(RetentionPolicy.RUNTIME)
public static @interface EventListener {
// NO-OP
}
// Добавляем @FunctionalInterface аннотацию, что-бы мы могли использовать
// интерфейс в качестве лямбда-выражения (прим.: "(param) -> { expr }")
@FunctionalInterface
// Используем generic тип <T> который обязательно должен наследоваться от события Event
public static interface EventDelegate<T extends Event> {
// Собственно наш метод который будет выступать в роли слушателя и принимать событие.
// Так-же необходимо добавить "throws Throwable", т.к. мы будем использовать MethodHandle#invoke в лямбде
void call(T event) throws Throwable;
}
}
Раздел 4. Thread-safety и EventBus
Теперь давайте поговорим об использовании шин событий в мульти-поточных ситуациях. Томить не буду, давайте сразу рассмотрим проблемную ситуацию когда нам могут понадобится
Lock
'и для защиты от дедлоков и race-condition'ов.Представим что у нас есть 2 потока, и они одномоментно начали использовать шину событий. Один - пытается пройтись по событиям, другой - добавить новый список слушателей с новым слушателем. Это потенциальная ситуация с race-condition'ом где потоки не будут останавливаться и будут использовать данные по принципу "кто успел - тот и съел". Для этого мы можем использовать
Lock
. Если кратко - это объекты которые накладывают блокировку на использование объекта другим потокам заставляя их ожидать, пока первый поток не освободит этот Lock
. Для наших целей я выбрал ReentrantReadWriteLock
. Его отличие заключается в том, что он имеет 2 Lock
'а - Один на чтение, один на запись. Что интересно, так это то, что Lock
на чтение можно открывать несколько раз без блокировок, в то время как Lock
на запись будет блокировать открытие Lock
на чтение и на запись. Таким образом мы можем безопасно вносить изменения в списки слушателей, и читать одновременно кучей потоков эти списки без опаски.Работа с
ReentrantReadWriteLock
выглядит примерно так:
ReentrantReadWriteLock Example:
private final ReentrantReadWriteLock reentrantLock = new ReentrantReadWriteLock();
private final Lock readLock = reentrantLock.readLock();
private final Lock writeLock = reentrantLock.writeLock();
void myMethod() {
// Обозначаем что мы читаем данные
readLock.lock();
// Оборачиваем код в try-finally блок, что-бы даже при появлении ошибки
// мы всё-равно освободили readLock.
try {
// Делаем что нам нужно
} finally {
// Освобождаем readLock, заканчивая чтение
readLock.unlock();
}
}
Таким образом вносим изменения в наш код, и теперь наша шина событий выглядит так:
EventBus.java:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodHandles.Lookup;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class EventBus {
protected final ReentrantReadWriteLock reentrantLock = new ReentrantReadWriteLock();
protected final Lock readLock = reentrantLock.readLock();
protected final Lock writeLock = reentrantLock.writeLock();
// В этой Map'е мы будем хранить всех слушателей которые мы зарегистрировали.
// Хранятся они в этой Map'е по логике Map<Тип события, Слушатели>.
// Обратим внимание что EventDelegate нетипизирован, подобное может
// вызвать вопросы у компилятора и он начнёт жаловаться на это.
// В нашем случае это необходимость, да-бы возможно было заносить в список
// объекты EventDelegate с разными типами в качестве generic значений.
protected Map<Class<? extends Event>, List<EventDelegate>> registry;
public EventBus() {
this.registry = new HashMap<>();
}
public void register(Object eventHandler) {
// Проверяем что eventHandler - не null
Objects.requireNonNull(eventHandler, "eventHandler cannot be null.");
registerAnnotated(eventHandler.getClass(), eventHandler);
}
public void register(Class<?> eventHandler) {
// Проверяем что eventHandler - не null
Objects.requireNonNull(eventHandler, "eventHandler cannot be null.");
registerAnnotated(eventHandler, null);
}
// Обратим внимание что у нас есть локальный generic тип который наследуется от Event.
// Это событие которое мы передаём внутрь, и событие того-же типа мы должны будем вернуть.
// Дабы не обезличивать возвращаемый тип - мы и используем generic.
public <T extends Event> T post(T event) {
// Проверяем что объект события - не null
Objects.requireNonNull(event, "event cannot be null.");
// Занимаем readLock
this.readLock.lock();
// Оборачиваем в try-finally, что-бы в случае ошибки всё-равно освободить readLock
try {
// Получаем список всех слушателей события по типу(классу) этого события
final List<EventDelegate> list = this.registry.get(event.getClass());
if (list == null) {
// Если у нас нету списка слушателей - значит нету и самих слушателей.
// В таком случае выходим из метода заранее и возвращаем объект события.
return event;
}
// Вызываем слушателей
for (EventDelegate delegate : list) {
// Оборачиваем в try-catch блок из-за выбрасываемого Throwable делегатом!
try {
delegate.call(event);
} catch (Throwable t) {
// Возникла ошибка! :(
throw new RuntimeException(t);
}
}
} finally {
// Освобождаем readLock
this.readLock.unlock();
}
// Возвращаем объект события по завершению.
return event;
}
protected void registerAnnotated(Class<?> type, Object instance) {
// Проверяем что класс где будут события - не null
Objects.requireNonNull(type, "type cannot be null.");
// Если мы не предоставили instance, значит нужно регистрировать статические методы
boolean registerStaticMethods = instance == null;
// Создаём Lookup которым будем получать MethodHandle'ы для EventDelegate
final Lookup lookup = MethodHandles.publicLookup();
// Проверяем что Lookup имеет доступ к нужному классу
try {
if (lookup.accessClass(type) == null) {
throw new RuntimeException("Lookup не имеет доступа к классу " + type.getSimpleName());
}
} catch (IllegalAccessException exception) {
// Ошибка доступа :(
throw new RuntimeException(exception);
}
// Занимаем writeLock
this.writeLock.lock();
// Ищем методы которые подходят под критерии слушателей и регистрируем их
try {
// Прогоняемся по всем методам нашего класса/объекта
for (Method method : type.getDeclaredMethods()) {
// Используем еще один try-catch блок внутри, для каждого метода по отдельности
try {
// Если метод - статический, но мы не регистрируем статические методы,
// то в таком случае мы просто пропускаем метод и берём следующий.
boolean isStatic = Modifier.isStatic(method.getModifiers());
if (isStatic != registerStaticMethods) {
continue;
}
// Проверяем подпадает ли под нужные критерии наш метод и имеет ли он аннотацию
if (!checkIsMethodValid(method, instance)) { // instance может быть null для статических методов
// Проверка не прошла, пропускаем метод
continue;
}
// Все проверки пройдены, регистрируем слушателя!
final Class eventType = method.getParameterTypes()[0]; // Получаем тип эвента
final MethodHandle handle = lookup.unreflect(method); // Получаем MethodHandle для EventDelegate
// Теперь получаем список делегатов что-бы туда добавить наш новый делегат.
// Стоит обратить внимание, что список может быть null, в таком случае нужно
// будет нам создать новый список, и внести его в registry.
List<EventDelegate> delegates = this.registry.get(eventType);
if (delegates == null) {
delegates = new ArrayList<>();
this.registry.put(eventType, delegates);
}
// Добавляем наш EventDelegate в список делегатов.
// MethodHandle#invoke требует первым аргументом инстанс при виртуальных вызовах,
// поэтому используя тернарный оператор разделяем создание статических и виртуальных делегатов
delegates.add(isStatic ? handle::invoke : (e) -> handle.invoke(instance, e));
} catch (IllegalAccessException exception) {
// Ошибка доступа :(
throw new RuntimeException(exception);
}
}
} catch (SecurityException exception) {
// Если getDeclaredMethods() выкинет ошибку - мы её перевыкидываем
throw new RuntimeException(exception);
} finally {
// Освобождаем writeLock
this.writeLock.unlock();
}
}
protected static boolean checkIsMethodValid(Method method, Object instance) {
// Проверяем что наш метод - не null
Objects.requireNonNull(method, "method cannot be null.");
// Получаем объект аннотации через рефлексию. Если аннотации над
// методом нет - то getAnnotation(...) вернёт null
final EventListener annotation = method.getAnnotation(EventListener.class);
if (annotation != null && // Смотрим что-бы аннотация была над методом
method.canAccess(instance) && // Проверяем что метод доступен
method.getReturnType() == Void.TYPE && // Проверяем что возвращаемый тип это void
method.getParameterCount() == 1 && // Смотрим что-бы кол-во параметров было 1 (собственно наш единственный Event)
// Важная проверка на то, что наш единственный параметр действительно наследуется от Event
Event.class.isAssignableFrom(method.getParameterTypes()[0])
) {
// Возвращаем true если все проверки прошли
return true;
}
// Возвращаем false если проверки провалились
return false;
}
public static abstract class Event {
// NO-OP
}
// Указываем что аннотацию можно использовать только на методах
@Target(ElementType.METHOD)
// Указываем что аннотация должна существовать в рантайме(после компиляции)
@Retention(RetentionPolicy.RUNTIME)
public static @interface EventListener {
// NO-OP
}
// Добавляем @FunctionalInterface аннотацию, что-бы мы могли использовать
// интерфейс в качестве лямбда-выражения (прим.: "(param) -> { expr }")
@FunctionalInterface
// Используем generic тип <T> который обязательно должен наследоваться от события Event
public static interface EventDelegate<T extends Event> {
// Собственно наш метод который будет выступать в роли слушателя и принимать событие.
// Так-же необходимо добавить "throws Throwable", т.к. мы будем использовать MethodHandle#invoke в лямбде
void call(T event) throws Throwable;
}
}
Заключение
Вот как-то так!
Вызвать мы это всё дело можем вот так:
Example.java:
public void example() {
// Создаём шину событий
EventBus bus = new EventBus();
// Регистрируем слушателей внутри класса и объекта
bus.register(ClassWithStaticMethod.class);
bus.register(new ClassWithSimpleMethod());
// Прогоняем по слушателям событие
bus.post(new MyEvent());
}
public static class ClassWithStaticMethod {
@EventListener
public static void staticMethod(MyEvent event) {
...
}
}
public static class ClassWithSimpleMethod {
@EventListener
public static void simpleMethod(MyEvent event) {
...
}
}
public static final class MyEvent extends Event {
// NO-OP
}
Для тех кому интересно, я написал
Пожалуйста, авторизуйтесь для просмотра ссылки.
с возможностью регистрации лямбд, отменой событий, приоритетом событий и всё это в примерно таком-же формате. Расписывать всё это я уже подустал, поэтому дальше дерзайте сами :)
Последнее редактирование: