Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.
Comment: restore

Scrollbar

Services consist of two main parts: a service interface and a service implementation.

...

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 environment (such as a servlet container or application server) things Just Work.

Anchor
serviceBuilderMethod
serviceBuilderMethod

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
java
java
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.

...

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
java
java
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.

...

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
java
java
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
java
java
  @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
java
java
  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 by placing the @ServiceId annotation directly on a service implementation class.

Code Block
java
java
  @ServiceId("FileSystemIndexer")
  public class IndexerImpl implements Indexer
  {
      ...
  }

When the service is bound, the value of the annotation is used as id:

Code Block
java
java
  binder.bind(Indexer.class, IndexerImpl.class);

This id can be overriden again by calling the method withId

Code Block
java
java
  binder.bind(Indexer.class, IndexerImpl.class).withId("FileSystemIndexer");

Anchor
Injecting_Dependencies
Injecting_Dependencies
Injecting Dependencies

It's pretty unlikely that your service will be able to operate in a total vacuum. It will have other dependencies.

...

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
java
java
  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).

...

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
java
java
  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.

...

We can associate those two JobSchedulers with two annotations.

Code Block
java
java
@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:

Code Block
java
java
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.

...

With a service builder method, you use the @Marker annotation:

Code Block
java
java
  @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.

...

Instead, the injections occur on constructor for the implementation class:

...

Code Block
java
java
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.

...

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
java
java
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.

...

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
java
java
package org.example.myapp.services;

import org.apache.tapestry5.ioc.annotations.InjectService;

public class IndexerImpl implements Indexer
{
  @InjectService("FileSystem")
  private FileSystem fileSystem;

  . . .
}

Anchor
ServiceScope
ServiceScope
Defining Service Scope

Each service has a scope that controls when the service implementation is instantiated. There are two built in scopes: "singleton" and "perthread", but more can be added.

...

In addition, it is possible to specify the scope when binding the service:

Code Block
java
java
  bind(MyServiceInterface.class, MyServiceImpl.class).scope(ScopeConstants.PERTHREAD);

Eager Loading Services

Services are normally created only as needed (per the scope discussion above).

...

You may also specify eager loading explicitly when binding the service:

Code Block
java
java
  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.

...

No annotation is needed for these cases.

See also service configuration for additional special cases of resources that can be injected.

Note: resources may not be injected into fields, they are injectable only via method or constructor parameters.

Example:

Code Block
java
java
  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.

...

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
java
java
  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.

...

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
java
java
  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
java
java
  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 receive the configured alerts email (see the symbols documentation for more about this syntax) rather than the service id.

...

With Tapestry IoC, this is not even considered a special case:

Code Block
java
java
  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);
  }  

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.

...

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 implementation'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