All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.killbill.commons.eventbus.SubscriberRegistry Maven / Gradle / Ivy

/*
 * Copyright (C) 2007 The Guava Authors
 * Copyright 2020-2022 Equinix, Inc
 * Copyright 2014-2022 The Billing Project, LLC
 *
 * The Billing Project licenses this file to you under the Apache License, version 2.0
 * (the "License"); you may not use this file except in compliance with the
 * License.  You may obtain a copy of the License at:
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
 * License for the specific language governing permissions and limitations
 * under the License.
 */

package org.killbill.commons.eventbus;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArraySet;
import javax.annotation.CheckForNull;

import org.killbill.commons.utils.Preconditions;
import org.killbill.commons.utils.Primitives;
import org.killbill.commons.utils.TypeToken;
import org.killbill.commons.utils.annotation.VisibleForTesting;
import org.killbill.commons.utils.cache.Cache;
import org.killbill.commons.utils.cache.DefaultCache;
import org.killbill.commons.utils.cache.DefaultSynchronizedCache;
import org.killbill.commons.utils.collect.Iterators;
import org.killbill.commons.utils.collect.MultiValueHashMap;
import org.killbill.commons.utils.collect.MultiValueMap;

/**
 * Registry of subscribers to a single event bus.
 *
 * @author Colin Decker
 */
final class SubscriberRegistry {

