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

org.apache.shiro.event.support.DefaultEventBus Maven / Gradle / Ivy

There is a newer version: 2.0.2
Show newest version
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF 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.apache.shiro.event.support;

import org.apache.shiro.event.EventBus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * A default event bus implementation that synchronously publishes events to registered listeners.  Listeners can be
 * registered or unregistered for events as necessary.
 * 

* An event bus enables a publish/subscribe paradigm within Shiro - components can publish or consume events they * find relevant without needing to be tightly coupled to other components. This affords great * flexibility within Shiro by promoting loose coupling and high cohesion between components and a much safer * pluggable architecture that is more resilient to change over time. *

Sending Events

* If a component wishes to publish events to other components: *
 *     MyEvent myEvent = createMyEvent();
 *     eventBus.publish(myEvent);
 * 
* The event bus will determine the type of event and then dispatch the event to components that wish to receive * events of that type. *

Receiving Events

* A component can receive events of interest by doing the following. *
    *
  1. For each type of event you wish to consume, create a public method that accepts a single event argument. * The method argument type indicates the type of event to receive.
  2. *
  3. Annotate each of these public methods with the {@link org.apache.shiro.event.Subscribe Subscribe} annotation.
  4. *
  5. Register the component with the event bus: *
     *         eventBus.register(myComponent);
     *     
    *
  6. *
* After registering the component, when when an event of a respective type is published, the component's * {@code Subscribe}-annotated method(s) will be invoked as expected. * * This design (and its constituent helper components) was largely influenced by * Guava's EventBus * concept, although no code was shared/imported (even though Guava is Apache 2.0 licensed and could have * been used). * * This implementation is thread-safe and may be used concurrently. * * @since 1.3 */ public class DefaultEventBus implements EventBus { private static final Logger log = LoggerFactory.getLogger(DefaultEventBus.class); private static final String EVENT_LISTENER_ERROR_MSG = "Event listener processing failed. Listeners should " + "generally handle exceptions directly and not propagate to the event bus."; //this is stateless, we can retain a static final reference: private static final EventListenerComparator EVENT_LISTENER_COMPARATOR = new EventListenerComparator(); private EventListenerResolver eventListenerResolver; //We want to preserve registration order to deliver events to objects in the order that they are registered //with the event bus. This has the nice effect that any Shiro system-level components that are registered first //(likely to happen upon startup) have precedence over those registered by end-user components later. // //One might think that this could have been done by just using a ConcurrentSkipListMap (which is available only on //JDK 6 or later). However, this approach requires the implementation of a Comparator to sort elements, and this //surfaces a problem: for any given random event listener, there isn't any guaranteed property to exist that can be //inspected to determine order of registration, since registration order is an artifact of this EventBus //implementation, not the listeners themselves. // //Therefore, we use a simple concurrent lock to wrap a LinkedHashMap - the LinkedHashMap retains insertion order //and the lock provides thread-safety in probably a much simpler mechanism than attempting to write a //EventBus-specific Comparator. This technique is also likely to be faster than a ConcurrentSkipListMap, which //is about 3-5 times slower than a standard ConcurrentMap. private final Map registry; private final Lock registryReadLock; private final Lock registryWriteLock; public DefaultEventBus() { this.registry = new LinkedHashMap(); //not thread safe, so we need locks: ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); this.registryReadLock = rwl.readLock(); this.registryWriteLock = rwl.writeLock(); this.eventListenerResolver = new AnnotationEventListenerResolver(); } public EventListenerResolver getEventListenerResolver() { return eventListenerResolver; } public void setEventListenerResolver(EventListenerResolver eventListenerResolver) { this.eventListenerResolver = eventListenerResolver; } public void publish(Object event) { if (event == null) { log.info("Received null event for publishing. Ignoring and returning."); return; } registryReadLock.lock(); try { //performing the entire iteration within the lock will be a slow operation if the registry has a lot of //contention. However, it is expected that the very large majority of cases the registry will be //read-mostly with very little writes (registrations or removals) occurring during a typical application //lifetime. // //The alternative would be to copy the registry.values() collection to a new LinkedHashSet within the lock //only and the iteration on this new collection could be outside the lock. This has the performance penalty //however of always creating a new collection every time an event is published, which could be more //costly for the majority of applications, especially if the number of listeners is large. // //Finally, the read lock is re-entrant, so multiple publish calls will be //concurrent without penalty since publishing is a read-only operation on the registry. for (Subscription subscription : this.registry.values()) { subscription.onEvent(event); } } finally { registryReadLock.unlock(); } } public void register(Object instance) { if (instance == null) { log.info("Received null instance for event listener registration. Ignoring registration request."); return; } unregister(instance); List listeners = getEventListenerResolver().getEventListeners(instance); if (listeners == null || listeners.isEmpty()) { log.warn("Unable to resolve event listeners for subscriber instance [{}]. Ignoring registration request.", instance); return; } Subscription subscription = new Subscription(listeners); this.registryWriteLock.lock(); try { this.registry.put(instance, subscription); } finally { this.registryWriteLock.unlock(); } } public void unregister(Object instance) { if (instance == null) { return; } this.registryWriteLock.lock(); try { this.registry.remove(instance); } finally { this.registryWriteLock.unlock(); } } private class Subscription { private final List listeners; public Subscription(List listeners) { List toSort = new ArrayList(listeners); Collections.sort(toSort, EVENT_LISTENER_COMPARATOR); this.listeners = toSort; } public void onEvent(Object event) { Set delivered = new HashSet(); for (EventListener listener : this.listeners) { Object target = listener; if (listener instanceof SingleArgumentMethodEventListener) { SingleArgumentMethodEventListener singleArgListener = (SingleArgumentMethodEventListener) listener; target = singleArgListener.getTarget(); } if (listener.accepts(event) && !delivered.contains(target)) { try { listener.onEvent(event); } catch (Throwable t) { log.warn(EVENT_LISTENER_ERROR_MSG, t); } delivered.add(target); } } } } }