Contribution Methods: Naming Conventions vs Annotations
One of the most powerful concepts of Tapestry IoC is distributed configuration. Every service can have a configuration which can be created across several IoC modules of an application. When a service instance is created all modules are scanned for contribution methods. A contribution method is a module method whose name consists of the prefix contribute and a service id. The service id is used to identify the service to contribute into. There are three different styles of configurations:
- Unordered Configuration of type java.util.Collection
- Ordered Configuration of type java.util.List
- Mapped Configuration of type java.util.Map
When creating a service instance the registry creates an empty configuration and passes it to all the available contribution methods for the service. In the following example you can see a contribution method for the service with the id ApplicationDefaults.
public class AppModule {
public static void contributeApplicationDefaults(
MappedConfiguration<String, String> config) {
config.add(SymbolConstants.PRODUCTION_MODE, "false");
}
}
This approach works fine because a service id is unique inside the service registry. But identifying services by string ids is often error prone. First if you want to contribute to a configuration of a service you need to know the id of the service. For this purpose you have to look into Tapestry sources or call the ServiceStatus page. Second if you refactor your application and rename an id of a service, your contribution won’t work anymore.
As of version 5.2 (I committed the code today) the annotation @Contribute can be used as an alternative for naming convention for contribution methods. The annotation may be placed on an arbitrary named method of a module to identify this method as a contribution method. The value of the annotation is the type of the service to contribute into. The following example is an alternative for the contribution method above. Note that service with id ApplicationDefaults is of type SymbolProvider.
public class AppModule {
@Contribute(SymbolProvider.class)
public static void arbitraryName(
MappedConfiguration<String, String> config) {
config.add(SymbolConstants.PRODUCTION_MODE, "false");
}
}
What if you have several implementations of a service interface? For example Tapestry provides several instance of the service SymbolProvider. In this case you have to disambiguate the service instances with marker annotations. In Tapestry the services ApplicationDefaults and FactoryDefaults are marked with annotations @ApplicationDefaults and @FactoryDefaults. We need to modify our contribution method a little bit in order to identify the service to contribute into. For this purpose the @Marker annotation should be placed on the contributor method. The value of the annotation is the class of a marker annotation which was used to disambiguate the service instance. The following contribution method is meant for the instance of SymbolProvider service marked with the annotation @ApplicationDefaults.
public class AppModule {
@Contribute(SymbolProvider.class)
@Marker(ApplicationDefaults.class)
public static void arbitraryName(
MappedConfiguration<String, String> config) {
config.add(SymbolConstants.PRODUCTION_MODE, "false");
}
}
Enjoy!
Nice. Could you also compare it with for example dynamic configuration service of OSGi?
Hello Renat,
nice to see you here.
The main difference is that in OSGi a configuration of a bundle or a service can be updated at runtime. You can use the ConfigurationAdmin service to add or update configurations.
In contrast a configuration of a Tapestry service is passed to the service at creation time. This means for singleton services that the configuration is created only once, for per-request services every time a new instance is created. The configuration is created by calling the contribution methods. There is no way to change the configuration after the service has been created.
A further difference is the configuration object. In OSGi a Configuration consists of a dictionary. The values types of the dictionary are limited to single values or arrays of String, Integer, Long, Float, Double, Byte, Short, Character, Boolean. In Tapestry a service configuration can also consist of further services available in the service registry.