View Javadoc
1   /*
2    * Copyright (C) 2014 The Guava Authors
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5    * in compliance with the License. You may obtain a copy of the License at
6    *
7    * http://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software distributed under the License
10   * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11   * or implied. See the License for the specific language governing permissions and limitations under
12   * the License.
13   */
14  
15  package com.google.common.eventbus;
16  
17  import static com.google.common.base.Preconditions.checkArgument;
18  import static com.google.common.base.Preconditions.checkNotNull;
19  
20  import com.google.common.annotations.VisibleForTesting;
21  import com.google.common.base.MoreObjects;
22  import com.google.common.base.Objects;
23  import com.google.common.base.Throwables;
24  import com.google.common.cache.CacheBuilder;
25  import com.google.common.cache.CacheLoader;
26  import com.google.common.cache.LoadingCache;
27  import com.google.common.collect.HashMultimap;
28  import com.google.common.collect.ImmutableList;
29  import com.google.common.collect.ImmutableSet;
30  import com.google.common.collect.Iterators;
31  import com.google.common.collect.Lists;
32  import com.google.common.collect.Maps;
33  import com.google.common.collect.Multimap;
34  import com.google.common.reflect.TypeToken;
35  import com.google.common.util.concurrent.UncheckedExecutionException;
36  import com.google.j2objc.annotations.Weak;
37  import java.lang.reflect.Method;
38  import java.util.Arrays;
39  import java.util.Collection;
40  import java.util.Iterator;
41  import java.util.List;
42  import java.util.Map;
43  import java.util.Set;
44  import java.util.concurrent.ConcurrentMap;
45  import java.util.concurrent.CopyOnWriteArraySet;
46  import javax.annotation.Nullable;
47  
48  /**
49   * Registry of subscribers to a single event bus.
50   *
51   * @author Colin Decker
52   */
53  final class SubscriberRegistry {
54  
55    /**
56     * All registered subscribers, indexed by event type.
57     *
58     * <p>The {@link CopyOnWriteArraySet} values make it easy and relatively lightweight to get an
59     * immutable snapshot of all current subscribers to an event without any locking.
60     */
61    private final ConcurrentMap<Class<?>, CopyOnWriteArraySet<Subscriber>> subscribers =
62        Maps.newConcurrentMap();
63  
64    /**
65     * The event bus this registry belongs to.
66     */
67    @Weak private final EventBus bus;
68  
69    SubscriberRegistry(EventBus bus) {
70      this.bus = checkNotNull(bus);
71    }
72  
73    /**
74     * Registers all subscriber methods on the given listener object.
75     */
76    void register(Object listener) {
77      Multimap<Class<?>, Subscriber> listenerMethods = findAllSubscribers(listener);
78  
79      for (Map.Entry<Class<?>, Collection<Subscriber>> entry : listenerMethods.asMap().entrySet()) {
80        Class<?> eventType = entry.getKey();
81        Collection<Subscriber> eventMethodsInListener = entry.getValue();
82  
83        CopyOnWriteArraySet<Subscriber> eventSubscribers = subscribers.get(eventType);
84  
85        if (eventSubscribers == null) {
86          CopyOnWriteArraySet<Subscriber> newSet = new CopyOnWriteArraySet<>();
87          eventSubscribers =
88              MoreObjects.firstNonNull(subscribers.putIfAbsent(eventType, newSet), newSet);
89        }
90  
91        eventSubscribers.addAll(eventMethodsInListener);
92      }
93    }
94  
95    /**
96     * Unregisters all subscribers on the given listener object.
97     */
98    void unregister(Object listener) {
99      Multimap<Class<?>, Subscriber> listenerMethods = findAllSubscribers(listener);
100 
101     for (Map.Entry<Class<?>, Collection<Subscriber>> entry : listenerMethods.asMap().entrySet()) {
102       Class<?> eventType = entry.getKey();
103       Collection<Subscriber> listenerMethodsForType = entry.getValue();
104 
105       CopyOnWriteArraySet<Subscriber> currentSubscribers = subscribers.get(eventType);
106       if (currentSubscribers == null || !currentSubscribers.removeAll(listenerMethodsForType)) {
107         // if removeAll returns true, all we really know is that at least one subscriber was
108         // removed... however, barring something very strange we can assume that if at least one
109         // subscriber was removed, all subscribers on listener for that event type were... after
110         // all, the definition of subscribers on a particular class is totally static
111         throw new IllegalArgumentException(
112             "missing event subscriber for an annotated method. Is " + listener + " registered?");
113       }
114 
115       // don't try to remove the set if it's empty; that can't be done safely without a lock
116       // anyway, if the set is empty it'll just be wrapping an array of length 0
117     }
118   }
119 
120   @VisibleForTesting
121   Set<Subscriber> getSubscribersForTesting(Class<?> eventType) {
122     return MoreObjects.firstNonNull(subscribers.get(eventType), ImmutableSet.<Subscriber>of());
123   }
124 
125   /**
126    * Gets an iterator representing an immutable snapshot of all subscribers to the given event at
127    * the time this method is called.
128    */
129   Iterator<Subscriber> getSubscribers(Object event) {
130     ImmutableSet<Class<?>> eventTypes = flattenHierarchy(event.getClass());
131 
132     List<Iterator<Subscriber>> subscriberIterators =
133         Lists.newArrayListWithCapacity(eventTypes.size());
134 
135     for (Class<?> eventType : eventTypes) {
136       CopyOnWriteArraySet<Subscriber> eventSubscribers = subscribers.get(eventType);
137       if (eventSubscribers != null) {
138         // eager no-copy snapshot
139         subscriberIterators.add(eventSubscribers.iterator());
140       }
141     }
142 
143     return Iterators.concat(subscriberIterators.iterator());
144   }
145 
146   /**
147    * A thread-safe cache that contains the mapping from each class to all methods in that class and
148    * all super-classes, that are annotated with {@code @Subscribe}. The cache is shared across all
149    * instances of this class; this greatly improves performance if multiple EventBus instances are
150    * created and objects of the same class are registered on all of them.
151    */
152   private static final LoadingCache<Class<?>, ImmutableList<Method>> subscriberMethodsCache =
153       CacheBuilder.newBuilder()
154           .weakKeys()
155           .build(
156               new CacheLoader<Class<?>, ImmutableList<Method>>() {
157                 @Override
158                 public ImmutableList<Method> load(Class<?> concreteClass) throws Exception {
159                   return getAnnotatedMethodsNotCached(concreteClass);
160                 }
161               });
162 
163   /**
164    * Returns all subscribers for the given listener grouped by the type of event they subscribe to.
165    */
166   private Multimap<Class<?>, Subscriber> findAllSubscribers(Object listener) {
167     Multimap<Class<?>, Subscriber> methodsInListener = HashMultimap.create();
168     Class<?> clazz = listener.getClass();
169     for (Method method : getAnnotatedMethods(clazz)) {
170       Class<?>[] parameterTypes = method.getParameterTypes();
171       Class<?> eventType = parameterTypes[0];
172       methodsInListener.put(eventType, Subscriber.create(bus, listener, method));
173     }
174     return methodsInListener;
175   }
176 
177   private static ImmutableList<Method> getAnnotatedMethods(Class<?> clazz) {
178     return subscriberMethodsCache.getUnchecked(clazz);
179   }
180 
181   private static ImmutableList<Method> getAnnotatedMethodsNotCached(Class<?> clazz) {
182     Set<? extends Class<?>> supertypes = TypeToken.of(clazz).getTypes().rawTypes();
183     Map<MethodIdentifier, Method> identifiers = Maps.newHashMap();
184     for (Class<?> supertype : supertypes) {
185       for (Method method : supertype.getDeclaredMethods()) {
186         if (method.isAnnotationPresent(Subscribe.class) && !method.isSynthetic()) {
187           // TODO(cgdecker): Should check for a generic parameter type and error out
188           Class<?>[] parameterTypes = method.getParameterTypes();
189           checkArgument(
190               parameterTypes.length == 1,
191               "Method %s has @Subscribe annotation but has %s parameters."
192                   + "Subscriber methods must have exactly 1 parameter.",
193               method,
194               parameterTypes.length);
195 
196           MethodIdentifier ident = new MethodIdentifier(method);
197           if (!identifiers.containsKey(ident)) {
198             identifiers.put(ident, method);
199           }
200         }
201       }
202     }
203     return ImmutableList.copyOf(identifiers.values());
204   }
205 
206   /**
207    * Global cache of classes to their flattened hierarchy of supertypes.
208    */
209   private static final LoadingCache<Class<?>, ImmutableSet<Class<?>>> flattenHierarchyCache =
210       CacheBuilder.newBuilder()
211           .weakKeys()
212           .build(
213               new CacheLoader<Class<?>, ImmutableSet<Class<?>>>() {
214                 // <Class<?>> is actually needed to compile
215                 @SuppressWarnings("RedundantTypeArguments")
216                 @Override
217                 public ImmutableSet<Class<?>> load(Class<?> concreteClass) {
218                   return ImmutableSet.<Class<?>>copyOf(
219                       TypeToken.of(concreteClass).getTypes().rawTypes());
220                 }
221               });
222 
223   /**
224    * Flattens a class's type hierarchy into a set of {@code Class} objects including all
225    * superclasses (transitively) and all interfaces implemented by these superclasses.
226    */
227   @VisibleForTesting
228   static ImmutableSet<Class<?>> flattenHierarchy(Class<?> concreteClass) {
229     try {
230       return flattenHierarchyCache.getUnchecked(concreteClass);
231     } catch (UncheckedExecutionException e) {
232       throw Throwables.propagate(e.getCause());
233     }
234   }
235 
236   private static final class MethodIdentifier {
237 
238     private final String name;
239     private final List<Class<?>> parameterTypes;
240 
241     MethodIdentifier(Method method) {
242       this.name = method.getName();
243       this.parameterTypes = Arrays.asList(method.getParameterTypes());
244     }
245 
246     @Override
247     public int hashCode() {
248       return Objects.hashCode(name, parameterTypes);
249     }
250 
251     @Override
252     public boolean equals(@Nullable Object o) {
253       if (o instanceof MethodIdentifier) {
254         MethodIdentifier ident = (MethodIdentifier) o;
255         return name.equals(ident.name) && parameterTypes.equals(ident.parameterTypes);
256       }
257       return false;
258     }
259   }
260 }