Гайд [Java] Учимся писать свою шину событий (Event Bus)

Начинающий
Начинающий
Статус
Оффлайн
Регистрация
5 Июн 2025
Сообщения
515
Реакции
26
В этом гайде мы рассмотрим что такое шина событий(Event Bus), рассмотрим примеры использования шин событий, как её можно реализовать.

Раздел 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:
Expand Collapse Copy
public abstract class Event {
// Тут пусто! :o
}
Невероятно, правда? Поскольку у нас не будет механизма отмены событий, нам достаточно пустого абстрактного класса, от которого будут наследоваться все реальные события. Этот абстрактный класс так-же мы будем использовать при поиске слушателей событий.

Далее по плану аннотация слушателя событий:
EventListener.java:
Expand Collapse Copy
// Указываем что аннотацию можно использовать только на методах
@Target(ElementType.METHOD)

// Указываем что аннотация должна существовать в рантайме(после компиляции)
@Retention(RetentionPolicy.RUNTIME)
public @interface EventListener {
}
Здесь тоже всё довольно просто, 2 другие аннотации на нашу аннотацию и всё готово.

Далее мы создадим объект который будет представлять делегат события, или если проще - функцию которая будет слушателем события:
EventDelegate.java:
Expand Collapse Copy
// Добавляем @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:
Expand Collapse Copy
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) {
        ...
    }
}
По сути примерно так будет выглядеть структура нашей шины событий. Последние 2 метода использовать будем только внутри самой шины для регистрации слушателей, поэтому фактически у нас будет всего 3 метода которыми мы будем пользоваться из-вне. Довольно просто!

Начнём с простого - с публикации события по нашим слушателям. У нас уже есть Map<...> registry который хранит всех наших слушателей, поэтому нам останется всего-лишь достать список со слушателями событий определенного типа, и пройтись по всем им и вызвать их:
EventBus.java (EventBus#post(Event)):
Expand Collapse Copy
// Обратим внимание что у нас есть локальный 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(...)):
Expand Collapse Copy
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(...)):
Expand Collapse Copy
// Регистрация объекта со слушателями событий
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:
Expand Collapse Copy
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:
Expand Collapse Copy
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:
Expand Collapse Copy
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:
Expand Collapse Copy
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
}

Для тех кому интересно, я написал
Пожалуйста, авторизуйтесь для просмотра ссылки.
с возможностью регистрации лямбд, отменой событий, приоритетом событий и всё это в примерно таком-же формате. Расписывать всё это я уже подустал, поэтому дальше дерзайте сами :)
 
Последнее редактирование:
1756850458230.png
 
:pepe2:На кого это рассчитано?
 
:pepe2:На кого это рассчитано?
На раздел в котором это написано, зачастую это запросто могут быть просто дети которые ЯП в принципе не знают
Пусть уж лучше так, чем копипастят
 
В этом гайде мы рассмотрим что такое шина событий(Event Bus), рассмотрим примеры использования шин событий, как её можно реализовать.

Раздел 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:
Expand Collapse Copy
public abstract class Event {
// Тут пусто! :o
}
Невероятно, правда? Поскольку у нас не будет механизма отмены событий, нам достаточно пустого абстрактного класса, от которого будут наследоваться все реальные события. Этот абстрактный класс так-же мы будем использовать при поиске слушателей событий.

Далее по плану аннотация слушателя событий:
EventListener.java:
Expand Collapse Copy
// Указываем что аннотацию можно использовать только на методах
@Target(ElementType.METHOD)

// Указываем что аннотация должна существовать в рантайме(после компиляции)
@Retention(RetentionPolicy.RUNTIME)
public @interface EventListener {
}
Здесь тоже всё довольно просто, 2 другие аннотации на нашу аннотацию и всё готово.

Далее мы создадим объект который будет представлять делегат события, или если проще - функцию которая будет слушателем события:
EventDelegate.java:
Expand Collapse Copy
// Добавляем @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:
Expand Collapse Copy
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) {
        ...
    }
}
По сути примерно так будет выглядеть структура нашей шины событий. Последние 2 метода использовать будем только внутри самой шины для регистрации слушателей, поэтому фактически у нас будет всего 3 метода которыми мы будем пользоваться из-вне. Довольно просто!