    /**
     * All registered subscribers, indexed by event type.
     *
     * 

The {@link CopyOnWriteArraySet} values make it easy and relatively lightweight to get an * immutable snapshot of all current subscribers to an event without any locking. */ private final ConcurrentMap, CopyOnWriteArraySet> subscribers = new ConcurrentHashMap<>(); /** The event bus this registry belongs to. */ private final EventBus bus; SubscriberRegistry(final EventBus bus) { this.bus = Preconditions.checkNotNull(bus); } /** Registers all subscriber methods on the given listener object. */ void register(final Object listener) { final MultiValueMap, Subscriber> listenerMethods = findAllSubscribers(listener); for (final Entry, List> entry : listenerMethods.entrySet()) { final Class eventType = entry.getKey(); final Collection eventMethodsInListener = entry.getValue(); CopyOnWriteArraySet eventSubscribers = subscribers.get(eventType); if (eventSubscribers == null) { final CopyOnWriteArraySet newSet = new CopyOnWriteArraySet<>(); eventSubscribers = Objects.requireNonNullElse(subscribers.putIfAbsent(eventType, newSet), newSet); } eventSubscribers.addAll(eventMethodsInListener); } } /** Unregisters all subscribers on the given listener object. */ void unregister(final Object listener) { final MultiValueMap, Subscriber> listenerMethods = findAllSubscribers(listener); for (final Entry, List> entry : listenerMethods.entrySet()) { final Class eventType = entry.getKey(); final Collection listenerMethodsForType = entry.getValue(); final CopyOnWriteArraySet currentSubscribers = subscribers.get(eventType); if (currentSubscribers == null || !currentSubscribers.removeAll(listenerMethodsForType)) { // if removeAll returns true, all we really know is that at least one subscriber was // removed... however, barring something very strange we can assume that if at least one // subscriber was removed, all subscribers on listener for that event type were... after // all, the definition of subscribers on a particular class is totally static throw new IllegalArgumentException("missing event subscriber for an annotated method. Is " + listener + " registered?"); } // don't try to remove the set if it's empty; that can't be done safely without a lock // anyway, if the set is empty it'll just be wrapping an array of length 0 } } @VisibleForTesting Set getSubscribersForTesting(final Class eventType) { return Objects.requireNonNullElse(subscribers.get(eventType), Collections.emptySet()); } /** * Gets an iterator representing an immutable snapshot of all subscribers to the given event at * the time this method is called. */ Iterator getSubscribers(final Object event) { final Set> eventTypes = flattenHierarchy(event.getClass()); final List> subscriberIterators = new ArrayList<>(eventTypes.size()); for (final Class eventType : eventTypes) { final CopyOnWriteArraySet eventSubscribers = subscribers.get(eventType); if (eventSubscribers != null) { // eager no-copy snapshot subscriberIterators.add(eventSubscribers.iterator()); } } return Iterators.concat(subscriberIterators.iterator()); } /** * A thread-safe cache that contains the mapping from each class to all methods in that class and * all super-classes, that are annotated with {@code @Subscribe}. The cache is shared across all * instances of this class; this greatly improves performance if multiple EventBus instances are * created and objects of the same class are registered on all of them. */ private static final Cache, List> subscriberMethodsCache = new DefaultSynchronizedCache<>( Integer.MAX_VALUE, DefaultCache.NO_TIMEOUT, SubscriberRegistry::getAnnotatedMethodsNotCached ); /** * Returns all subscribers for the given listener grouped by the type of event they subscribe to. */ private MultiValueMap, Subscriber> findAllSubscribers(final Object listener) { final MultiValueMap, Subscriber> methodsInListener = new MultiValueHashMap<>(); final Class clazz = listener.getClass(); for (final Method method : subscriberMethodsCache.get(clazz)) { final Class[] parameterTypes = method.getParameterTypes(); final Class eventType = parameterTypes[0]; methodsInListener.putElement(eventType, Subscriber.create(bus, listener, method)); } return methodsInListener; } private static List getAnnotatedMethodsNotCached(final Class clazz) { final Set> supertypes = TypeToken.getRawTypes(clazz); final Map identifiers = new HashMap<>(); for (final Class supertype : supertypes) { for (final Method method : supertype.getDeclaredMethods()) { if (method.isAnnotationPresent(Subscribe.class) && !method.isSynthetic()) { // TODO(cgdecker): Should check for a generic parameter type and error out final Class[] parameterTypes = method.getParameterTypes(); Preconditions.checkArgument(parameterTypes.length == 1, "Method %s has @Subscribe annotation but has %s parameters. Subscriber methods must have exactly 1 parameter.", method, parameterTypes.length); Preconditions.checkArgument(!parameterTypes[0].isPrimitive(), "@Subscribe method %s's parameter is %s. Subscriber methods cannot accept primitives. Consider changing the parameter to %s.", method, parameterTypes[0].getName(), Primitives.wrap(parameterTypes[0]).getSimpleName()); final MethodIdentifier ident = new MethodIdentifier(method); if (!identifiers.containsKey(ident)) { identifiers.put(ident, method); } } } } return List.copyOf(identifiers.values()); } /** * Global cache of classes to their flattened hierarchy of supertypes. * * Guava version * */ private static final Cache, Set>> flattenHierarchyCache = new DefaultSynchronizedCache<>( // max size in our cache is mandatory. OTOH, guava version of flattenHierarchyCache have no maxSize. Integer.MAX_VALUE, DefaultCache.NO_TIMEOUT, // Note Issue: 1615: Originally, flattenHierarchyCache data type was "LoadingCache" from Guava: // https://github.com/google/guava/blob/master/guava/src/com/google/common/eventbus/SubscriberRegistry.java#L219 // CacheLoader used ImmutableSet as return value. Somehow ImmutableSet maintains its order, where // HashSet isn't. This is why we have LinkedHashSet here. key -> new LinkedHashSet<>(TypeToken.getRawTypes(key))); /** * Flattens a class's type hierarchy into a set of {@code Class} objects including all * superclasses (transitively) and all interfaces implemented by these superclasses. */ @VisibleForTesting static Set> flattenHierarchy(final Class concreteClass) { return flattenHierarchyCache.get(concreteClass); } private static final class MethodIdentifier { private final String name; private final List> parameterTypes; MethodIdentifier(final Method method) { this.name = method.getName(); this.parameterTypes = Arrays.asList(method.getParameterTypes()); } @Override public int hashCode() { return Objects.hash(name, parameterTypes); } @Override public boolean equals(@CheckForNull final Object o) { if (o instanceof MethodIdentifier) { final MethodIdentifier ident = (MethodIdentifier) o; return name.equals(ident.name) && parameterTypes.equals(ident.parameterTypes); } return false; } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy