I decided to roll out my own EventBus system which is intended to be thread-safe.
Hence a review should focus extra on thread safety apart from all regular concerns.
The EventBus
can work in two ways:
- You can register events and listeners directly on the
EventBus
. - You can the methods, of a specific object, that are single argument void-methods annotated with
@Event
.
First the code, then the unit tests below:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Event { }
public interface EventBus {
void registerListenersOfObject(final Object callbackObject);
<T> void registerListener(final Class<T> eventClass, final Consumer<? extends T> eventListener);
void executeEvent(final Object event);
void removeListenersOfObject(final Object callbackObject);
<T> void removeListener(final Class<T> eventClass, final Consumer<? extends T> eventListener);
void removeAllListenersOfEvent(final Class<?> eventClass);
void removeAllListeners();
}
public class SimpleEventBus implements EventBus {
private final static Set<EventHandler> EMPTY_SET = new HashSet<>();
private final ConcurrentMap<Class<?>, Set<EventHandler>> eventMapping = new ConcurrentHashMap<>();
private final Class<?> classConstraint;
public SimpleEventBus() {
this(Object.class);
}
public SimpleEventBus(final Class<?> eventClassConstraint) {
this.classConstraint = Objects.requireNonNull(eventClassConstraint);
}
@Override
public void registerListenersOfObject(final Object callbackObject) {
Arrays.stream(callbackObject.getClass().getMethods())
.filter(method -> (method.getAnnotation(Event.class) != null))
.filter(method -> method.getReturnType().equals(void.class))
.filter(method -> method.getParameterCount() == 1)
.forEach(method -> {
Class<?> clazz = method.getParameterTypes()[0];
if (!classConstraint.isAssignableFrom(clazz)) {
return;
}
synchronized (eventMapping) {
eventMapping.putIfAbsent(clazz, new HashSet<>());
eventMapping.get(clazz).add(new MethodEventHandler(method, callbackObject, clazz));
}
});
}
@Override
@SuppressWarnings("unchecked")
public <T> void registerListener(final Class<T> eventClass, final Consumer<? extends T> eventListener) {
Objects.requireNonNull(eventClass);
Objects.requireNonNull(eventListener);
if (!classConstraint.isAssignableFrom(eventClass)) {
return;
}
synchronized(eventMapping) {
eventMapping.putIfAbsent(eventClass, new HashSet<>());
eventMapping.get(eventClass).add(new ConsumerEventHandler((Consumer<Object>)eventListener));
}
}
@Override
public void executeEvent(final Object event) {
if (classConstraint.isAssignableFrom(event.getClass())) {
eventMapping.getOrDefault(event.getClass(), EMPTY_SET).forEach(eventHandler -> eventHandler.invoke(event));
}
}
@Override
public void removeListenersOfObject(final Object callbackObject) {
Arrays.stream(callbackObject.getClass().getMethods())
.filter(method -> (method.getAnnotation(Event.class) != null))
.filter(method -> method.getReturnType().equals(void.class))
.filter(method -> method.getParameterCount() == 1)
.forEach(method -> {
Class<?> clazz = method.getParameterTypes()[0];
if (classConstraint.isAssignableFrom(clazz)) {
eventMapping.getOrDefault(clazz, EMPTY_SET).remove(new MethodEventHandler(method, callbackObject, clazz));
}
});
}
@Override
@SuppressWarnings("unchecked")
public <T> void removeListener(final Class<T> eventClass, final Consumer<? extends T> eventListener) {
Objects.requireNonNull(eventClass);
Objects.requireNonNull(eventListener);
if (classConstraint.isAssignableFrom(eventClass)) {
eventMapping.getOrDefault(eventClass, EMPTY_SET).remove(new ConsumerEventHandler((Consumer<Object>)eventListener));
}
}
@Override
public void removeAllListenersOfEvent(final Class<?> eventClass) {
Objects.requireNonNull(eventClass);
eventMapping.remove(eventClass);
}
@Override
public void removeAllListeners() {
eventMapping.clear();
}
private static interface EventHandler {
void invoke(final Object event);
}
private static class MethodEventHandler implements EventHandler {
private final Method method;
private final Object callbackObject;
private final Class<?> eventClass;
public MethodEventHandler(final Method method, final Object object, final Class<?> eventClass) {
this.method = Objects.requireNonNull(method);
this.callbackObject = Objects.requireNonNull(object);
this.eventClass = Objects.requireNonNull(eventClass);
}
@Override
public void invoke(final Object event) {
try {
method.setAccessible(true);
method.invoke(callbackObject, Objects.requireNonNull(event));
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
throw new RuntimeException(ex);
}
}
@Override
public int hashCode() {
int hash = 7;
hash = 71 * hash + Objects.hashCode(this.method);
hash = 71 * hash + Objects.hashCode(this.callbackObject);
hash = 71 * hash + Objects.hashCode(this.eventClass);
return hash;
}
@Override
public boolean equals(final Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final MethodEventHandler other = (MethodEventHandler)obj;
if (!Objects.equals(this.method, other.method)) {
return false;
}
if (!Objects.equals(this.callbackObject, other.callbackObject)) {
return false;
}
if (!Objects.equals(this.eventClass, other.eventClass)) {
return false;
}
return true;
}
}
private static class ConsumerEventHandler implements EventHandler {
private final Consumer<Object> eventListener;
public ConsumerEventHandler(final Consumer<Object> consumer) {
this.eventListener = Objects.requireNonNull(consumer);
}
@Override
public void invoke(final Object event) {
eventListener.accept(Objects.requireNonNull(event));
}
@Override
public int hashCode() {
int hash = 5;
hash = 19 * hash + Objects.hashCode(this.eventListener);
return hash;
}
@Override
public boolean equals(final Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final ConsumerEventHandler other = (ConsumerEventHandler)obj;
if (!Objects.equals(this.eventListener, other.eventListener)) {
return false;
}
return true;
}
}
}
public class SimpleEventBusTest {
static {
assertTrue(true);
}
private AtomicInteger alphaCounter;
private AtomicInteger betaCounter;
private AtomicInteger gammaCounter;
@Before
public void before() {
alphaCounter = new AtomicInteger(0);
betaCounter = new AtomicInteger(0);
gammaCounter = new AtomicInteger(0);
}
private Stream<AtomicInteger> counters() {
return Stream.of(alphaCounter, betaCounter, gammaCounter);
}
@Test
public void testConstructor() {
EventBus eventBus = new SimpleEventBus();
eventBus.registerListenersOfObject(new Object() {
@Event
public void onAlphaEvent(final AlphaEvent alphaEvent) {
alphaCounter.incrementAndGet();
}
});
eventBus.executeEvent(new AlphaEvent());
assertEquals(1, alphaCounter.get());
}
@Test
public void testConstructorWithEventClassConstraint() {
EventBus eventBus = new SimpleEventBus(BetaEvent.class);
eventBus.registerListenersOfObject(new Object() {
@Event
public void onAlphaEvent(final AlphaEvent alphaEvent) {
alphaCounter.incrementAndGet();
}
});
eventBus.registerListener(AlphaEvent.class, alphaEvent -> alphaCounter.incrementAndGet());
eventBus.executeEvent(new AlphaEvent());
assertEquals(0, alphaCounter.get());
}
@Test
public void testRegisterListenersOfObject() {
EventBus eventBus = new SimpleEventBus();
eventBus.registerListenersOfObject(new Object() {
@Event
public void onAlphaEvent1(final AlphaEvent alphaEvent) {
alphaCounter.incrementAndGet();
}
@Event
public void onAlphaEvent2(final AlphaEvent alphaEvent) {
alphaCounter.incrementAndGet();
}
@Event
public void onAlphaEvent3(final AlphaEvent alphaEvent) {
alphaCounter.incrementAndGet();
}
@Event
public void onBetaEvent1(final BetaEvent betaEvent) {
betaCounter.incrementAndGet();
}
@Event
public void onBetaEvent2(final BetaEvent betaEvent) {
betaCounter.incrementAndGet();
}
@Event
public void onGammaEvent(final GammaEvent gammaEvent) {
gammaCounter.incrementAndGet();
}
});
eventBus.executeEvent(new AlphaEvent());
eventBus.executeEvent(new BetaEvent());
eventBus.executeEvent(new GammaEvent());
assertEquals(3, alphaCounter.get());
assertEquals(2, betaCounter.get());
assertEquals(1, gammaCounter.get());
eventBus.executeEvent(new AlphaEvent());
eventBus.executeEvent(new BetaEvent());
eventBus.executeEvent(new GammaEvent());
assertEquals(6, alphaCounter.get());
assertEquals(4, betaCounter.get());
assertEquals(2, gammaCounter.get());
}
@Test
public void testRegisterListener() {
EventBus eventBus = new SimpleEventBus();
eventBus.registerListener(AlphaEvent.class, alphaEvent -> alphaCounter.incrementAndGet());
eventBus.registerListener(AlphaEvent.class, alphaEvent -> alphaCounter.incrementAndGet());
eventBus.registerListener(AlphaEvent.class, alphaEvent -> alphaCounter.incrementAndGet());
eventBus.registerListener(BetaEvent.class, betaEvent -> betaCounter.incrementAndGet());
eventBus.registerListener(BetaEvent.class, betaEvent -> betaCounter.incrementAndGet());
eventBus.registerListener(GammaEvent.class, gammaEvent -> gammaCounter.incrementAndGet());
eventBus.executeEvent(new AlphaEvent());
eventBus.executeEvent(new BetaEvent());
eventBus.executeEvent(new GammaEvent());
assertEquals(3, alphaCounter.get());
assertEquals(2, betaCounter.get());
assertEquals(1, gammaCounter.get());
eventBus.executeEvent(new AlphaEvent());
eventBus.executeEvent(new BetaEvent());
eventBus.executeEvent(new GammaEvent());
assertEquals(6, alphaCounter.get());
assertEquals(4, betaCounter.get());
assertEquals(2, gammaCounter.get());
}
@Test
public void testExecuteEvent() {
EventBus eventBus = new SimpleEventBus();
eventBus.registerListener(AlphaEvent.class, alphaEvent -> alphaCounter.incrementAndGet());
eventBus.executeEvent(new AlphaEvent());
assertEquals(1, alphaCounter.get());
}
@Test
public void testExecuteEventSameInstance() {
AlphaEvent specificAlphaEvent = new AlphaEvent();
EventBus eventBus = new SimpleEventBus();
eventBus.registerListener(AlphaEvent.class, alphaEvent -> assertTrue(alphaEvent == specificAlphaEvent));
}
@Test
public void testRemoveListenersOfObject() {
EventBus eventBus = new SimpleEventBus();
Object object1 = new Object() {
@Event
public void onAlphaEvent(final AlphaEvent alphaEvent) {
alphaCounter.incrementAndGet();
}
@Event
public void onBetaEvent(final BetaEvent betaEvent) {
betaCounter.incrementAndGet();
}
@Event
public void onGammaEvent(final GammaEvent gammaEvent) {
gammaCounter.incrementAndGet();
}
};
Object object2 = new Object() {
@Event
public void onAlphaEvent(final AlphaEvent alphaEvent) {
alphaCounter.incrementAndGet();
}
@Event
public void onBetaEvent(final BetaEvent betaEvent) {
betaCounter.incrementAndGet();
}
@Event
public void onGammaEvent(final GammaEvent gammaEvent) {
gammaCounter.incrementAndGet();
}
};
eventBus.registerListenersOfObject(object1);
eventBus.executeEvent(new AlphaEvent());
eventBus.executeEvent(new BetaEvent());
eventBus.executeEvent(new GammaEvent());
counters().allMatch(counter -> counter.get() == 1);
eventBus.registerListenersOfObject(object2);
eventBus.executeEvent(new AlphaEvent());
eventBus.executeEvent(new BetaEvent());
eventBus.executeEvent(new GammaEvent());
counters().allMatch(counter -> counter.get() == 3);
eventBus.removeListenersOfObject(object2);
eventBus.executeEvent(new AlphaEvent());
eventBus.executeEvent(new BetaEvent());
eventBus.executeEvent(new GammaEvent());
counters().allMatch(counter -> counter.get() == 4);
eventBus.removeListenersOfObject(object1);
eventBus.executeEvent(new AlphaEvent());
eventBus.executeEvent(new BetaEvent());
eventBus.executeEvent(new GammaEvent());
counters().allMatch(counter -> counter.get() == 4);
}
@Test
public void testRemoveListener() {
EventBus eventBus = new SimpleEventBus();
Consumer<AlphaEvent> alphaEventListener = alphaEvent -> alphaCounter.incrementAndGet();
Consumer<BetaEvent> betaEventListener = betaEvent -> betaCounter.incrementAndGet();
Consumer<GammaEvent> gammaEventListener = gammaEvent -> gammaCounter.incrementAndGet();
eventBus.registerListener(AlphaEvent.class, alphaEventListener);
eventBus.registerListener(BetaEvent.class, betaEventListener);
eventBus.registerListener(GammaEvent.class, gammaEventListener);
eventBus.executeEvent(new AlphaEvent());
eventBus.executeEvent(new BetaEvent());
eventBus.executeEvent(new GammaEvent());
assertEquals(1, alphaCounter.get());
assertEquals(1, betaCounter.get());
assertEquals(1, gammaCounter.get());
eventBus.removeListener(GammaEvent.class, gammaEventListener);
eventBus.executeEvent(new AlphaEvent());
eventBus.executeEvent(new BetaEvent());
eventBus.executeEvent(new GammaEvent());
assertEquals(2, alphaCounter.get());
assertEquals(2, betaCounter.get());
assertEquals(1, gammaCounter.get());
eventBus.removeListener(BetaEvent.class, betaEventListener);
eventBus.executeEvent(new AlphaEvent());
eventBus.executeEvent(new BetaEvent());
eventBus.executeEvent(new GammaEvent());
assertEquals(3, alphaCounter.get());
assertEquals(2, betaCounter.get());
assertEquals(1, gammaCounter.get());
eventBus.removeListener(AlphaEvent.class, alphaEventListener);
eventBus.executeEvent(new AlphaEvent());
eventBus.executeEvent(new BetaEvent());
eventBus.executeEvent(new GammaEvent());
assertEquals(3, alphaCounter.get());
assertEquals(2, betaCounter.get());
assertEquals(1, gammaCounter.get());
}
@Test
public void testRemoveAllListenersOfEvent() {
EventBus eventBus = new SimpleEventBus();
eventBus.registerListener(AlphaEvent.class, alphaEvent -> alphaCounter.incrementAndGet());
eventBus.registerListener(AlphaEvent.class, alphaEvent -> alphaCounter.incrementAndGet());
eventBus.registerListener(AlphaEvent.class, alphaEvent -> alphaCounter.incrementAndGet());
eventBus.registerListener(BetaEvent.class, betaEvent -> betaCounter.incrementAndGet());
eventBus.registerListener(BetaEvent.class, betaEvent -> betaCounter.incrementAndGet());
eventBus.registerListener(GammaEvent.class, gammaEvent -> gammaCounter.incrementAndGet());
eventBus.executeEvent(new AlphaEvent());
eventBus.executeEvent(new BetaEvent());
eventBus.executeEvent(new GammaEvent());
assertEquals(3, alphaCounter.get());
assertEquals(2, betaCounter.get());
assertEquals(1, gammaCounter.get());
eventBus.removeAllListenersOfEvent(AlphaEvent.class);
eventBus.executeEvent(new AlphaEvent());
eventBus.executeEvent(new BetaEvent());
eventBus.executeEvent(new GammaEvent());
assertEquals(3, alphaCounter.get());
assertEquals(4, betaCounter.get());
assertEquals(2, gammaCounter.get());
eventBus.removeAllListenersOfEvent(BetaEvent.class);
eventBus.executeEvent(new AlphaEvent());
eventBus.executeEvent(new BetaEvent());
eventBus.executeEvent(new GammaEvent());
assertEquals(3, alphaCounter.get());
assertEquals(4, betaCounter.get());
assertEquals(3, gammaCounter.get());
eventBus.removeAllListenersOfEvent(GammaEvent.class);
eventBus.executeEvent(new AlphaEvent());
eventBus.executeEvent(new BetaEvent());
eventBus.executeEvent(new GammaEvent());
assertEquals(3, alphaCounter.get());
assertEquals(4, betaCounter.get());
assertEquals(3, gammaCounter.get());
}
@Test
public void testRemoveAllListeners() {
EventBus eventBus = new SimpleEventBus();
eventBus.registerListener(AlphaEvent.class, alphaEvent -> alphaCounter.incrementAndGet());
eventBus.registerListener(AlphaEvent.class, alphaEvent -> alphaCounter.incrementAndGet());
eventBus.registerListener(AlphaEvent.class, alphaEvent -> alphaCounter.incrementAndGet());
eventBus.registerListener(BetaEvent.class, betaEvent -> betaCounter.incrementAndGet());
eventBus.registerListener(BetaEvent.class, betaEvent -> betaCounter.incrementAndGet());
eventBus.registerListener(GammaEvent.class, gammaEvent -> gammaCounter.incrementAndGet());
eventBus.executeEvent(new AlphaEvent());
eventBus.executeEvent(new BetaEvent());
eventBus.executeEvent(new GammaEvent());
assertEquals(3, alphaCounter.get());
assertEquals(2, betaCounter.get());
assertEquals(1, gammaCounter.get());
eventBus.removeAllListeners();
eventBus.executeEvent(new AlphaEvent());
eventBus.executeEvent(new BetaEvent());
eventBus.executeEvent(new GammaEvent());
assertEquals(3, alphaCounter.get());
assertEquals(2, betaCounter.get());
assertEquals(1, gammaCounter.get());
}
private static class AlphaEvent { }
private static class BetaEvent { }
private static class GammaEvent { }
}