Начнём с простого - с публикации события по нашим слушателям. У нас уже есть Map<...> registry который хранит всех наших слушателей, поэтому нам останется всего-лишь достать список со слушателями событий определенного типа, и пройтись по всем им и вызвать их:
EventBus.java (EventBus#post(Event)):
Expand Collapse Copy
// Обратим внимание что у нас есть локальный 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(...)):
Expand Collapse Copy
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(...)):
Expand Collapse Copy
// Регистрация объекта со слушателями событий
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:
Expand Collapse Copy
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:
Expand Collapse Copy
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:
Expand Collapse Copy
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:
Expand Collapse Copy
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
}

Для тех кому интересно, я написал
Пожалуйста, авторизуйтесь для просмотра ссылки.
с возможностью регистрации лямбд, отменой событий, приоритетом событий и всё это в примерно таком-же формате. Расписывать всё это я уже подустал, поэтому дальше дерзайте сами :)
реализация неплохая, но есть и другой способ не создавая свои лямбды, а вызывая метод (не через байткод естественно, но если выебаться конкретно в мозг, то можно и через байткод), и естественно он будет чуть медленней (и я даже не про вызов слушателей через рефлексию), но зато в какой-то степени удобней, чем лямбды
 
реализация неплохая, но есть и другой способ не создавая свои лямбды, а вызывая метод (не через байткод естественно, но если выебаться конкретно в мозг, то можно и через байткод), и естественно он будет чуть медленней (и я даже не про вызов слушателей через рефлексию), но зато в какой-то степени удобней, чем лямбды
Не очень понял, ты про создание простых классов-пустышек с зависимостью от EventDelegate и выполнением условного MethodHandles#invoke вместо лямбды?
У меня оно написано в гитхаб репе которую я прикрепил, мне чет кажется что вызов metafactory будет помедленнее чем создание объекта-пустышки который вызывается через invokevirtual а не invokedynamic

Ещё не очень понял аргумент "в какой-то степени удобней", в примере лямбды вообще не зарегать, а в гитхаб репе тип возможность есть, но оно всё-равно всё подкапотное, можно регать как просто имплементацию интерфейса, так лямбду, или отдельные методы которые прост обернутся в классы-пустышки для вызовов

На счёт быстрее/медленнее - я бы в принципе забил, учитывая что кубач не то что-бы пример хорошего и производительного кода, да и в разделе далеко не то что-бы перформанс-дрочеры сидят. В кубаче всегда просто будет что-то что можно будет оптимизировать и это даст буст по производительности
 
Последнее редактирование:
Не очень понял, ты про создание простых классов-пустышек с зависимостью от EventDelegate и выполнением условного MethodHandles#invoke вместо лямбды?
ты не помнишь darkmagician6 апи событий? там реализовано так, как я написал, т.е. создается метод в классе, который прослушивается (и имеет аргумент в виде нужного события) и он вызывается в post'e
У меня оно написано в гитхаб репе которую я прикрепил, мне чет кажется что вызов metafactory будет помедленнее чем создание объекта-пустышки который вызывается через invokevirtual а не invokedynamic
я думаю, что invokedynamic не будет проигрывать сильно, только при первой генерации и всё
На счёт быстрее/медленнее - я бы в принципе забил, учитывая что кубач не то что-бы пример хорошего и производительного кода, да и в разделе далеко не то что-бы перформанс-дрочеры сидят. В кубаче всегда просто будет что-то что можно будет оптимизировать и это даст буст по производительности
да я понимаю, но всё же, мне кажется используя ту же рефлексию на примере darkmagician6 кода в событиях, которые используются чуть ли не каждый тик (а может и вовсе каждый кадр) то тут всё таки стоило бы подумать над этим
 
ты не помнишь darkmagician6 апи событий? там реализовано так, как я написал, т.е. создается метод в классе, который прослушивается (и имеет аргумент в виде нужного события) и он вызывается в post'e
Я вообще если честно не знаю что это, я где-то слышал что когда-то Forge использовал что-то подобное, только проблема в том что это провернуть в рантайме будто бы анальная боль, т.к. условно EventHandler твой уже загрузился, потом его нужно каким-то образом трансформировать, а после - поменить вызов соответствующих эвентов, как при этом к примеру происходила бы отмена регистрации - я представить не особо могу, кроме как повторно всё это прогонять через трансформеры (это что касается патчинга байткода)
То что я на
Пожалуйста, авторизуйтесь для просмотра ссылки.
, там вообще обычный Method#invoke используется, поэтому сомневаюсь прям что там подмена байткода какая-то есть

