Avalon Framework is the central piece to the entire Avalon project. If you understand the contracts and constructs defined in the framework, you can understand anything that uses it. Remember the principles and patterns we have already discussed so far. In this section, we will expound on how the Role concept works practically, the lifecycle of components, and how the interfaces work.
In Avalon, all components implements a Service, so we need to define the Service Interface and the associated semantic contract.
Below you will find an example interface, followed by some best practices along with their reasoning.
package org.apache.avalon.bizserver.docrepository; /** Interface to store and retrieve arbitrary Documents in a persisted store. * * The DocumentRepository is responsible to store Documents, create a reference * identity and be able to retrieve the same document at any point later in * time using the document identity returned in the <code>storeDocument()</code> * method call. */ public interface DocumentRepository { /** Retrieves a Document from the persisted document store. * * @return Document of the previously stored Document or null if not * found. * @throw SecurityException if the Principal is not authorized to * retrieve that document. */ Document retrieveDocument(Principal requestor, int refId) throws SecurityException; /** Store a Document into the persisted store. * * @return A Unique Document Identity, to be used later to retrieve the * same document. * @throw SecurityException if the Principal is not allowed to store * documents in the persisted store. */ int storeDocument( Principal requestor, Document doc ) throws SecurityException; }
- Do NOT extend the any life cycle interfaces in your service interface.
- Document as much semantic rules (implicit or explicit) as possible. This will limit the possibilities of misunderstanding between the people who define the Service Contract and the implementing developer. Even if that is today the same person, the Service may be re-implemented in the future by someone else. Also, additional clients shouldn't have to understand the implementation, and everything should be 100% clear from the service documentation.
- Do one thing and do it well. A Service should have the simplest interface possible, When your service interface extends several other interfaces, you muddy the contract for this Service. An old American acronym helps define this pattern: Keep It Simple, Stupid (KISS). It's not hard to outsmart yourself -- We do it all the time.
- Only specify the methods that are needed in the Service interface. It is important to realize that the Service interface is used by client code. Within the component, you probably need additional methods between the classes. These methods should be package protected and NOT exposed in the Service interface. This increase understandability and the possibility to exchange the component in the future.
The entire Avalon Framework can be divided into 5 main categories (as is the API): Activity, Service, Configuration, Context, Logger. Each of those categories represents a unique concern area. It is common for a component to implement several interfaces to identify all the concern areas that the component is concerned about. This will allow the component's container to manage each component in a consistent manner.
When a framework implements several interfaces to separate the concerns of the component, there is potential for confusion over the order of method calls. Avalon Framework realizes this, so we developed the contract for lifecycle ordering of events. If your component does not implement the associated Interface, then simply skip to the next that will be called. The lifecycle order makes it easy to setup and prepare the component for use, in each of the lifecycle methods.
The Lifecycle of a component is split into three phases: Initialization, Active Service, and Destruction. Because these phases are sequential, we will discuss them in that order. The Active Service phase is outside the concern of the container, and basically consists of other components calling the methods in the Service interface. In addition, the act of Construction and Finalization is implicit due to the Java language, so they will be skipped. The steps will list the method name, and the required interface. Within each phase, there will be a number of stages identified by method names. Those stages are executed if your component extends the associated interface specified in parenthesis.
This list of stages occurs in this specific order, and occurs only once during the life of the component.
-
enableLogging
[LogEnabled
] -
contextualize
[Contextualizable
] -
compose
[Composeable
] -
service
[Serviceable
] -
configure
[Configurable
] orparameterize
[Parameterizable
] -
initialize
[Initializable
] -
start
[Startable
]
This list of stages occurs in the order specified, and occurs only once during the life of the component.
-
stop
[Startable
] -
dispose
[Disposable
]
In this section, we will cover all the sections alphabetically.
This group of interfaces refers to contracts for the life cycle of
the component. Each method allows the generic
java.lang.Exception
to be thrown in any case of error.
The Disposable
interface is used by any
component that wants a structured way of knowing it is no longer
needed. Once a component is disposed of, it can no longer be used.
In fact, it should be awaiting garbage collection. The interface
only has one method dispose
that has no
parameters.
The contract surrounding this interface is that the
dispose
method is called once and the method
is the last one called during the life of the component. Further
implications include that the component will no longer be used,
and all resources held by this component must be released. In a way,
it is a replacement of finalize()
, which has very
complicated semantics, that doesn't apply to dispose()
.
The Initializable
interface is used by any
component that needs to perform initializations that take information
from other initialization steps. It is more natural to place all
the initialization in the intialize()
method, and only
gather the required pieces in the preceeding lifecycle methods.
The interface only has one method
initialize
that has no parameters.
The contract surrounding this interface is that the
initialize
method is called once and the
method is the last one called during the initialization sequence.
Further implications include that the component is now live, and it
can be used by other components in the system.
The Startable
interface is used by any
component that is constantly running for the duration of its life.
The interface defines two methods: start
and
stop
. Neither method has any parameters.
The contract surrounding this interface is that the
start
method is called once after the
component is fully initialized, and the stop
method is called once before the component is disposed of. Neither
method will be called more than once, and start
will always be called before stop
.
Furthermore, the method must return, it can not go into a endless
loop. From this follows that the component should create a thread
and call the Thread.start()
method inside this
method to ctart the continous processing.
The stop()
method should interrupt the thread in a
safe manner. This should be done by variables and the
Thread.interrupt()
method, slightly depending on how
the thread is operating.
Implications of using this interface require that the
start
and stop
methods be
conducted safely (unlike the Thread.stop
method) and not render the system unstable.
This group of interfaces describes the concern area of configuration.
If there are any problems, such as required
Configuration
elements that are missing, then
the component may throw a ConfigurationException
.
Components that modify their exact behavior based on configurations
must implement this interface to obtain an instance of the
Configuration
object. There is one method
associated with this interface: configure
with
a Configuration
object as the only
parameter.
The contract surrounding this interface is that the
configure
method is called once during the
life of the component. The Configuration
object passed in must not be null.
The Configuration
object is a representation
of a tree of configuration elements that have attributes. In a
way, you can view the configuration object as an overly simplified
DOM. There are too many methods to cover in this document, so
please review the JavaDocs. You can get the
Configuration
object's value as a
String
, int
,
long
, float
, or
boolean
-- all with default values. You
can do the same for attribute values. You may also get child
Configuration
objects.
There is a contract that says that if a
Configuration
object has a value that it
should not have any children, and the corollary is also
true -- if there are any children, there should be no value.
To simplify the code, the method calls getChild()
and getChildren()
will always return Configuration
and never null.
You will notice that you may not get parent
Configuration
objects. This is by design.
To reduce the complexity of the Configuration
system, containers will most likely pass child configuration
objects to child components. The child components should not have
any access to parent configuration values. This approach might
provide a little inconvenience, but the Avalon team opted for
security by design in every instance where there was a tradeoff.
The concept of the Context
in Avalon arose
from the need to provide a mechanism to pass simple objects
(i.e. non-components) from a container to a component.
This has since then grown into a very powerful mechanism in
Avalon Merlin, where you can create your own Context entries,
as well as extend the Context instance itself with your own
implementation.
See the Avalon Merlin documentation for details on how to define
custom context entries.
The Context
interface defines only the
method get
. It has an
Object
for a parameter, and it returns an
object based on that key. The Context
is
populated by the container, and passed to the child component who
only has access to read the Context
.
There is no set contract with the Context
other than it should always be read-only by
the child component. If you extend Avalon's
Context
, please respect that contract. It
is part of the Inversion of Control pattern as well as security by
design. In addition, it is a bad idea to pass a reference to the
container in the Context for the same reason that the Context
should be read-only.
A component that wishes to receive the container's
Context
will implement this interface. It
has one method named contextualize
with the
parameter being the container's Context
object.
The contract surrounding this interface is that the
contextualize
method is called once during the
life of a component, after LogEnabled
but
before any other initialization method.
Every system needs the ability to log events. Avalon Merlin uses a subsystem called Avalon Logging which is capable of logger subsystem plug-ins. Currently, it supports Apache Log4J and the original Avalon LogKit. Expect more systems to follow soon. Also, it is fairly easy to create a new Avalon Logging Plug-In, in case you need to access a custom logging system.
Logging is often done by statically addressing some factory method in the Logging subsystem. Avalon always tries to apply Inversion of Control, which means that the component should not try to figure out how to obtain a Logger instance, it should be given one by the container. The container can then be configured with the most suitable Logging system for the application, without any changes to the components.
Every component that needs a Logger instance implements this
interface. The interface has one method named
enableLogging
and passes Avalon Framework's
Logger
instance to the component.
The contract surrounding this method is that it is called only once during the component's lifecycle before any other initialization step.
The Logger
interface is used to abstract
away the differences in logging systems. It provides only a
client API, and a fairly simple API that is.
This is the core of Avalon Framework. Any interface defined in this concern area will throw ServiceException.
A component that uses other components (what we call a
Dependency ) needs to implement this interface. The
interface has only one method service
with a
ServiceManager
passed in as the only
parameter.
The contract surrounding this interface is that the
service
is called once and only once during
the lifetime of this component.
The ServiceManager
is passed to the components that
implements the Serviceable
interface in the
service
method. The ServiceManager
has
a single method, lookup
, which is used by the
component to locate its dependencies. It does this by passing a
key in the moethod call and the ServiceManager
will
hand back a component reference of the requested Service.
I.e. The component requests a Service, but doesn't need
to be concerned about which implementation, and safely cast the
returned object to the Service Interface.
The component must declare which dependencies it has, and can
explicitly also declare the key that will be used in the
lookup
method call. If no key is defined, the
Service Interface name must be used.