blob: 218dcc0e9aac7ba3ac93e7b40dfdfe34a509a3f9 [file] [log] [blame]
----
Tapestry IoC Services
----
Tapestry IoC Services
Services consist of two main parts: a service interface and a service implementation.
The service interface is how the service will be represented throughout the rest of the
registry. Since what gets passed around is normally a proxy, you can't expect to cast a service
object down to the implementation class (you'll see a ClassCastException instead). In other
words, you should be careful to ensure that your service interface is complete, since
Tapestry IoC effectively walls you off from backdoors such as casts.
Service Life-cycle
Every service has a very specific life-cycle.
* Defined: The service has a definition (from some module) but has not yet been referenced.
* Virtual: The service has been referenced, so a proxy for the class has been created.
* Realized: A method on the proxy has been invoked, so the service implementation has
been instantiated, and any decorators applied.
* Shutdown: The entire Registry has been shut down and with it, all the proxies have been disabled.
[]
When the Registry is first created, all modules are scanned and the definitions for all services
are created.
Services will be referenced by either accessing them using the Registry, or as dependencies
of other realized services.
Tapestry IoC waits until the last possible moment to <realize> the service: that's defined
as when a method of the service is invoked. Tapestry is <thread-safe>, so even in a heavily
contested, highly threaded envrionment (such as a servlet container or application server)
things <Just Work>.
Service Builder Methods
Tapestry doesn't know how to instantiate and configure your service; instead it relies
on you to provide the code to do so, in a service builder method:
+-----------------------------------------------------------------------------------+
package org.example.myapp.services;
public class MyAppModule
{
public static Indexer build()
{
return new IndexerImpl();
}
}
+-----------------------------------------------------------------------------------+
Here the service interface is Indexer (presumably inside the org.example.myapp.services package,
since there isn't an import). Tapestry IoC doesn't know about the IndexerImpl class (the
service implementation of the Indexer service), but it does know
about the build() method.
That's one of the great innovations of Tapestry IoC: we don't try to encapsulate in XML or annotations
all the different ways possible to create a service; those things are best expressed in Java code.
For a simple case (as here), it would be hard for external configuration (again, in XML or Java annotations)
to be shorter than "new IndexerImpl()".
<The above paragraph was written before Binding and Autobuilding were introduced.>
For more complex and realistic scenarios, such as injecting dependencies via the constructor, or
doing more interest work (such as registering the newly created service for events published by some other service),
the Java code is simply the most direct, flexible, extensible and readable approach.
Binding and Autobuilding
Tapestry IoC can also <autobuild> your service. Autobuilding is the <preferred> way to
instantiate your services.
Every module may have an optional, static bind() method which is passed a
{{{../apidocs/org/apache/tapestry5/ioc/ServiceBinder.html}ServiceBinder}}. Services may be registered with
the container by "binding" a service interface to a service implementation:
+------+
package org.example.myapp.services;
import org.apache.tapestry5.ioc.ServiceBinder;
public class MyAppModule
{
public static void bind(ServiceBinder binder)
{
binder.bind(Indexer.class, IndexerImpl.class);
}
}
+----+
You can make repeated calls to ServiceBinder.bind(), to bind additional services.
You might ask, "which is better, a builder method for each service, or a bind() method for the module?" For simple services,
those that are just an instantiated instance with simple dependencies, binding is better than building. That covers
at least 90% of all services, so bind away!
There are many cases, however, where constructing a service is more than just instantiating a class. Often the new service
will (for example) be registered as a listener with some other service. In other cases, the implementation of the
service is generated at runtime. These are where the service builder methods are most useful.
In terms of the evolution of the framework, service builder methods came first, and autobuilding was a later
addition, inspired by the terseness of the {{{http://code.google.com/p/google-guice/}Guice}} IoC container.
Service Ids
Every service will have a unique service id.
When using a service builder method, the service id is the <simple name> of the service interface.
This can be overridden by adding the service id to the method name, after "build", for example:
+---+
public static Indexer buildFileSystemIndexer(@InjectService("FileSystem") FileSystem fileSystem)
{
. . .
}
+----+
Here, the service id is "FileSystemIndexer" not "Indexer".
For autobuilt services, the service id can be specified when the service is bound:
+---+
binder.bind(Indexer.class, IndexerImpl.class).withId("FileSystemIndexer");
+---+
{Injecting Dependencies}
It's pretty unlikely that your service will be able to operate in a total vacuum. It will
have other dependencies.
Dependencies are provided to a service in one of three ways:
* As parameters to the service builder method
* As parameters to the service implementation class' constructor (for autobuilt services)
* As parameters passed to the constructor of the service's module builder (cached inside instance variables)
[]
For example, let's say the Indexer needs a JobScheduler to control
when it executes, and a FileSystem to access files and store indexes.
+-----------------------------------------------------------------------------------+
public static Indexer build(JobScheduler scheduler, FileSystem fileSystem)
{
IndexerImpl indexer = new IndexerImpl(fileSystem);
scheduler.scheduleDailyJob(indexer);
return indexer;
}
+-----------------------------------------------------------------------------------+
Here we've annotated the parameters of the service builder method to identify what
service to inject for that parameter.
This is an example of when you would want to use the service builder method, rather than
just binding the service interface to the implementation class: because we want to do something extra,
in this case, register the new indexer service with the scheduler.
Note that we don't invoke those service builder methods ... we just "advertise" that we need
the named services. Tapestry IoC will provide the necessary proxies and, when we start to
invoke methods on those proxies, will ensure that the full service, including its
interceptors and its dependencies, are ready to go. Again, this is done in a
thread-safe manner.
What happens if there is more than one service that implements the JobScheduler interface, or
the FileSystem interface? You'll see a runtime exception, because Tapestry is unable to resolve
it down to a <single> service. At this point, it is necessary to <disambiguate> the link between
the service interface and <one> service. One approach is to use
the
{{{../apidocs/org/apache/tapestry5/ioc/annotations/InjectService.html}InjectService}} annotation:
+-----------------------------------------------------------------------------------+
public static Indexer build(@InjectService("JobScheduler")
JobScheduler scheduler,
@InjectService("FileSystem")
FileSystem fileSystem)
{
IndexerImpl indexer = new IndexerImpl(fileSystem);
scheduler.scheduleDailyJob(indexer);
return indexer;
}
+-----------------------------------------------------------------------------------+
If you find yourself injecting the same dependencies into multiple service builder
(or service decorator) methods, you can
{{{module.html#Caching Services}cache dependency injections}} in your module, by defining
a constructor. This reduces duplication in your module.
Disambiguation with Marker Annotations
In the previous example we were faced with a problem: multiple versions of the JobScheduler
service. They had the same service interface but unique service ids. If you try to inject
based on type, the service to inject will be ambiguous. Tapestry will throw an exception (identifying
the parameter type and the matching services that implement that type).
The problem is that when injecting a JobScheduler into some other service we need to know
which <one> to inject. Rather than using the service id, another approach is to
use a <marker annotation>.
You may optionally link a service implementation with a marker annotation.
For example, maybe you have one JobScheduler implementation where the jobs are spread across
a number of nodes in a cluster, and you have another JobScheduler where the jobs are all executed exclusively
in the current process.
We can associate those two JobSchedulers with two annotations.
+----+
@Target(
{ PARAMETER, FIELD })
@Retention(RUNTIME)
@Documented
public @interface Clustered
{
}
@Target(
{ PARAMETER, FIELD })
@Retention(RUNTIME)
@Documented
public @interface InProcess
{
}
public class MyModule
{
public static void bind(ServiceBinder binder)
{
binder.bind(JobScheduler.class, ClusteredJobSchedulerImpl.class).withId("ClusteredJobScheduler").withMarker(Clustered.class);
binder.bind(JobScheduler.class, SimpleJobSchedulerImpl.class).withId("InProcessJobScheduler").withMarker(InProcess.class);
}
}
+---+
Notice that the marker annotations have no attributes. Further, we support markers on fields
(for use in Tapestry components) as well as parameters.
To get the right version of the service, you use one of the annotations:
+---+
public class MyServiceImpl implements MyService
{
private final JobScheduler jobScheduler;
public MyServiceImpl(@Clustered JobScheduler jobScheduler)
{
this.jobScheduler = jobScheduler;
}
. . .
}
+---+
The @Clustered annotation on the parameter is combined with the parameter type (JobScheduler) to find the exact
service implementation.
Why is this better than using the service id? It's more refactoring-safe. Service ids can change, which can break
your services. However, using an IDE to rename or move an annotation class or service interface
will be able to update all the uses of the annotation or interface.
With a service builder method, you use the
{{{../apidocs/org/apache/tapestry5/ioc/annotations/Marker.html}@Marker}} annotation:
+---+
@Marker(Clustered.class)
public JobScheduler buildClusteredJobScheduler()
{
return . . .;
}
+---+
The @Marker annotation may also be placed on an implementation class, which means that you may omit
the call to withMarker() inside the bind() method.
Injecting Dependencies for Autobuilt Services
With autobuilt services, there's no service builder method in which to specify injections.
Instead, the injections occur on <constructor> for the implementation class:
+---+
package org.example.myapp.services;
import org.apache.tapestry5.ioc.annotations.InjectService;
public class IndexerImpl implements Indexer
{
private final FileSystem fileSystem;
public IndexerImpl(@InjectService("FileSystem") FileSystem fileSystem)
{
this.fileSystem = fileSystem;
}
. . .
}
+---+
If the class has multiple constructors, the constructor with the <most> parameters will be invoked.
Alternately, you may mark a single constructor with the Inject annotation, and Tapestry will use
<that> constructor specifically, ignoring all other constructors.
Note how we are using final fields for our dependencies; this is generally a Good Idea.
These services will often execute inside a multi-threaded environment, such as a web application,
and the use of final fields inside a constructor ensures that the fields will be properly published
(meaning, "visible to other threads") in accordance with the Java Memory Model.
Once thing that is not a good idea is to pass in another service, such as JobScheduler in the previous
example, and pass <<<this>>> from a constructor:
+---+
package org.example.myapp.services;
import org.apache.tapestry5.ioc.annotations.InjectService;
public class IndexerImpl implements Indexer
{
private final FileSystem fileSystem;
public IndexerImpl(@InjectService("FileSystem") FileSystem fileSystem,
@InjectService("JobScheduler") JobScheduler scheduler)
{
this.fileSystem = fileSystem;
scheduler.scheduleDailyJob(this); // Bad Idea
}
. . .
}
+---+
Understanding why this is a bad idea involves a long detour into inner details of the Java Memory Model.
The short form is that other threads may end up invoking methods on the IndexerImpl instance, and its fields
(even though they are final, even though they appear to already have been set) may be uninitialized.
Defining Service Scope
Each service has a <lifecycle> that controls when the service implementation is instantiated.
There are two built in lifecycles: "singleton" and "perthread", but
more can be added.
Service lifecycle is specified using the
{{{../apidocs/org/apache/tapestry5/ioc/annotations/Scope.html}@Scope annotation}},
which is attached to a builder method. When this annotation is not present, the
default scope, "singleton" is used.
* singleton
Most services use the default scope, "singleton". With this scope a <proxy>
is created when the service is first referenced. By reference, we mean any situation in which
the service is requested by name, such as using the @InjectService annotation on a
service builder method, or by using the
{{{../apidocs/org/apache/tapestry5/ioc/Registry.html}Registry}} API from outside the
container.
In any case, the service proxy will only create the service implementation when a method
on the service interface is invoked. Until then, the service can be thought of as "virtual".
As the first method is invoked, the service builder method is invoked, then any service
decorations occur. This construction process, called "realization", occurs only once.
You should be aware when writing services that your code must be thread safe; any service
you define could be invoked simulataneously by multiple threads. This is rarely an issue
in practice, since most services take input, use local variables, and invoke methods on other services,
without making use of non-final instance variables. The few instance variables
in a service implementation are usually references to other Tapestry IoC services.
* perthread
The perthread service scope exists primarily to help multi-threaded servlet applications,
though it has other applications.
With perthread, the service proxy will delegate to a local service instance that is associated
with the current thread. Two different threads, invoking methods on the same proxy, will
ultimately be invoking methods on two different service instances, each reserved to their own thread.
This is useful when a service needs to keep request specific state, such as information extracted
from the HttpServletRequest (in a web application). The default singleton model would not work
in such a multi threaded environment. Using perthread on select services allows state to be isolated
to those services. Because the dispatch occurs <inside> the proxy, you can treat the service
as a global, like any other.
You will see that your service builder method is invoked more than once. It is invoked in each
thread where the perthread service is used.
At the end of the request, the Registry's cleanupThread() method is invoked; it will discard
any perthread service implementations for the current thread.
<<Caution:>> A common technique in Tapestry IoC is to have a service builder method
register a core service implementation as an event listener with some event hub service.
With non-singleton objects, this can cause a number of problems; the event hub will
hold a reference to the per-thread instance, even after that per-thread instance has been
cleaned up (discarded by the inner proxy). Simply put, this is a pattern to avoid. For
the most part, perthread services should be simple holders of data specific to a thread or
a request, and should not have overly complex relationships with the other services
in the registry.
Defining the scope of Autobuilt Services
There are two options for defining the scope for an autobuilt service.
The service implementation class may include the @Scope annotation. This is generally the preferred way
to specify scope.
In addition, it is possible to specify the scope when binding the service:
+----+
bind(MyServiceInterface.class, MyServiceImpl.class).scope("perthread");
+----+
Eager Loading Services
Services are normally created only as needed (per the scope discussion above).
This can be tweaked slightly; by adding the
{{{../apidocs/org/apache/tapestry5/ioc/annotations/EagerLoad.html}EagerLoad}} annotation to
the service builder method, Tapestry will instantiate the service when the Registry is first created.
This will cause the service builder method to be invoked, as well as any service decorator methods.
This feature is used when a service manages a resource, such as a thread, that needs to be created
as soon as the application starts up. Another common example is a service that listens for events produced
by a second service; the first service may need to be created, and start listening, before any of its
service methods are invoked (which would normally trigger the instantiation of the service).
Many services may be annotated with @EagerLoad; the order in which services are created is not defined.
With the perthread lifecycle, the service builder method will not be invoked (this won't happen until
a service method is invoked), but the decorators for
the service will be created.
Eager Loading Autobuilt Services
As with service scope, there are two options for indicating that an autobuilt service should be
eagerly loaded.
The service implementation class may include the @EagerLoad annotation.
You may also specify eager loading explicitly when binding the service:
+----+
bind(MyServiceInterface.class, MyServiceImpl.class).eagerLoad();
+----+
Injecting Resources
In addition to injecting services, Tapestry will key off of the parameter type to allow
other things to be injected.
* java.lang.String: unique id for the service
* {{{http://www.slf4j.org/api/org/slf4j/Logger.html}org.slf4j.Logger}}: logger for the service
* java.lang.Class: service interface implemented by the service to be constructed
* {{{../apidocs/org/apache/tapestry5/ioc/ServiceResources.html}ServiceResources}}: access to other services
[]
No annotation is needed for these cases.
See also {{{configuration.html}service configuration}} for additional special cases
of resources that can be injected.
Example:
+-----------------------------------------------------------------------------------+
public static Indexer build(String serviceId, Log serviceLog,
JobScheduler scheduler, FileSystem fileSystem)
{
IndexerImpl indexer = new IndexerImpl(serviceLog, fileSystem);
scheduler.scheduleDailyJob(serviceId, indexer);
return indexer;
}
+-----------------------------------------------------------------------------------+
The order of parameters is completely irrelevant. They can come first or last or be
interspersed however you like.
Injecting in the ServiceResources can be handy when you want to calculate the name
of a service dependency on the fly. However, in the general case (where the
id of service dependencies is known at build time), it is easier
to use the @InjectService annotation.
The Log's name (used when configuring logging settings for the service) consists of
the module class name and the service id seperated by a period, i.e. "org.example.myapp.MyModule.Indexer".
Further, ServiceResources includes an autobuild() method that allows you to easily trigger
the construction of a class, including dependencies. Thus the previos example could be rewritten as:
+-----------------------------------------------------------------------------------+
public static Indexer build(ServiceResources resources, JobScheduler jobScheduler)
{
IndexerImpl indexer = resources.autobuild(IndexerImpl.class);
scheduler.scheduleDailyJob(resources.getServiceId(), indexer);
return indexer;
}
+-----------------------------------------------------------------------------------+
This works the exact same way with autobuilt services, except that the parameters of the service
implementation constructor are considered, rather than the parameters of the service
builder method.
The @InjectService annotation takes precendence over these resources.
If the @InjectService annotation is not present, and the parameter type does not exactly match
a resource type, then {{{provider.html}object injection}} occurs. Object injection will find the correct
object to inject based on a number of (extensible) factors, including the parameter type and any additional annotations
on the parameter.
Every once and a while, you'll have a conflict between a resource type and an object injection. For example,
the following does not work as expected:
+-----------------------------------------------------------------------------------+
public static Indexer build(String serviceId, Log serviceLog,
JobScheduler scheduler, FileSystem fileSystem,
@Value("${index-alerts-email}")
String alertEmail)
{
IndexerImpl indexer = new IndexerImpl(serviceLog, fileSystem, alertEmail);
scheduler.scheduleDailyJob(serviceId, indexer);
return indexer;
}
+-----------------------------------------------------------------------------------+
It doesn't work because type String always gets the service id, as a resource (as with the serviceId parameter).
In order to get this to work, we need to turn off the resource injection for the alertEmail parameter.
That's what the {{{../apidocs/org/apache/tapestry5/ioc/annotations/Inject.html}Inject}} annotation does:
+-----------------------------------------------------------------------------------+
public static Indexer build(String serviceId, Log serviceLog,
JobScheduler scheduler, FileSystem fileSystem,
@Inject @Value("${index-alerts-email}")
String alertEmail)
{
IndexerImpl indexer = new IndexerImpl(serviceLog, fileSystem, alertEmail);
scheduler.scheduleDailyJob(serviceId, indexer);
return indexer;
}
+-----------------------------------------------------------------------------------+
Here, the alertEmail parameter will recieve the configured alerts email (see
{{{symbols.html}the symbols documentation}} for more about this syntax) rather than the service id.
Builtin Services
A few services within the Tapestry IOC Module are "builtin"; there is no
service builder method
in the
{{{../apidocs/org/apache/tapestry5/ioc/services/TapestryIOCModule.html}TapestryIOCModule}} class.
*---------------------+-----------------------------------------------------------------------------------------+
| <<Service Id>> | <<Service Interface>> |
*---------------------+-----------------------------------------------------------------------------------------+
| ClassFactory | {{{../apidocs/org/apache/tapestry5/ioc/services/ClassFactory.html}ClassFactory}} |
*---------------------+-----------------------------------------------------------------------------------------+
| LoggerSource | {{{../apidocs/org/apache/tapestry5/ioc/LoggerSource.html}LoggerSource}} |
*---------------------+-----------------------------------------------------------------------------------------+
| RegistryShutdownHub | {{{../apidocs/org/apache/tapestry5/ioc/RegistryShutdownHub.html}RegistryShutdownHub}} |
*---------------------+-----------------------------------------------------------------------------------------+
| PerthreadManager | {{{../apidocs/org/apache/tapestry5/ioc/services/PerthreadManager.html}PerthreadManager}} |
*---------------------+-----------------------------------------------------------------------------------------+
Consult the JavaDoc for each of these services to identify under what circumstances you'll need to use them.
Mutually Dependant Services
One of the benefits of Tapestry IoC's proxy-based approach to just-in-time instantiation
is the automatic support for mutually dependent services. For example, suppose that
the Indexer and the FileSystem needed to talk directly to each other. Normally, this
would cause a "chicken-and-the-egg" problem: which one to create first?
With Tapestry IoC, this is not even considered a special case:
+-----------------------------------------------------------------------------------+
public static Indexer build(JobScheduler scheduler, FileSystem fileSystem)
{
IndexerImpl indexer = new IndexerImpl(fileSystem);
scheduler.scheduleDailyJob(indexer);
return indexer;
}
public static Indexed build(Indexer indexer)
{
return new FileSystemImpl(indexer);
}
+-----------------------------------------------------------------------------------+
Here, Indexer and FileSystem are mutually dependent. Eventually, one or the other
of them will be created ... let's say its FileSystem. The buildFileSystem() builder
method will be invoked, and a proxy to Indexer will be passed in. Inside the
FileSystemImpl constructor (or at some later date), a method of the Indexer service
will be invoked, at which point, the builderIndexer() method is invoked. It still receives
the proxy to the FileSystem service.
If the order is reversed, such that Indexer is built before FileSystem, everything still
works the same.
This approach can be very powerful: I've (HLS) used it to break apart untestable
monolithic code into two mutually dependent halves, each of which can be unit tested.
The exception to this rule is a service that depends on itself <during construction>.
This can occur when (indirectly, through other services) building the service
tries to invoke a method on the service being built. This can happen when the service
implemention's constructor invoke methods on service dependencies passed into it,
or when the service builder method itself does the same. This is actually a very rare
case and difficult to illustrate.