я думаю, что invokedynamic не будет проигрывать сильно, только при первой генерации и всё
Ну тип ради интереса могу вообще у знающего человека расспросить касательно этого

мне кажется используя ту же рефлексию на примере darkmagician6 кода в событиях, которые используются чуть ли не каждый тик (а может и вовсе каждый кадр) то тут всё таки стоило бы подумать над этим
Ну судя из того что я подкрепил, там дефолт рефлексия, накладные расходы на рефлексию думаю полюбому будут дороже любых двух вызовов через invoke* опкоды чисто из-за оверхеда на проверки корректности вызовов в рантайме.

У меня вообще если честно есть ощущение что я тебя не очень понимаю, если можешь прикрепи какой-нибудь пример минимального кода, мб так попроще будет понять что ты имеешь ввиду.
 
У меня вообще если честно есть ощущение что я тебя не очень понимаю, если можешь прикрепи какой-нибудь пример минимального кода, мб так попроще будет понять что ты имеешь ввиду.
ну вот это "примерный" код с использованием darkmagician6 like шины событий

Экземпл:
Expand Collapse Copy
public class TestListener {
    @Subscribe // создаем аннотацию, чтобы было найти легко этот метод, который нужно вызывать для события
    public void onKeyInput(InputKeyEvent event) { // public лучше в данном контексте, т.к. не нужно вызывать setAccessible, ну или если мы решили всё таки создавать целый акцессор для нашего метода, то мы ведь не хотим наследовать его от MagicAccessor верно?
        int k = event.getKey(); // ну и соответственно получаем нужные поля объекта события, например нажатая клавиша
    }
}

его напрямую вызывать нельзя, ну или можно, но только придется поработать с классами (т.е. создавать велосипед в стиле invokedynamic, билдить специальный класс accessor для этого и через него уже вызывать этот метод с помощью байткода, а не через нативный JNIEnv::CallMethod как это делает та же рефлексия, или MethodHandle, который тоже опускается в вм)
 
ну вот это "примерный" код с использованием darkmagician6 like шины событий

Экземпл:
Expand Collapse Copy
public class TestListener {
    @Subscribe // создаем аннотацию, чтобы было найти легко этот метод, который нужно вызывать для события
    public void onKeyInput(InputKeyEvent event) { // public лучше в данном контексте, т.к. не нужно вызывать setAccessible, ну или если мы решили всё таки создавать целый акцессор для нашего метода, то мы ведь не хотим наследовать его от MagicAccessor верно?
        int k = event.getKey(); // ну и соответственно получаем нужные поля объекта события, например нажатая клавиша
    }
}
Структура вроде бы обычная самого слушателя, что за MagicAccessor - если честно не понял, ничего не нашёл касательно этого. Поэтому для меня это пока-что всё еще странно. На всякий спросил у знакомого, мб что-то выдаст интересное, тогда попозже отвечу если что.

ну или можно, но только придется поработать с классами (т.е. создавать велосипед в стиле invokedynamic, билдить специальный класс accessor для этого и через него уже вызывать этот метод с помощью байткода, а не через нативный JNIEnv::CallMethod)
Так то и через лямбды можно, передав его к примеру как TestListener::onKeyInput, в гит-репе которую я подкидывал там такое провернуть спокойно можно с одиночными лямбдами которые ты регаешь вручную, остальные через методхендлы и рефлексию.

Из интересного пока копался,
Пожалуйста, авторизуйтесь для просмотра ссылки.
, думаю разница в 3-5ns сильно много сыграла бы один фиг, эвентов не много, слушателей не много у читов, даже взять форджевские вызовы - дай бог 5-10μs займёт за все вызовы/кадр(что в целом не то что-бы много как накладные расходы)
 
:bayan:Пишите как удобно вам, рефлексия не жрёт настолько много чтобы от неё полностью отказываться. Если вы конечно не используете её каждую секунду.
Удобно писать события методами? Пишите
 
Так то и через лямбды можно, передав его к примеру как TestListener::onKeyInput, в гит-репе которую я подкидывал там такое провернуть спокойно можно с одиночными лямбдами которые ты регаешь вручную, остальные через методхендлы и рефлексию.
ну, когда у тебя классов-слушателей много, и ты их не хочешь просто наследовать от класса, в котором будут прописаны все методы для каждого события, то это уже будет выглядеть как говнокод, если честно

