The OSGi EventAdmin
service provides a means to publish and receive events between bundles. This can be used to build applications that dynamically react to changes from external or internal sources.
This chapter will present the OSGi EventAdmin
service and how it can be used to build decoupled applications. The EventAdmin
service is an example of the whiteboard pattern, and therefore provides a means to loosely couple the components together.
The OSGi EventAdmin
service is described in the OSGi Compendium and OSGi Enterprise specifications in Chapter 113, Event Admin Service Specification. It provides a means to use a publish and subscribe mechanism to send
events that may be targeted at a particular topic and may contain an arbitrary number of event properties.
Event topics are text names that are used to identify where the event will be delivered. They are represented with a slash (/
) separating parts of the name, for example, org/osgi/framework/ServiceEvent
or org/osgi/service/log/LogEntry
.
An Event
is an immutable object, initialized from a Dictionary
or Map
, which has a number of properties. These properties can store user-specific data, along with a number of other (potentially null
) standard properties from the EventConstants
class:
BUNDLE_ID
– bundle.id
, the bundle's ID numberBUNDLE_SIGNER
– bundle.signer
, the name of the signer of the bundleBUNDLE_SYMBOLICNAME
– bundle.symbolicName
, the symbolic name of the bundleBUNDLE_VERSION
– bundle.version
, the bundle's Version
numberEXCEPTION
– exception
, a Throwable
object if the event was raised as an errorEXCEPTION_CLASS
– exception.class
, the class name of the exception
object (useful for filtering out types of exceptions such as NullPointerException
)EXCEPTION_MESSAGE
– exception.message
, the message returned from the exception
object if presentMESSAGE
– message
, a human-readable message that is usually not localizedSERVICE_ID
– service.id
, the ID of the service that generated the eventSERVICE_OBJECTCLASS
– service.objectClass
, the class name of the service that generated the event (suitable for matching/filtering)SERVICE_PID
– service.pid
, the persistent identifier of the service that raised the eventTIMESTAMP
– timestamp
, the time that the event was postedThe example in the next few sections will cover the case of components needing to send e-mails. A component may desire to send an e-mail when a particular condition occurs, such as if an error is received when processing an event, or if a user has feedback to submit.
Clearly, the component can be directly linked to an SMTP library such as commons-email
. This requires some configuration in the component such as what the SMTP hostname is, what ports should be used, and whether there are any additional authentication details required.
Cleanly separating the e-mail generator (the component wishing to report an error) from the component that sends the e-mail is desirable, as this provides a loosely coupled system, as described in the Loosely coupled and highly cohesive section in Chapter 7, Designing Modular Applications. The configuration data of the generator component itself does not need to worry about how the e-mail is transmitted, while the component that sends the mail can be configured appropriately (or updated when the e-mail requirements are updated).
Although this can be implemented as an OSGi service, using the EventAdmin
service allows the event to be handed off into the background, thereby not blocking the call site. The mail can also be created using properties in the Event
object so that the clients can add whatever information is required in order to generate the e-mails.
The org.osgi.service.event.Event
object is a standard class in the OSGi framework. It can be constructed from either a Map
or Dictionary
of standard key/value pairs, along with a topic (the intended destination of the event).
As e-mails have an Importance
field, the topic name can be used to distinguish between low, normal, and high priority items with smtp/low
, smtp/normal
, and smtp/high
. The e-mail's content can be stored as event properties using standard RFC 822 header names (To
, From
, and Subject
):
import org.osgi.service.event.Event; … Map<String,String> email = new HashMap<String, String>(); email.put("Subject","Hello World"); email.put("From","[email protected]"); email.put("To","[email protected]"); email.put("Body","Sample email sent via event"); Event event = new Event("smtp/high",email);
When this event is created, a copy of the email
map is made so that subsequent changes to the email
map are not reflected in the event
object.
The EventAdmin
service is used to deliver events synchronously or asynchronously.
Synchronous delivery can be performed with the sendEvent
method. The event may be posted on the same thread that the client code is running on, but will be blocked until the event has been delivered to all the registered listeners. This is useful when events require processing before they are committed, but using this pattern can lead to deadlock in the system. For example, if the posting thread has a lock on a critical section, and the event listener also requires that critical lock, then the listener may never receive that notification. Once a system is deadlocked, then it cannot make further progress.
Asynchronous delivery can be performed with the postEvent
method. The EventAdmin
service enqueues the event and then calls it back with a different thread than the caller. The asynchronous delivery is more performant than the synchronous one, because it needs to use less locking and internal bookkeeping to determine which listeners have seen which events.
Sending an event requires acquiring an EventAdmin
instance followed by the sendEvent
or postEvent
methods. The EventAdmin
instance can be injected via Declarative Services or acquired through the BundleContext
via FrameworkUtil
if necessary (see Chapter 3, Using OSGi Services to Dynamically Wire Applications, for more details on how to acquire services in this way):
EventAdmin eventAdmin = getService(EventAdmin.class); eventAdmin.postEvent(event);
The event
will be queued and delivered to the listeners at some point later. Listeners that subscribe after the event is posted/sent will not see that event, though they will see future events.
Note that there are no guarantees that the message will be sent; unlike an enterprise message queuing system, the message is not persisted to disk or replayed upon restart. It is stored in-memory only, and will be discarded when the JVM shuts down.
Additionally, there is no guarantee that a listener is present at the time an event is sent; it may just be silently discarded. EventAdmin
does not guarantee consistency like a two-phase commit message store.
EventAdmin
is used to manage a set of listeners for particular events. Each event listener implements the EventHandler
interface. However, instead of having an addListener
such as the Observable
class, EventAdmin
looks for services published under the EventHandler
interface. This allows listeners to come and go or be replaced with alternative (or mock) ones. These can be registered with all the usual service properties (such as service.ranking
) or with Declarative Services:
package com.packtpub.e4.advanced.event.mailman; import org.apache.commons.mail.*; import org.osgi.service.event.*; import org.osgi.service.log.*; public class MailSender implements EventHandler { public void handleEvent(Event event) { String topic = event.getTopic(); if (topic.startsWith("smtp/")) { String importance = topic.substring("smtp/".length()); String to = (String) event.getProperty("To"); String from = (String) event.getProperty("From"); String subject = (String) event.getProperty("Subject"); String body = (String) event.getProperty("DATA"); try { Email email = new SimpleEmail(); email.setHostName(hostname); email.setSmtpPort(port); email.setFrom(from); email.addTo(to); email.setSubject(subject); email.addHeader("Importance",importance); email.setMsg(body); email.send(); log(LogService.LOG_INFO, "Message sent to " + to); } catch (EmailException e) { log(LogService.LOG_ERROR, "Error occurred" + e); } } } private void log(int level, String message) { LogService log = this.log; if (log != null) { log.log(level, message); } } }
The code sample uses commons-email
from the Apache project and javax.mail:mail
, both of which are available at Maven Central. Copies of the JARs are also available in the book's GitHub repository at https://github.com/alblue/com.packtpub.e4.advanced/.
When an event is received, if the topic begins with smtp/
, then the event is converted into an Email
object and sent.
The rest of the topic is used as a field for the Importance
value, described in RFC 4021, and can take the values low
, normal
, and high
(this is a hint and is not displayed by all e-mail clients).
The log
method is a simple wrapper around a LogService
, which logs if the service is available and not otherwise.
To allow the handler to receive events, it needs to be registered as a service with the event.topics
service property. The topics can be specified with an exact name (so that different handlers can be used to send smtp/low
and smtp/high
), but it's also possible to use a wildcard at the end of the topic name to pick up all the events (such as smtp/*
).
If registering the handler manually (such as in an Activator
), it would look like the following:
Dictionary<String, String> properties = new Hashtable<String, String>(); properties.put("event.topics","smtp/*"); context.registerService(EventHandler.class, new MailSender(), properties);
However, it would be much cleaner to create a Declarative Services component to register the EventHandler
; this will stay on standby and be created when first used, along with providing an easy way to acquire the LogService
:
<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" name="com.packtpub.e4.advanced.event.mailman.mailman"> <implementation class="com.packtpub.e4.advanced.event.mailman.MailSender"/> <service> <provide interface="org.osgi.service.event.EventHandler"/> </service> <reference bind="setLogService" cardinality="0..1" interface="org.osgi.service.log.LogService" name="LogService" policy="dynamic"/> <property name="event.topics" value="smtp/*"/> </scr:component>
Another benefit of using Declarative Services is the ability to pick up the configuration information from Config Admin to set the various properties on the object, such as the hostname to send mails to and the from address of the sender.
The EventAdmin
service provides a coarse way to filter events through the use of topics. The topic name is a path with segments separated by a slash /
, which can either be exact, or with the last segment as a wildcard, such as smtp/*
, as shown in the previous example.
However, this does not provide a means to filter specific events. If additional filtering is desired (for example, ensuring that only select mails are processed), then this would traditionally be done in the code:
if(to.equals("[email protected]")) { … }
Such hard-coded tests are difficult to debug or modify in the future.
The EventAdmin
service provides a standardized way to filter events before they are delivered to the EventHandler
. This allows more specific filters to be set as a way of reducing the volume of events that are handled for processing.
The filter is set by adding a service property event.filter
, which contains an LDAP style expression. The expression can refer to any of the properties set in the event.
To add a filter equivalent to the previous e-mail filter, set the following to the Declarative Services component when registering the filter:
<property name="event.topics" value="smtp/*"/> <property name="event.filter" value="(Subject=Hello World)"/>
Now only e-mails with a specific subject will be matched. Other LDAP filters can be used including wildcards to match substrings. For example, to match the local domain, (To=*@localhost)
could be used.
Using the event.filter
is usually more efficient than hardcoding the logic in the handleEvent
method, because the LDAP filter is optimized in OSGi and is translated to a highly performant match when the handler is installed.
Secondly, when using Declarative Services, the component is only enabled when a matching event is seen; if events that don't match the filter are received, then the component isn't enabled. This allows the system to start up quickly without needing to enable components until they are first needed.
The EventAdmin
specification ensures that events posted asynchronously will be delivered on a different thread than the one that posts it. It does not guarantee that a particular thread will be used for all the events; nor does it guarantee that for synchronous event delivery, the same thread will be used. Implementations are free to either reuse the delivery thread for synchronous events, or use a different thread, provided that the blocking/waiting synchronization primitives are obeyed.
What the EventAdmin
specification does guarantee is that events delivered to a particular topic are delivered in-order to individual subscribers. This implies that for each topic there is a queue for delivery to subscribers. Some implementations use a single thread to enforce ordered event delivery.
If asynchronous event delivery does not need to be strictly in-order, there is an event.delivery
property that can take the value async.unordered
(or referenced via the constant DELIVERY_ASYNC_UNORDERED
in the EventConstants
type). If the strict ordered requirement is relaxed, the EventAdmin
implementation may be able to take advantage of multiple threads for event delivery.
Apache Felix provides a configuration option that can control the number of threads that are used for the EventAdmin
delivery process. The default size for the Felix EventAdmin
is 10, specified in org.apache.felix.eventadmin.ThreadPoolSize
. The Equinox implementation (as of Eclipse 4.4) does not support this property, nor does it support multiple threads for delivery to event clients.
Another approach to sending e-mails would be to use an e-mail Service
, with a sendEmail
method taking a Dictionary
or Map
object. This would do the same thing as the handleEvent
method in the MailSender
class.
There are a few differences that are worth covering when deciding whether to use events or services:
Event
) and essentially stores arbitrary untyped payloads.com/example/event/bus
will be picked up by listeners for com/example/event/*
as well as com/example/*
or even com/*
. These events could be mapped into other communication technologies such as JSON messages over WebSocket to a browser, or even hooked up to a traditional JMS-based messaging system.There isn't a hard rule as to when it's appropriate to use services or topics. Because topics can be created on the fly, it can be useful to represent events being fired for individual data objects to notify listeners that a change has happened; in fact, this technique is used in E4 to notify view components when the underlying model has changed as well as when the selection has changed.
In general, for broadcast mechanisms and topics that need to be created on the fly, the event mechanisms may be more suitable. For library and functional services, especially where a return value or state is needed, OSGi services may be more appropriate.
The OSGi framework also has a number of events that get fired to notify subscribers when state changes occur in the framework itself. For example, when a bundle transitions through its life cycle states (uninstalled, installed, resolved, starting, active, and stopping), an event will be sent to notify clients of the new value. The events are sent after the transition has occurred.
The framework also has events that represent the system as a whole (to indicate the system has started, packages are refreshed, and for startlevel modifications) as well as certain log message types (info, warning, and error).
Framework events are sent under the org/osgi/framework/FrameworkEvent/
topic prefix and include:
STARTED
PACKAGES_REFRESHED
STARTLEVEL_CHANGED
INFO
WARNING
ERROR
Bundle events are sent under the org/osgi/framework/BundleEvent/
topic prefix and include:
UNINSTALLED
INSTALLED
UNRESOLVED
RESOLVED
STARTED
UPDATED
STOPPED
Service events are sent under the org/osgi/framework/ServiceEvent/
topic prefix and include:
REGISTERED
MODIFIED
UNREGISTERING
For framework events, the following information will be provided:
For bundle events, the following information will be provided:
event
: The BundleEvent
object sent to bundle listenersbundle
: The Bundle
objectbundle.id
: The source's bundle ID as a Long
bundle.signer
: The string or collection of strings containing the distinguished name of the bundle's signers, if signedbundle.symbolicName
: The bundle's symbolic name if setbundle.version
: The bundle version as a Version
For service events, the following information will be provided:
event
: The ServiceEvent
object sent to the service listenersservice
: The ServiceReference
of the serviceservice.id
: The service's ID as a Long
service.objectClass
: The array of strings of the object classes this service is registered againstservice.pid
: The service's persistent identifier as a string or collection of stringsAlong with delivery via EventAdmin
, the framework supports several custom listeners that can be added via the BundleContext
add listener methods:
Note that classes such as ServiceTracker
(covered in Chapter 3, Using OSGi Services to Dynamically Wire Applications) subscribe to service events in order to determine when services come and go, and extender patterns such as Declarative Services listen to bundle events in order to process them when they are installed or removed. Implementing an extender pattern generally looks like the following:
context.addBundleListener(new BundleListener() { public void bundleChanged(BundleEvent event) { int type = event.getType(); Bundle bundle = event.getBundle(); String myHeader = bundle.getHeaders().get("X-MyHeader"); if (type == BundleEvent.STARTED && myHeader != null) { addBundle(bundle); } else if (type == BundleEvent.STOPPED && myHeader != null) { removeBundle(bundle); } } }); for (Bundle bundle : context.getBundles()) { if (bundle.getState() == Bundle.ACTIVE && bundle.getHeaders().get("X-MyHeader") != null) { addBundle(bundle); } }
Note that the pattern is generally to add the bundle listener first, and then iterate through all the existing bundles. This way, there may be some duplicate calls to the addBundle
method, but the bundles should not be missed. Generally, the extender pattern will maintain a list of bundles under management so that when the extender provider is stopped, the bundles can be appropriately released.
Also note that most patterns use the existence of a header in the manifest to determine whether or not the bundle should be extended. This allows for a cheap test to determine if the bundle has any contents, and also why headers such as Service-Component
exist in the Declarative Services specification.