View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.shiro.event.support;
20  
21  import org.apache.shiro.event.EventBus;
22  import org.slf4j.Logger;
23  import org.slf4j.LoggerFactory;
24  
25  import java.util.ArrayList;
26  import java.util.Collections;
27  import java.util.HashSet;
28  import java.util.LinkedHashMap;
29  import java.util.List;
30  import java.util.Map;
31  import java.util.Set;
32  import java.util.concurrent.locks.Lock;
33  import java.util.concurrent.locks.ReentrantReadWriteLock;
34  
35  /**
36   * A default event bus implementation that synchronously publishes events to registered listeners.  Listeners can be
37   * registered or unregistered for events as necessary.
38   * <p/>
39   * An event bus enables a publish/subscribe paradigm within Shiro - components can publish or consume events they
40   * find relevant without needing to be tightly coupled to other components.  This affords great
41   * flexibility within Shiro by promoting loose coupling and high cohesion between components and a much safer
42   * pluggable architecture that is more resilient to change over time.
43   * <h2>Sending Events</h2>
44   * If a component wishes to publish events to other components:
45   * <pre>
46   *     MyEvent myEvent = createMyEvent();
47   *     eventBus.publish(myEvent);
48   * </pre>
49   * The event bus will determine the type of event and then dispatch the event to components that wish to receive
50   * events of that type.
51   * <h2>Receiving Events</h2>
52   * A component can receive events of interest by doing the following.
53   * <ol>
54   * <li>For each type of event you wish to consume, create a public method that accepts a single event argument.
55   * The method argument type indicates the type of event to receive.</li>
56   * <li>Annotate each of these public methods with the {@link org.apache.shiro.event.Subscribe Subscribe} annotation.</li>
57   * <li>Register the component with the event bus:
58   * <pre>
59   *         eventBus.register(myComponent);
60   *     </pre>
61   * </li>
62   * </ol>
63   * After registering the component, when when an event of a respective type is published, the component's
64   * {@code Subscribe}-annotated method(s) will be invoked as expected.
65   *
66   * This design (and its constituent helper components) was largely influenced by
67   * Guava's <a href="http://docs.guava-libraries.googlecode.com/git/javadoc/com/google/common/eventbus/EventBus.html">EventBus</a>
68   * concept, although no code was shared/imported (even though Guava is Apache 2.0 licensed and could have
69   * been used).
70   *
71   * This implementation is thread-safe and may be used concurrently.
72   *
73   * @since 1.3
74   */
75  public class DefaultEventBus implements EventBus {
76  
77      private static final Logger log = LoggerFactory.getLogger(DefaultEventBus.class);
78  
79      private static final String EVENT_LISTENER_ERROR_MSG = "Event listener processing failed.  Listeners should " +
80              "generally handle exceptions directly and not propagate to the event bus.";
81  
82      //this is stateless, we can retain a static final reference:
83      private static final EventListenerComparator EVENT_LISTENER_COMPARATOR = new EventListenerComparator();
84  
85      private EventListenerResolver eventListenerResolver;
86  
87      //We want to preserve registration order to deliver events to objects in the order that they are registered
88      //with the event bus.  This has the nice effect that any Shiro system-level components that are registered first
89      //(likely to happen upon startup) have precedence over those registered by end-user components later.
90      //
91      //One might think that this could have been done by just using a ConcurrentSkipListMap (which is available only on
92      //JDK 6 or later).  However, this approach requires the implementation of a Comparator to sort elements, and this
93      //surfaces a problem: for any given random event listener, there isn't any guaranteed property to exist that can be
94      //inspected to determine order of registration, since registration order is an artifact of this EventBus
95      //implementation, not the listeners themselves.
96      //
97      //Therefore, we use a simple concurrent lock to wrap a LinkedHashMap - the LinkedHashMap retains insertion order
98      //and the lock provides thread-safety in probably a much simpler mechanism than attempting to write a
99      //EventBus-specific Comparator.  This technique is also likely to be faster than a ConcurrentSkipListMap, which
100     //is about 3-5 times slower than a standard ConcurrentMap.
101     private final Map<Object, Subscription> registry;
102     private final Lock registryReadLock;
103     private final Lock registryWriteLock;
104 
105     public DefaultEventBus() {
106         this.registry = new LinkedHashMap<Object, Subscription>(); //not thread safe, so we need locks:
107         ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
108         this.registryReadLock = rwl.readLock();
109         this.registryWriteLock = rwl.writeLock();
110         this.eventListenerResolver = new AnnotationEventListenerResolver();
111     }
112 
113     public EventListenerResolver getEventListenerResolver() {
114         return eventListenerResolver;
115     }
116 
117     public void setEventListenerResolver(EventListenerResolver eventListenerResolver) {
118         this.eventListenerResolver = eventListenerResolver;
119     }
120 
121     public void publish(Object event) {
122         if (event == null) {
123             log.info("Received null event for publishing.  Ignoring and returning.");
124             return;
125         }
126 
127         registryReadLock.lock();
128         try {
129             //performing the entire iteration within the lock will be a slow operation if the registry has a lot of
130             //contention.  However, it is expected that the very large majority of cases the registry will be
131             //read-mostly with very little writes (registrations or removals) occurring during a typical application
132             //lifetime.
133             //
134             //The alternative would be to copy the registry.values() collection to a new LinkedHashSet within the lock
135             //only and the iteration on this new collection could be outside the lock.  This has the performance penalty
136             //however of always creating a new collection every time an event is published,  which could be more
137             //costly for the majority of applications, especially if the number of listeners is large.
138             //
139             //Finally, the read lock is re-entrant, so multiple publish calls will be
140             //concurrent without penalty since publishing is a read-only operation on the registry.
141 
142             for (Subscription subscription : this.registry.values()) {
143                 subscription.onEvent(event);
144             }
145         } finally {
146             registryReadLock.unlock();
147         }
148     }
149 
150     public void register(Object instance) {
151         if (instance == null) {
152             log.info("Received null instance for event listener registration.  Ignoring registration request.");
153             return;
154         }
155 
156         unregister(instance);
157 
158         List<EventListener> listeners = getEventListenerResolver().getEventListeners(instance);
159 
160         if (listeners == null || listeners.isEmpty()) {
161             log.warn("Unable to resolve event listeners for subscriber instance [{}]. Ignoring registration request.",
162                     instance);
163             return;
164         }
165 
166         Subscription subscription = new Subscription(listeners);
167 
168         this.registryWriteLock.lock();
169         try {
170             this.registry.put(instance, subscription);
171         } finally {
172             this.registryWriteLock.unlock();
173         }
174     }
175 
176     public void unregister(Object instance) {
177         if (instance == null) {
178             return;
179         }
180         this.registryWriteLock.lock();
181         try {
182             this.registry.remove(instance);
183         } finally {
184             this.registryWriteLock.unlock();
185         }
186     }
187 
188     private class Subscription {
189 
190         private final List<EventListener> listeners;
191 
192         public Subscription(List<EventListener> listeners) {
193             List<EventListener> toSort = new ArrayList<EventListener>(listeners);
194             Collections.sort(toSort, EVENT_LISTENER_COMPARATOR);
195             this.listeners = toSort;
196         }
197 
198         public void onEvent(Object event) {
199 
200             Set<Object> delivered = new HashSet<Object>();
201 
202             for (EventListener listener : this.listeners) {
203                 Object target = listener;
204                 if (listener instanceof SingleArgumentMethodEventListener) {
205                     SingleArgumentMethodEventListener singleArgListener = (SingleArgumentMethodEventListener) listener;
206                     target = singleArgListener.getTarget();
207                 }
208                 if (listener.accepts(event) && !delivered.contains(target)) {
209                     try {
210                         listener.onEvent(event);
211                     } catch (Throwable t) {
212                         log.warn(EVENT_LISTENER_ERROR_MSG, t);
213                     }
214                     delivered.add(target);
215                 }
216             }
217         }
218     }
219 }