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 EventListenerComparatorventListenerComparator">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 SingleArgumentMethodEventListenersupport/SingleArgumentMethodEventListener.html#SingleArgumentMethodEventListener">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 }