Gaderian Services
In Gaderian, a service is simply an object that implements a particular
interface, the service interface (see this note). 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, Gaderian puts it all together.
Gaderian defines 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 services. Gaderian is kept deliberately 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, Gaderian doesn't care, and there's no
base Gaderian 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.
Gaderian 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. Gaderian will instantiate
the class and configure it as needed. The exact timing is determined
by the service implementation'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 gaderian.ServiceModels
configuration point.
Gaderian uses a system of proxies for most of the service
models (all except the primitive service model, which primarily exists
to bootstrap the core Gaderian 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 demarcation 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:
- <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 gaderian.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 gaderian.utilitiesEJBProxyFactory
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 excluding any <assembly> elements. Much like a configuration contribution,
these parameters are converted from XML into Java objects before being provided to the
factory.
Enclosed <assembly> elements represent assembly instructions with which the core
service implementation object created by the service implementation factory may be
initialized. This initilization encompasses dependency injection, event listener registration,
and invoking any initialization method. Assembly
instructions are documented separately.
The most common service factory is gaderian.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 gaderian.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 interceptor 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. The value of the before and after
attributes must be a comm-separated list of interceptor names (name defaults to the
service factory id which created the interceptor if none is provided).
![[A Stack of Interceptors]](images/InterceptorStack.png)
In this example, it 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
arrangement 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: gaderian.LoggingInterceptor for
com.myco.MyService(com.myco.MyServiceInterface)>
This string identifies the interceptor service factory
(gaderian.LoggingInterceptor), the service extension point
(com.myco.MyService) and the service interface
(com.myco.MyServiceInterface).
Warning
If
toString()
is part of the service interface (really,
a very rare case), then the interceptor
does not
override the
service implementation's method. However, this is not a recommended practice.
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, Gaderian doesn't care.
We need to create a module to contain this service. We'll create a
simple Gaderian deployment descriptor. This is an XML file, named
module.xml, that must be included in the module's META-INF/org/ops4j/gaderian
directory.
<?xml version="1.0"?>
<module id="com.myco.mypackage" version="1.0.0">
<service-point id="Adder" interface="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="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="gaderian.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.
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 Gaderian 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 unnecessary object
creation due to the dependencies between services.
To resolve this, Gaderian defers the actual creation of services by
default. This is controled by the model attribute of the
<create-instance> or <invoke-factory>
elements; 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 Gaderian) 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.
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 gaderian module.
There is rarely a need to use this service model in your own code. It exists primarily to support the
services in the Gaderian module itself (the implementation of the singleton service model is dependent on
several services that use the primitive service model).
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 gaderian.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.
Gaderian 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 gaderian.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.
Bean Services
In Gaderian 1.0, you always must have a service interface. Starting in Gaderian 1.1, the
interface attribute of the <service-point> descriptor element can be a simple class as well.
The class you specify must have a public, no-arguments constructor. Your service constructor
(<create-instance> or <invoke-factory>) should create an instance of the specified interface class (or a subclass).
This represents a "quick and dirty" approach to Gaderian. Some features of Gaderian, particularily
specialized service implementation factories, may not be compatible with bean services ... they are designed
for interface services.
Frequently Asked Questions
- Why do I pass the interface class to getService()?
This is to add an additional level of error checking and reporting.
Gaderian 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,
Gaderian 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?Gaderian 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 implementation's constructor (for instance, if the
initializations are based on properties set after the service
implementation object is instantiated), then your class should use
the gaderian.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 gaderian.EagerLoad
configuration; this will force Gaderian 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.