HiveMind Services |
- Defining Services
- Extending Services
- A short example
- Primitive Service Model
- Singleton Service Model
- Threaded Service Model
- Pooled Service Model
- Service Lifecycle
- Services and Events
- Frequently Asked Questions
In HiveMind, a service is simply an object that implements a particular interface, the service interface. You supply the service interface (packaged as part of a module). You supply the core implementation of the interface (in the same module, or in a different module). At runtime, HiveMind puts it all together.
HiveMind uses four service models: primitive, singleton, threaded and pooled. In the primitive and singleton models, each service will ultimately be just a single object instance. In the threaded and pooled models, there may be many instances simultaneously, one for each thread.
Unlike EJBs, there's no concept of location transparency: services are always local to the same JVM. Unlike XML-based web services, there's no concept of language transparency: services are always expressed in terms of Java interfaces. Unlike JMX or Jini, there's no concept of hot-loading of of services. HiveMind is kept delibrately simple, yet still very powerful, so that your code is kept simple.
Defining Services
A service definition begins with a Java interface, the service interface. Any interface will do, HiveMind doesn't care, and there's no base HiveMind interface.
A module descriptor may include <service-point> elements to define services. A module may contain any number of services.
Each <service-point> establishes an id for the service and defines the interface for the service. An example is provided later in this document.
HiveMind is responsible for supplying the service implementation as needed; in most cases, the service implementation is an additional Java class which implements the service interface. HiveMind will instantiate the class and configure it as needed. The exact timing is determined from the service's service model:
- primitive : the service is constructed on first reference
- singleton : the service is not constructed until a method of the service interface is invoked
- threaded : invoking a service method constructs and binds an instance of the service to the current thread
- pooled : as with threaded, but service implementations are stored in a pool when unbound from a thread for future use in other threads.
Additional service models can be defined via the hivemind.ServiceModels configuration point.
HiveMind uses a system of proxies for most of the service models (all except the primitive service model, which primarily exists to bootstrap the core HiveMind services used by other services). Proxies are objects that implement the service interface and take care of details such as constructing the actual implementation of a service on the fly. These lifecycle issues are kept hidden from your code behind the proxies.
A service definition may include service contributions, or may leave that for another module.
Ultimately, a service will consist of a core implementation (a Java object that implements the service interface) and, optionally, any number of interceptors. Interceptors sit between the core implementation and the client, and add functionality to the core implementation such as logging, security, transaction demarkation or performance monitoring. Interceptors are yet more objects that implement the service interface.
Instantiating the core service implementation, configuring it, and wrapping it with any interceptors is referred to as constructing the service. Typically, a service proxy will be created first. The first time that a service method is invoked on the proxy, the service implementation is instantiated and configured, and any interceptors for the service are created.
Extending Services
Any module may contribute to any service extension point. An <implementation> element contains these contributions. Contributions take three forms:
- Service constructors:
- <create-instance> to instantiate an instance of a Java class as the implementation
- <invoke-factory> to have another service create the implementation
- <interceptor> to add additional logic to a core implementation
Service Constructors
A service constructor is used to instantiate a Java class as the core implementation instance for the service.
There are two forms of service constructors: instance creators and implementation factories.
An instance creator is represented by a <create-instance> element. It includes a class attribute, the Java class to instantiate.
An implementation factory is represented by a <invoke-factory> element. It includes a service-id attribute, the id of a service implementation factory service (which implements the ServiceImplementationFactory interface). The most common example is the hivemind.BuilderFactory service.
Implementation Factories
An implementation factory is used to create a core implementation for a service at runtime.
Often, the factory will need some additional configuration information. For example, the hivemind.lib.EJBProxyFactory service uses its parameters to identify the JNDI name of the EJB's home interface, as well as the home interface class itself.
Parameters to factory services are the XML elements enclosed by the <invoke-factory> element. Much like a configuration contribution, these parameters are converted from XML into Java objects before being provided to the factory.
The most common service factory is hivemind.BuilderFactory. It is used to construct a service and then set properties of the service implementation object.
Interceptor Contributions
An interceptor contribution is represented by an <interceptor> element. The service-id attribute identifies a service interceptor factory service: a service that implements the ServiceInterceptorFactory interface.
An interceptor factory knows how to create an object that implements an arbitrary interface (the interface being defined by the service extension point), adding new functionality. For example, the hivemind.LoggingInterceptor factory creates an instance that logs entry and exit to each method.
The factory shouldn't care what the service interface itself is ... it should adapt to whatever interface is defined by the service extension point it will create an instance for.
A service extension point may have any number of interceptor contributions. If the order in which interceptors are applied is important, then the optional before and after attributes can be specified.
In this example, is was desired that any method logging occur first, before the other interceptors. This ensures that the time taken to log method entry and exit is not included in the performance statistics (gathered by the performance interceptor). To ensure that the logging interceptor is the first, or earliest, interceptor, the special value * (rather than a list of interceptor service ids) is given for its before attribute (within the <interceptor> element). This forces the logging interceptor to the front of the list (however, only a single interceptor may be so designated).
Likewise, the security checks should occur last, after logging and after performance; this is accomplished by setting the after attribute to *. The performance interceptor naturally falls between the two.
This is about as complex as an interceptor stack is likely to grow. However, through the use of explicit dependencies, almost any arraingment of interceptors is possible ... even when different modules contribute the interceptors.
Interceptors implement the toString() method to provide
a useful identification for the interceptor, for example:
<Iterceptor: hivemind.LoggingInterceptor for
com.myco.MyService(com.myco.MyServiceInterface)>
This string identifies the interceptor service factory (hivemind.LoggingInterceptor), the service extension point (com.myco.MyService) and the service interface (com.myco.MyServiceInterface).
A short example
As an example, let's create an interface with a single method, used to add together two numbers.
package com.myco.mypackage; public interface Adder { public int add(int arg1, int arg2); }
We could define many methods, and the methods could throw exceptions. Once more, HiveMind doesn't care.
We need to create a module to contain this service. We'll create a simple HiveMind deployment descriptor. This is an XML file, named hivemodule.xml, that must be included in the module's META-INF directory.
<?xml version="1.0"?> <module id="com.myco.mypackage" version="1.0.0"> <service-point id="Adder" interface="com.myco.mypackage.Adder"/> </module>
The complete id for this service is com.myco.mypackage.Adder , formed from the module id and the service id. Commonly, the service id will exactly match the complete name of the service interface, but this is not required.
Normally, the <service-point> would contain a <create-instance> or <invoke-factory> element, used to create the core implementation. For this example, we'll create a second module that provides the implementation. First we'll define the implementation class.
package com.myco.mypackage.impl; import com.myco.mypackage.Adder; public class AdderImpl implements Adder { public int add(int arg1, int arg2) { return arg1 + arg2; } }
That's what we meant by a POJO. We'll create a second module to provide this implementation.
<?xml version="1.0"?> <module id="com.myco.mypackage.impl" version="1.0.0"> <implementation service-id="com.myco.mypackage.Adder"> <create-instance class="com.myco.mypackage.impl.AdderImpl"/> </implementation> </module>
The runtime code to access the service is very streamlined:
Registry registry = . . . Adder service = (Adder) registry.getService("com.myco.mypackage.Adder", Adder.class); int sum = service.add(4, 7);
Another module may provide an interceptor:
<?xml version="1.0"?> <module id="com.myco.anotherpackage version="1.0.0"> <implementation service-id="com.myco.mypackage.Adder"> <interceptor service-id="hivemind.LoggingInterceptor"> </implementation> </module>
Here the Logging interceptor is applied to the service extension point. The interceptor will be inserted between the client code and the core implementation. The client in the code example won't get an instance of the AdderImpl class, it will get an instance of the interceptor, which internally invokes methods on the AdderImpl instance. Because we code against interfaces instead of implementations, the client code neither knows nor cares about this.
Primitive Service Model
The simplest service model is the primitive service model; in this model the service is constructed on first reference. This is appropriate for services such as service factories and interceptor factories, and for several of the basic services provided in the hivemind module.
Singleton Service Model
Constructing a service can be somewhat expensive; it involves instantiating a core service implementation, configuring its properties (some of which may also be services), and building the stack of interceptors for the service. Although HiveMind encourages you to define your application in terms of a large number of small, simple, testable services, it is also desirable to avoid a cascade of unneccesary object creation due to the dependencies between services.
To resolve this, HiveMind defers the actual creation of services by default. This is controled by the model attribute of the <service-point> element; the default model is singleton.
When a service is first requested a proxy for the service is created. This proxy implements the same service interface as the actual service and, the first time a method of the service interface is invoked, will force the construction of the actual service (with the core service implementation, interceptors, references to other services, and so forth).
In certain cases (including many of the fundamental services provided by HiveMind) this behavior is not desired; in those cases, the primitive service model is specified. In addition, there is rarely a need to defer service implementation or service interceptor factory services.
Threaded Service Model
In general, singleton services (using the singleton or primitive service models) should be sufficient. In some cases, the service may need to keep some specific state. State and multithreading don't mix, so the threaded service model constructs, as needed, a service instance for the current thread. Once constructed, the service instance stays bound to the thread until it is discarded. The particular service implementation is exclusive to the thread and is only accessible from that thread.
The threaded service model uses a special proxy class (fabricated at runtime) to support this behavior; the proxy may be shared between threads but methods invoked on the proxy are redirected to the private service implementation bound to the thread. Binding of a service implementation to a thread occurs automatically, the first time a service method is invoked.
The service instance is discarded when notified to cleanup; this is controlled by the hivemind.ThreadEventNotifier service. If your application has any threaded services, you are responsible for invoking the fireThreadCleanup() method of the service.
A core implementation may implement the Discardable interface. If so, it will receive a notification as the service instance is discarded.
HiveMind includes a servlet filter to take care creating the Registry and managing the ThreadEventNotifier service.
Pooled Service Model
The pooled service model is very similar to the threaded model, in that a service implementation will be exclusively bound to a particular thread (until the thread is cleaned up). Unlike the threaded model, the service is not discarded; instead it is stored into a pool for later reuse with the same or a different thread.
As with the threaded model, all of this binding and unbinding is hidden behind a dynamically fabricated proxy class.
Core service implementations may implement the RegistryShutdownListener interface to receive a callback for final cleanups (as with the singleton and deferred service models).
In addition, a service may implement the PoolManageable interface to receive callbacks specific to the pooled service. The service is notified when it is activated (bound to a thread) and deactivated (unbound from the thread and returned to the pool).
Service Lifecycle
As discussed, the service model determines when a service is instantiated. In many cases, the service needs to know when it has been created (to perform any final initializations) or when the Registry has been shut down.
A core service implementation may also implement the RegistryShutdownListener interface. When a Registry is shutdown, the registryDidShutdown() method is invoked on all services (and many other objects, such as proxies). The order in which these notifications occur is not defined. A service may release any resources it may hold at this time. It should not invoke methods on other service interfaces.
The threaded service model does not register services for Registry shutdown notification; regardless of whether the core service implementation implements the RegistryShutdownListener interface or not. Instead, the core service implementation should implement the Discardable interface, to be informed when a service bound to a thread is discarded.
It is preferred that, whenever possible, services use the singleton service model (the default) and not the primitive model. All the service models (except for the primitive service model) expose a proxy object (implementing the service interface) to client code (included other services). These proxies are aware of when the Registry is shutdown and will throw an exception when a service method is invoked on them.
Services and Events
It is fairly common that some services will produce events and other services will consume events. The use of the hivemind.BuilderFactory to construct a service simplifies this, using the < event-listener> element. The BuilderFactory can register a core service implementation (not the service itself!) as a listener of events produced by some other service.
The producing service must include a matched pair of listener registration methods, i.e., both addFooListener() and removeFooListener. Note that only the implementation class must implement the listener interface; the service interface does not have to extend the listener interface. The core service implementation is registered directly with the producer service, bypassing any interceptors or proxies.
Frequently Asked Questions
-
Why do I pass the interface class to getService()?
This is to add an additional level of error checking and reporting. HiveMind knows, from the module descriptors, the interface provided by the service extension point, but it can't tell if you know that. By passing in the interface you'll cast the returned service to, HiveMind can verify that you won't get a ClassCastException. Instead, it throws an exception with more details (the service extension point id, the actual interface provided, and the interface you passed it).
-
What if no module provides a core implementation of the
service?
HiveMind checks for a service constructor when the registry itself is assembled. If a service extension point has no service constructor, an error is logged (identifying the extension point id). In addition, getService() will throw an ApplicationRuntimeException.
-
What if I need to do some initializations in my service?
If you have additional initializations that can't occur inside your core service implementations constructor (for instance, if the initializations are based on properties set after the service implementation object is instantiated), then your class should use the hivemind.BuilderFactory to invoke an initializer method.
-
What if I don't invoke Registry.cleanupThread()?
Then service implementations bound to the current thread stay bound. When the thread is next used to process a request, the same services, in whatever state they were left in, will be used. This may not be desirable in a servlet or Tapestry application, as some state from a client may be left inside the services, and a different client may be associated with the thread in later executions.
-
What if I want my service to be created early, not just when
needed?
Contribute your service into the hivemind.EagerLoad configuration; this will force HiveMind to instantiate the service on startup. This is often used when developing an application, so that configuration errors are caught early; it may also be useful when a service should be instantiated to listen for events from some other service.