...
Scrollbar |
---|
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 back doors 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 environment (such as a servlet container or application server) things Just Work.
Anchor | ||||
---|---|---|---|---|
|
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, a method whose name is (or starts with) "build":
Code Block | ||
---|---|---|
| ||
package org.example.myapp.services;
public class MyAppModule
{
public static Indexer build()
{
return new IndexerImpl();
}
} |
...
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 ServiceBinder. Services may be registered with the container by "binding" a service interface to a service implementation:
Code Block | ||||
---|---|---|---|---|
| ||||
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);
}
} |
...
Following the convention over configuration principle, the autobuilding of services can be even less verbose. If a service interface is passed as a single argument to the bind() method, Tapestry will try to find an implementation in the same package whose name matches the name of the service interface followed by the suffix Impl.
Code Block | ||||
---|---|---|---|---|
| ||||
package org.example.myapp.services;
import org.apache.tapestry5.ioc.ServiceBinder;
public class MyAppModule
{
public static void bind(ServiceBinder binder)
{
binder.bind(Indexer.class);
}
} |
Service Ids
Every service will have a unique service id.
...
This can be overridden by adding the @ServiceId annotation to the service builder method:
Code Block | ||||
---|---|---|---|---|
| ||||
@ServiceId("FileSystemIndexer")
public static Indexer buildIndexer(@InjectService("FileSystem") FileSystem fileSystem)
{
. . .
} |
Another option is to add the service id to the method name, after "build", for example:
Code Block | ||||
---|---|---|---|---|
| ||||
public static Indexer buildFileSystemIndexer(@InjectService("FileSystem") FileSystem fileSystem)
{
. . .
} |
...
For autobuilt services, the service id can be specified by placing the @ServiceId annotation directly on a service implementation class.
Code Block | ||||
---|---|---|---|---|
| ||||
@ServiceId("FileSystemIndexer")
public class IndexerImpl implements Indexer
{
...
} |
When the service is bound, the value of the annotation is used as id:
Code Block | ||||
---|---|---|---|---|
| ||||
binder.bind(Indexer.class, IndexerImpl.class); |
This id can be overriden again by calling the method withId(String):
Code Block | ||||
---|---|---|---|---|
| ||||
binder.bind(Indexer.class, IndexerImpl.class).withId("FileSystemIndexer"); |
Anchor | ||||
---|---|---|---|---|
|
It's pretty unlikely that your service will be able to operate in a total vacuum. It will have other dependencies.
...
- 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 class (to be cached inside instance variables)
- Directly into fields of the service implementation
For example, let's say the Indexer needs a JobScheduler to control when it executes, and a FileSystem to access files and store indexes.
Code Block | ||||
---|---|---|---|---|
| ||||
public static Indexer build(JobScheduler scheduler, FileSystem fileSystem) { IndexerImpl indexer = new IndexerImpl(fileSystem); scheduler.scheduleDailyJob(indexer); return indexer; } |
Tapestry assumes that parameters to builder methods are dependencies; in this example it is able to figure out what services to pass in based just on the type (later we'll see how we can fine tune this with annotations, when the service type is not sufficient to identify a single service).
...
Note that we don't invoke those service builder methods ... we just "advertise" (via naming convention or annotation) 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 @InjectService annotation:
Code Block | ||||
---|---|---|---|---|
| ||||
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 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).
...
We can associate those two JobSchedulers with two annotations.
Code Block | ||||
---|---|---|---|---|
| ||||
@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);
}
} |
...
To get the right version of the service, you use one of the annotations:
Code Block | ||||
---|---|---|---|---|
| ||||
public class MyServiceImpl implements MyService
{
private final JobScheduler jobScheduler;
public MyServiceImpl(@Clustered JobScheduler jobScheduler)
{
this.jobScheduler = jobScheduler;
}
. . .
} |
...
With a service builder method, you use the @Marker @Marker annotation:
Code Block | ||||
---|---|---|---|---|
| ||||
@Marker(Clustered.class)
public JobScheduler buildClusteredJobScheduler()
{
return . . .;
} |
...
Finally, the point of injection may have multiple marker annotations; only services that are marked with all those markers will be considered for injection. Each marker annotation creates an increasingly narrow subset from the set of all possible services (compatible with the indicated dependency type).
Local Dependencies
A special marker interface, @Local@Local, indicates a dependency that should only be resolved using services from within the same module.
@Local can also be combined with other marker annotations.
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:
Code Block | ||||
---|---|---|---|---|
| ||||
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; } . . . } |
...
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:
Code Block | ||||
---|---|---|---|---|
| ||||
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.
Field Injection
The @Inject and @InjectService annotations may be used on instance fields of a service implementation class, as an alternative to passing dependencies of the service implementation in via the constructor.
Note that only dependencies are settable this way; if you want resources, including the service's configuration, you must pass those through the constructor. You are free to mix and match, injecting partially with field injection and partially with constructor injection.
Caution: injection via fields uses reflection to make the fields accessible. In addition, it may not be as thread-safe as using the constructor to assign to final fields.
Code Block | ||||
---|---|---|---|---|
| ||||
package org.example.myapp.services;
import org.apache.tapestry5.ioc.annotations.InjectService;
public class IndexerImpl implements Indexer
{
@InjectService("FileSystem")
private FileSystem fileSystem;
. . .
} |
Anchor | ||||
---|---|---|---|---|
|
Each service has a lifecycle scope that controls when the service implementation is instantiated. There are two built in lifecyclesscopes: "singleton" and "perthread", but more can be added.
Service lifecycle scope is specified using the @Scope @Scope annotation, which is attached to a builder method, or to the service implementation class. 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 Registry API from outside the container.
...
You should be aware when writing services that your code must be thread safe; any service you define could be invoked simulataneously simultaneously 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.
...
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 multithreaded 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.
...
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 @Scope annotation. This is generally the preferred way to specify scope.
In addition, it is possible to specify the scope when binding the service:
Code Block | ||||
---|---|---|---|---|
| ||||
bind(MyServiceInterface.class, MyServiceImpl.class).scope(IOCConstantsScopeConstants.PERTHREAD_SCOPE); |
Eager Loading Services
Services are normally created only as needed (per the scope discussion above).
This can be tweaked slightly; by adding the @EagerLoad annotation to the service builder method, Tapestry will instantiate the service when the Registry is first created.
...
With the perthread scope, 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.
...
You may also specify eager loading explicitly when binding the service:
Code Block | ||||
---|---|---|---|---|
| ||||
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
- org.slf4j.Logger: logger for the service
- java.lang.Class: service interface implemented by the service to be constructed
- ServiceResources: access to other services
No annotation is needed for these cases.
See also service configuration for additional special cases of resources that can be injected.
...
Example:
Code Block | ||||
---|---|---|---|---|
| ||||
public static Indexer build(String serviceId, Log serviceLog, JobScheduler scheduler, FileSystem fileSystem) { IndexerImpl indexer = new IndexerImpl(serviceLog, fileSystem); scheduler.scheduleDailyJob(serviceId, indexer); return 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:
Code Block | ||||
---|---|---|---|---|
| ||||
public static Indexer build(ServiceResources resources, JobScheduler jobScheduler) { IndexerImpl indexer = resources.autobuild(IndexerImpl.class); scheduler.scheduleDailyJob(resources.getServiceId(), indexer); return indexer; } |
...
The @InjectService annotation takes precendence precedence over these resources.
If the @InjectService annotation is not present, and the parameter type does not exactly match a resource type, then 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:
Code Block | ||||
---|---|---|---|---|
| ||||
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 @Inject annotation does:
Code Block | ||||
---|---|---|---|---|
| ||||
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 receive the configured alerts email (see the symbols documentation for more about this syntax) rather than the service id.
Binding ServiceBuilders
Yet another option is available: instead of binding an interface to a implemention class, you can bind a service to a ServiceBuilder, a callback used to create the service implementation. This is very useful in very rare circumstances.
Builtin Services
A few services within the Tapestry IOC Module are "builtin"; there is no service builder method in the TapestryIOCModule class.
Service Id | Service Interface |
ClassFactory | |
LoggerSource | |
RegistryShutdownHub | |
PerthreadManager |
Consult the JavaDoc for each of these services to identify under what circumstances you'll need to use them.
Mutually
...
Dependent 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:
Code Block | ||||
---|---|---|---|---|
| ||||
public static Indexer buildIndexer(JobScheduler scheduler, FileSystem fileSystem) { IndexerImpl indexer = new IndexerImpl(fileSystem); scheduler.scheduleDailyJob(indexer); return indexer; } public static FileSystem buildFileSystem(Indexer indexer) { return new FileSystemImpl(indexer); } |
...
This approach can be very powerful: I've (HLS) used it . For example, it can be used 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 implementionimplementation'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.
Scrollbar |
---|