также неважно какое название у метода, хоть asdasd, главное то что он помечен как метод, который должен вызываться при определенном событии, и ещё то что у него имеется аргумент в виде объекта события
 
:bayan:Пишите как удобно вам, рефлексия не жрёт настолько много чтобы от неё полностью отказываться. Если вы конечно не используете её каждую секунду.
Удобно писать события методами? Пишите
События в кубаче как правило гоняются сотнями в пределах тика(50мс), и по несколько десятков на кадр(мин. 16.7мс)
Конечно тут не варик втупую использовать рефлексию

ну, когда у тебя классов-слушателей много, и ты их не хочешь просто наследовать от класса, в котором будут прописаны все методы для каждого события, то это уже будет выглядеть как говнокод, если честно
Ну это как минимум канает для высокозагруженных методов, можно зарегать класс, перерегать высоконагруженный (если вообще конечно будет необходимость в приросте на пару наносекунд, в чём я сомневаюсь если честно), но если прописывать кучу - то тут согласен

На счёт генерации классов пустышек я узнал, знакомый привёл пример от Forge, в целом да, там это спокойно работает, как аналог я бы забабахал MethodHandle#find* что-бы вызовы были более простыми, но тут пока разбираюсь почему оно решило откинуть копыта и не компилится, еще и с мавен публикацией пока-что ебусь чуток

На всякий прикреплю что-бы уточнить это ли ты имел ввиду под тем приколом с классами пустышками или нет:
Forge ASM EventListener:
Expand Collapse Copy
// Класс сгенерированный через ASM, создаётся 1 раз и кэшируется
public final class DynamicWrapperName1234 implements IEventListener {
   private final SomeEventHandlerClass instance; // Инстанс EventHandler'а, класса со слушателями событий

   public DynamicWrapperName1234(SomeEventHandlerClass instance) {
      this.instance = instance;
   }

   @Override
   public void invoke(Event event) {
      // Вызов слушателя события
      this.instance.onBlockBreak((BlockEvent.BreakEvent) event);
   }
}
(Хотя, тот-же фордж их создаёт через рефлексию, но в целом накладных при использовании реально меньше)
 
События в кубаче как правило гоняются сотнями в пределах тика(50мс), и по несколько десятков на кадр(мин. 16.7мс)
Конечно тут не варик втупую использовать рефлексию


Ну это как минимум канает для высокозагруженных методов, можно зарегать класс, перерегать высоконагруженный (если вообще конечно будет необходимость в приросте на пару наносекунд, в чём я сомневаюсь если честно), но если прописывать кучу - то тут согласен

На счёт генерации классов пустышек я узнал, знакомый привёл пример от Forge, в целом да, там это спокойно работает, как аналог я бы забабахал MethodHandle#find* что-бы вызовы были более простыми, но тут пока разбираюсь почему оно решило откинуть копыта и не компилится, еще и с мавен публикацией пока-что ебусь чуток

На всякий прикреплю что-бы уточнить это ли ты имел ввиду под тем приколом с классами пустышками или нет:
Forge ASM EventListener:
Expand Collapse Copy
// Класс сгенерированный через ASM, создаётся 1 раз и кэшируется
public final class DynamicWrapperName1234 implements IEventListener {
   private final SomeEventHandlerClass instance; // Инстанс EventHandler'а, класса со слушателями событий

   public DynamicWrapperName1234(SomeEventHandlerClass instance) {
      this.instance = instance;
   }

   @Override
   public void invoke(Event event) {
      // Вызов слушателя события
      this.instance.onBlockBreak((BlockEvent.BreakEvent) event);
   }
}
(Хотя, тот-же фордж их создаёт через рефлексию, но в целом накладных при использовании реально меньше)
Ну срать рефлексией и не надо, один раз в инициализации по методам пробежался и сидишь говно накидываешь в автобус.
 
Ну срать рефлексией и не надо, один раз в инициализации по методам пробежался и сидишь говно накидываешь в автобус.
ну т.к. для этого либо нужно брать ASM и генерить классы-пустышки, либо вызовы в лямбды оборачивать. Тут чуток другой пример рассматривается
 
Назад
Сверху Снизу