/* * Copyright (C) 2014 The Guava Authors * * Licensed 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 com.google.common.eventbus; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.MoreObjects; import com.google.common.base.Objects; import com.google.common.base.Throwables; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.common.collect.HashMultimap; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterators; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Multimap; import com.google.common.reflect.TypeToken; import com.google.common.util.concurrent.UncheckedExecutionException; import java.lang.reflect.Method; import java.util.Arrays; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CopyOnWriteArraySet; import javax.annotation.Nullable; import javax.annotation.concurrent.GuardedBy; /** * Registry of subscribers to a single event bus. * * @author Colin Decker */ final class SubscriberRegistry { /** * All registered subscribers, indexed by event type. * * <p>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. */ @GuardedBy("lock") private final ConcurrentMap<Class<?>, CopyOnWriteArraySet<Subscriber>> subscribers = Maps.newConcurrentMap(); /** * The event bus this registry belongs to. */ private final EventBus bus; SubscriberRegistry(EventBus bus) { this.bus = checkNotNull(bus); } /** * Registers all subscriber methods on the given listener object. */ void register(Object listener) { Multimap<Class<?>, Subscriber> listenerMethods = findAllSubscribers(listener); for (Map.Entry<Class<?>, Collection<Subscriber>> entry : listenerMethods.asMap().entrySet()) { Class<?> eventType = entry.getKey(); Collection<Subscriber> eventMethodsInListener = entry.getValue(); CopyOnWriteArraySet<Subscriber> eventSubscribers = subscribers.get(eventType); if (eventSubscribers == null) { CopyOnWriteArraySet<Subscriber> newSet = new CopyOnWriteArraySet<Subscriber>(); eventSubscribers = MoreObjects.firstNonNull( subscribers.putIfAbsent(eventType, newSet), newSet); } eventSubscribers.addAll(eventMethodsInListener); } } /** * Unregisters all subscribers on the given listener object. */ void unregister(Object listener) { Multimap<Class<?>, Subscriber> listenerMethods = findAllSubscribers(listener); for (Map.Entry<Class<?>, Collection<Subscriber>> entry : listenerMethods.asMap().entrySet()) { Class<?> eventType = entry.getKey(); Collection<Subscriber> listenerMethodsForType = entry.getValue(); CopyOnWriteArraySet<Subscriber> 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<Subscriber> getSubscribersForTesting(Class<?> eventType) { return MoreObjects.firstNonNull(subscribers.get(eventType), ImmutableSet.<Subscriber>of()); } /** * Gets an iterator representing an immutable snapshot of all subscribers to the given event at * the time this method is called. */ Iterator<Subscriber> getSubscribers(Object event) { ImmutableSet<Class<?>> eventTypes = flattenHierarchy(event.getClass()); List<Iterator<Subscriber>> subscriberIterators = Lists.newArrayListWithCapacity(eventTypes.size()); for (Class<?> eventType : eventTypes) { CopyOnWriteArraySet<Subscriber> 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 LoadingCache<Class<?>, ImmutableList<Method>> subscriberMethodsCache = CacheBuilder.newBuilder() .weakKeys() .build(new CacheLoader<Class<?>, ImmutableList<Method>>() { @Override public ImmutableList<Method> load(Class<?> concreteClass) throws Exception { return getAnnotatedMethodsNotCached(concreteClass); } }); /** * Returns all subscribers for the given listener grouped by the type of event they subscribe to. */ private Multimap<Class<?>, Subscriber> findAllSubscribers(Object listener) { Multimap<Class<?>, Subscriber> methodsInListener = HashMultimap.create(); Class<?> clazz = listener.getClass(); for (Method method : getAnnotatedMethods(clazz)) { Class<?>[] parameterTypes = method.getParameterTypes(); Class<?> eventType = parameterTypes[0]; methodsInListener.put(eventType, Subscriber.create(bus, listener, method)); } return methodsInListener; } private static ImmutableList<Method> getAnnotatedMethods(Class<?> clazz) { return subscriberMethodsCache.getUnchecked(clazz); } private static ImmutableList<Method> getAnnotatedMethodsNotCached(Class<?> clazz) { Set<? extends Class<?>> supertypes = TypeToken.of(clazz).getTypes().rawTypes(); Map<MethodIdentifier, Method> identifiers = Maps.newHashMap(); for (Class<?> supertype : supertypes) { for (Method method : supertype.getDeclaredMethods()) { if (method.isAnnotationPresent(Subscribe.class) && !method.isSynthetic()) { // TODO(user): Should check for a generic parameter type and error out Class<?>[] parameterTypes = method.getParameterTypes(); checkArgument(parameterTypes.length == 1, "Method %s has @Subscribe annotation but has %s parameters." + "Subscriber methods must have exactly 1 parameter.", method, parameterTypes.length); MethodIdentifier ident = new MethodIdentifier(method); if (!identifiers.containsKey(ident)) { identifiers.put(ident, method); } } } } return ImmutableList.copyOf(identifiers.values()); } /** * Global cache of classes to their flattened hierarchy of supertypes. */ private static final LoadingCache<Class<?>, ImmutableSet<Class<?>>> flattenHierarchyCache = CacheBuilder.newBuilder() .weakKeys() .build(new CacheLoader<Class<?>, ImmutableSet<Class<?>>>() { @SuppressWarnings("RedundantTypeArguments") // <Class<?>> is actually needed to compile @Override public ImmutableSet<Class<?>> load(Class<?> concreteClass) { return ImmutableSet.<Class<?>>copyOf( TypeToken.of(concreteClass).getTypes().rawTypes()); } }); /** * 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 ImmutableSet<Class<?>> flattenHierarchy(Class<?> concreteClass) { try { return flattenHierarchyCache.getUnchecked(concreteClass); } catch (UncheckedExecutionException e) { throw Throwables.propagate(e.getCause()); } } private static final class MethodIdentifier { private final String name; private final List<Class<?>> parameterTypes; MethodIdentifier(Method method) { this.name = method.getName(); this.parameterTypes = Arrays.asList(method.getParameterTypes()); } @Override public int hashCode() { return Objects.hashCode(name, parameterTypes); } @Override public boolean equals(@Nullable Object o) { if (o instanceof MethodIdentifier) { MethodIdentifier ident = (MethodIdentifier) o; return name.equals(ident.name) && parameterTypes.equals(ident.parameterTypes); } return false; } } }