Chapter 4. Services
In this chapter, you will learn how to use services, an important feature for creating modular codebases. After learning the basics of providing and consuming services, we will apply them to EasyText, making it more extensible.
Factory Pattern
In the previous chapter, you saw that encapsulation alone doesnât get us very far when we want to create truly decoupled modules. If we still write
MyInterface
i
=
new
MyImpl
();
every time we need to use an implementation class, it means the implementation class must be exported. Consequently, strong coupling still remains between the consumer and provider of the implementation: the consumer requires the provider module directly to use its exported implementation class. Changes in the implementation directly affect all consumers. As you will soon see, services are an excellent solution to this problem. But before diving into services, letâs see if we can fix this problem by using an existing pattern, building on our knowledge of the module system so far.
The factory pattern is a well-known creational design pattern that seems to address the very problem we are dealing with. Its goal is to decouple a consumer of objects from the instantiation of specific classes. Many variations on the factory pattern have emerged since it was first described in the iconic Gang of Four Design Patterns book by Gamma et al. (Addison-Wesley). Letâs try to implement a simple variation of this pattern and see how far it gets us with decoupling modules.
We will use the EasyText application again to illustrate the example, by implementing a factory for Analyzer
instances.
Getting an implementation for a given algorithm name is quite straightforward, as shown in Example 4-1.
Example 4-1. A factory class for Analyzer instances (⥠chapter4/easytext-factory)
public
class
AnalyzerFactory
{
public
static
List
<
String
>
getSupportedAnalyses
()
{
return
List
.
of
(
FleschKincaid
.
NAME
,
Coleman
.
NAME
);
}
public
static
Analyzer
getAnalyzer
(
String
name
)
{
switch
(
name
)
{
case
FleschKincaid
.
NAME
:
return
new
FleschKincaid
();
case
Coleman
.
NAME
:
return
new
Coleman
();
default
:
throw
new
IllegalArgumentException
(
"No such analyzer!"
);
}
}
}
You can retrieve a list of supported algorithms from the factory, and request an Analyzer
instance for an algorithm name.
Callers of the AnalyzerFactory
are now oblivious to any underlying implementation classes for the analyzers.
But where do we place this factory?
For one, the factory itself still needs access to multiple analysis modules with their implementation classes.
Otherwise, the instantiation of the various implementation classes in getAnalyzer
would not be possible.
We could put the factory in the API module, but then the API module would have a compile-time dependency on all implementation modules, which is unsatisfactory.
An API should not be tightly coupled to its implementations.
Letâs put the factory in its own module for now, as shown in Figure 4-1.
Now, the frontend modules know about only the API and the factory:
module easytext.cli { requires easytext.analysis.api; requires easytext.analysis.factory; }
Getting an Analyzer
instance becomes trivial:
Analyzer
analyzer
=
AnalyzerFactory
.
getAnalyzer
(
"Flesch-Kincaid"
);
Did we gain anything with this factory approach, besides increased complexity?
On the one hand, yes, the frontend modules are now blissfully unaware of the analysis modules and implementation classes.
There is no direct requires
relation anymore between the consumer and providers of analyses.
Frontend modules can be compiled independently from analysis implementation modules.
When the factory offers additional analyses, the frontends will happily use them without any modification. (Remember, with AnalyzerFactory::getSupportedAnalyses
, they can discover algorithm names to request instances.)
On the other hand, the same tight coupling issues are still present at the factory module level and below.
Whenever a new analysis module comes along, the factory needs to get a dependency on it and expand the getAnalyzer
implementation.
And the analysis modules still need to export their implementation classes for the factory to use.
They could do so through a qualified export (as discussed in âQualified Exportsâ) toward the factory module to limit the scope of exposure.
But that presumes the analysis modules know about the factory module, which is another form of unwanted coupling.
So the factory pattern provides only a partial solution.
We are running into a fundamental limitation of what you can do with modules through requires
and exports
relations.
Programming to interfaces is all well and good, but we have to sacrifice encapsulation to create instances.
Fortunately, there is a solution in the Java module system.
In the next section, weâll explore how services provide a way out of this tough spot.
Services for Implementation Hiding
We tried hiding the implementation classes by using the factory pattern and succeeded only partially. The main problem is that the factory still has to know about all available implementations at compile-time, and the implementation classes must be exported. A solution similar to traditional classpath scanning to discover implementations is not going to solve this, because this would still require readability to all implementation classes in all modules. It still wouldnât be possible to extend the application with another implementation (new algorithms in the case of EasyText) without changing code and recompiling. This doesnât sound like seamless extensibility at all!
The decoupling story can be improved a lot by the services mechanism in the Java module system. Using services, we can truly just share public interfaces, and strongly encapsulate implementation code in packages that are not exported. Keep in mind, using services in the module system is completely optional, unlike strong encapsulation (with explicit exports) and explicit dependencies (with requires). You donât have to use services, but they offer a compelling way of decoupling modules.
Services are expressed both in module descriptors and in code by using the ServiceLoader
API.
In that sense, using services is intrusive: you need to design your application to use them.
As discussed in âDependency Injectionâ, there are alternative ways to achieve inversion of control besides using services.
In the remainder of this chapter, you will learn how services bring better decoupling and extensibility.
We will refactor the EasyText application to start using services. Our goal is to have several modules provide an analysis implementation. The frontend modules can consume those analysis implementations without knowing the provider modules at compile-time.
Providing Services
Exposing service implementations to another module without exporting implementation classes is not possible without special support from the module system. The Java module system allows for a declarative description of providing and consuming services in module-info.java.
In the EasyText code, we already defined the Analyzer
interface, which will be our service interface type.
The interface is exported by the easytext.analysis.api
module, which is strictly an API-only module.
package
javamodularity
.
easytext
.
analysis
.
api
;
import
java.util.List
;
public
interface
Analyzer
{
String
getName
();
double
analyze
(
List
<
List
<
String
>>
text
);
}
Typically, the service type is an interface, as is the case here.
However, it could also be an abstract or even concrete class; there is no inherent technical limitation.
Also, the Analyzer
type is meant to be used by service consumers directly.
Itâs also possible to expose a service type that acts like a factory or proxy.
If, for example, Analyzer
instances would be expensive to instantiate, or extra steps or arguments are required for initialization, the service type could be more akin to the AnalyzerFactory
.
This approach allows the consumer to be more in control of the instantiation.
Now letâs refactor our first new analyzer implementation, the Coleman-Liau algorithm (provided by the easytext.algorithm.coleman
module) to a service provider.
This requires only a change to module-info.java, as shown in Example 4-2.
Example 4-2. Module descriptor providing an Analyzer service (⥠chapter4/easytext-services)
module easytext.analysis.coleman { requires easytext.analysis.api; provides javamodularity.easytext.analysis.api.Analyzer with javamodularity.easytext.analysis.coleman.ColemanAnalyzer; }
The provides with syntax declares that this module provides an implementation of the Analyzer
interface with the ColemanAnalyzer
as an implementation class.
Both the service type (after provides
) and the implementation class (after with
) must be fully qualified type names.
Most important, the package containing the ColemanAnalyzer
implementation class is not exported from this provider module.
This construct works only when the module declaring the provides
has access to both the service type and the implementation class.
Usually this means that an interface, Analyzer
in this example, is either part of the module or is exported by another module that is required.
The implementation class is typically part of the provider module, in an encapsulated (nonexported) package.
When you use nonexistent or inaccessible types in the provides
clause, the module descriptor wonât compile, and a compiler error is generated.
The implementation class used in the with
part of the declaration is normally not exported.
After all, the whole point of services is to hide implementation details.
No code changes are necessary to the service type or implementation class to provide it as a service. Besides this module-info.java declaration, nothing needs to be done. Service implementations are plain Java classes. There are no special annotations to use, no APIs to implement.
Services allow a module to provide implementations to other modules without exporting the concrete implementation class. The module system has special privileges to reach into the provider module to instantiate the nonexported implementation class on behalf of the consumer. This means consumers of the service can use instances of this implementation class, without having access to it directly. Also, a service consumer doesnât know which module provided an implementation, nor does it need to. Because the only shared type between provider and consumer is the service type (most often an interface), there is true decoupling.
Now that weâre done providing our first service, we can repeat this process for the other Analyzer
implementations, and weâre halfway done.
Again, note that these service-providing modules donât export any packages.
Having a module without exports may seem a bit counterintuitive at first.
Nevertheless, these analysis implementation modules contribute useful functionality through the services mechanism at run-time, encapsulating their implementation details at compile-time.
The other half of the refactoring is consuming the services.
Letâs refactor the CLI module to use the Analyzer
services.
Consuming Services
Providing services is useful if other modules can consume them.
Consuming a service in the Java module system requires two steps.
The first step is adding a uses
clause to module-info.java in the CLI module:
module easytext.cli { requires easytext.analysis.api; uses javamodularity.easytext.analysis.api.Analyzer; }
The uses
clause instructs the ServiceLoader
, which you will see in a moment, that this module wants to use implementations of Analyzer
.
The ServiceLoader
then makes Analyzer
instances available to the module.
The uses
clause does not call for an Analyzer
implementation to be available during compile-time.
After all, a service implementation could be provided by a module that we donât have on the module path at compile-time.
Services provide extensibility exactly because providers and consumers are bound only at run-time.
Compilation will not fail when no service providers are found.
The service type (Analyzer
), on the other hand, must be accessible at compile-timeâhence the requires easytext.analysis.api
clause in the module descriptor.
A uses
clause also doesnât guarantee there will be providers during run-time.
The application will start successfully without any providers of services.
This means there can be zero or more providers available at run-time, and our code has to deal with this.
Now that the module has declared that it wants to use Analyzer
implementations, we can start writing code that uses the service.
Consuming services happens through the ServiceLoader
API.
The ServiceLoader
API itself has been around since Java 6 already.
Although it is widely used in the JDK, few Java developers know or use ServiceLoader
.
âServiceLoader Before Java 9â provides more historical background.
The ServiceLoader
API is repurposed in the Java module system to work with modules, and is an important programming construct when working with the Java module system.
Letâs look at an example; see Example 4-3.
Example 4-3. Main.java
Iterable
<
Analyzer
>
analyzers
=
ServiceLoader
.
load
(
Analyzer
.
class
)
;
for
(
Analyzer
analyzer:
analyzers
)
{
System
.
out
.
println
(
analyzer
.
getName
(
)
+
": "
+
analyzer
.
analyze
(
sentences
)
)
;
}
Initialize a
ServiceLoader
for services of typeAnalyzer
.Iterate over the instances and invoke the
analyze
method.
The ServiceLoader::load
method returns a ServiceLoader
instance that also conveniently implements Iterable
.
When you iterate over it as in the example, instances are created for all the provider types that have been discovered for the requested Analyzer
interface.
Note that we get only the actual instances here, with no additional information on which modules provided them.
After iterating over the services, we can use them like any other Java object.
In fact, they are plain Java objects, except they are instantiated by ServiceLoader
for us.
Being just normal Java instances, there is zero overhead when invoking a service.
Invoking a method on a service is just a direct method call; there are no proxies or other indirections that decrease performance.
With these changes, we have refactored our EasyText code from a partially decoupled factory structure to a fully modular and extensible setup as shown in Figure 4-2.
The code is fully decoupled because the CLI module doesnât need to know anything about modules providing Analyzer
implementations.
The application is easily extensible because we can add a new Analyzer
implementation by simply adding a new provider module to the module path.
Any services provided by these additional modules are picked up automatically through the ServiceLoader
service discovery.
No code changes or recompilation are necessary.
Arguably the best part is that the code is clean.
Programming with services is just as simple as writing plain Java code (thatâs because it is plain Java code), but the impact on architecture and design is quite positive.
You have seen that services provide an easy way to achieve decoupling. Think of services as the cornerstone of modular development. Although strong mechanisms for defining module boundaries are the first step toward modular design, services are required to create and use strictly decoupled modules.
Service Life Cycle
If the ServiceLoader
is responsible for creating instances of provided services, itâs important to know how this works exactly.
In Example 4-3, the iteration caused the Analyzer
implementation classes to be instantiated.
ServiceLoader
works lazily, meaning the ServiceLoader::load
call doesnât immediately instantiate all known provider implementation classes.
A new ServiceLoader
is instantiated every time you call ServiceLoader::load
.
Such a new ServiceLoader
in turn reinstantiates provider classes when they are requested.
Requesting services from an existing ServiceLoader
instance returns cached instances of provider classes.
This is demonstrated by the following code:
ServiceLoader
<
Analyzer
>
first
=
ServiceLoader
.
load
(
Analyzer
.
class
)
;
System
.
out
.
println
(
"Using the first analyzers"
)
;
for
(
Analyzer
analyzer:
first
)
{
System
.
out
.
println
(
analyzer
.
hashCode
(
)
)
;
}
Iterable
<
Analyzer
>
second
=
ServiceLoader
.
load
(
Analyzer
.
class
)
;
System
.
out
.
println
(
"Using the second analyzers"
)
;
for
(
Analyzer
analyzer:
second
)
{
System
.
out
.
println
(
analyzer
.
hashCode
(
)
)
;
}
System
.
out
.
println
(
"Using the first analyzers again, hashCode is the same"
)
;
for
(
Analyzer
analyzer:
first
)
{
System
.
out
.
println
(
analyzer
.
hashCode
(
)
)
;
}
first
.
reload
(
)
;
System
.
out
.
println
(
"Reloading the first analyzers, hashCode is different"
)
;
for
(
Analyzer
analyzer:
first
)
{
System
.
out
.
println
(
analyzer
.
hashCode
(
)
)
;
}
Iterating over
first
,ServiceLoader
instantiatesAnalyzer
implementations.A new
ServiceLoader
,second
, will instantiate its own, fresh,Analyzer
implementations. It returns different instances thanfirst
.The originally instantiated services are returned from
first
when iterating again, as they are cached by the firstServiceLoader
instance.After
reload
, the originalfirst
ServiceLoader
provides fresh instances.
This code outputs something like the following (actual hashCodes will vary, of course):
Using the first analyzers 1379435698 Using the second analyzers 876563773 Using the first analyzers again, hashCode is the same 1379435698 Reloading the first analyzers, hashCode is different 87765719
Because every invocation of ServiceLoader::load
leads to new service instances, different modules using the same service will all have their own instance.
This is something to remember when working with services that contain state.
The state is, without other provisions, not shared between usages across different ServiceLoader
s for the same service type.
There is no singleton service instance, unlike what is typically the case in dependency injection frameworks.
Service Provider Methods
Service instances can be created in two ways. Either the service implementation class must have a public no-arg constructor, or a static provider method can be used. Itâs not always desirable for a service implementation class to have a public no-arg constructor. In cases where more information needs to be passed to the constructor, a static provider method is the better option. Or, you may want to expose an existing class without a no-arg constructor as a service.
A provider method is a public static
no-arg method called provider
, where the return type is the service type.
It must return a service instance of the correct type (or a subtype).
How the service is instantiated in this method is completely up to the provider
implementation.
Possibly a singleton is cached and returned, or it just instantiates a new service instance for each call.
When using the provider method approach, the provides .. with
clause refers to the class containing the provider method after with
.
This can very well be the service implementation class itself, but it can also be another class.
A class appearing after with
must have either a provider method or a public no-arg constructor.
If there is no static provider method, the class is assumed to be the service implementation itself and must have a public no-arg constructor.
The compiler will complain when this is not the case.
Letâs look at a provider method example (Example 4-4).
Weâll use another Analyzer
implementation for this, just to highlight the use of a provider method.
Example 4-4. ExampleProviderMethod.java (⥠chapter4/providers/provider.method.example)
package
javamodularity
.
providers
.
method
;
import
java.util.List
;
import
javamodularity.easytext.analysis.api.Analyzer
;
public
class
ExampleProviderMethod
implements
Analyzer
{
private
String
name
;
ExampleProviderMethod
(
String
name
)
{
this
.
name
=
name
;
}
@Override
public
String
getName
()
{
return
name
;
}
@Override
public
double
analyze
(
List
<
List
<
String
>>
sentences
)
{
return
0
;
}
public
static
ExampleProviderMethod
provider
()
{
return
new
ExampleProviderMethod
(
"Analyzer created by static method"
);
}
}
The Analyzer
implementation is fairly useless, but it does show the usage of a provider
method.
The module-info.java for this example would be exactly the same as we have seen so far; the Java module system will figure out the right way to instantiate the class.
In this example, the provider
method is part of the implementation class.
Alternatively, we can place the provider
method in another class, which then serves as a factory for the service implementation. Example 4-5 shows this approach.
Example 4-5. ExampleProviderFactory.java (⥠chapter4/providers/provider.factory.example)
package
javamodularity
.
providers
.
factory
;
public
class
ExampleProviderFactory
{
public
static
ExampleProvider
provider
()
{
return
new
ExampleProvider
(
"Analyzer created by factory"
);
}
}
Now we do have to change module-info.java to reflect this change.
The provides .. with
must now point to the class containing the static provider method as shown in Example 4-6.
Example 4-6. module-info.java (⥠chapter4/providers/provider.factory.example)
module provider.factory.example { requires easytext.analysis.api; provides javamodularity.easytext.analysis.api.Analyzer with javamodularity.providers.factory.ExampleProviderFactory; }
Tip
ServiceLoader
can instantiate a service only if the provider class is public.
Only the provider class itself needs to be public; our second example shows that the implementation can be package-private as long as the provider class is public.
Note that in all cases the exposed service type Analyzer
remains unchanged.
From the perspective of the consumer, it makes no difference how the service is instantiated.
A static provider method offers more flexibility on the provider side.
In many cases, a public no-arg constructor on the service implementation class suffices.
Services in the module system donât offer a shutdown or service de-registration mechanism. The death of a service is implicit, through garbage collection. Garbage collection behaves the same for service instances as for any other objects in Java. Once there are no hard references to the object anymore, it can be garbage collected.
Factory Pattern Revisited
Consumer modules can obtain services through the ServiceLoader
API.
You can employ a useful pattern to avoid the use of this API in consumers, if desired.
Instead, you can offer an API to consumers similar to the factory example in the beginning of this chapter.
Itâs based on the ability to have static methods in interfaces as of Java 8.
The service type itself is extended with a static method (factory method) that does the ServiceLoader
lookup, as shown in Example 4-7.
Example 4-7. Provide a factory method on the service interface (⥠chapter4/easytext-services-factory)
public
interface
Analyzer
{
String
getName
(
)
;
double
analyze
(
List
<
List
<
String
>
>
text
)
;
static
Iterable
<
Analyzer
>
getAnalyzers
(
)
{
return
ServiceLoader
.
load
(
Analyzer
.
class
)
;
}
}
Because the ServiceLoader
lookup is done in Analyzer
in the API module, its module descriptor must express the uses
constraint:
module easytext.analysis.api { exports javamodularity.easytext.analysis.api; uses javamodularity.easytext.analysis.api.Analyzer; }
Now, the API module both exports the interface and uses implementations of the Analyzer
interface.
Consumer modules that want to obtain Analyzer
implementations no longer need to use ServiceLoader
(though they still can, of course).
Instead, all a consumer module needs to do is require the API module and call Analyzer::getAnalyzers
.
No need for a uses
constraint or the ServiceLoader
API anymore from the perspective of the consumer.
Through this mechanism, you can use the power of services unobtrusively.
Users of an API are not forced to know about services or ServiceLoader
but still get the benefits of decoupling and extensibility.
Default Service Implementations
So far, weâve worked from the assumption that there is an API module, and there are several distinct provider modules implementing this API. Thatâs not unreasonable, but itâs far from the only way to set things up. Itâs perfectly possible to put an implementation into the same module exporting the service type. When a service type has an obvious default implementation, why not provide it from the same module directly?
You see this pattern a lot in the way the JDK itself uses services.
Even though it is possible to provide your own implementations for javax.sound.sampled.spi.AudioFileWriter
or javax.print.PrintServiceLookup
, most of the time the default implementations provided by the java.desktop
module are adequate.
These service types are exported from java.desktop
, and at the same time default implementations are provided.
In fact, java.desktop
itself even has uses
constraints for those service types.
This shows how a module can play the role of API owner, service provider, and consumer at the same time.
Bundling a default service implementation with the service type guarantees that at least one implementation is always available. In that case, no defensive coding is necessary on the consumerâs part. Some service dependencies are intended to be optional. A default implementation in the same module as the service type precludes this scenario. Then, having a separate API module is necessary. In âImplementing Optional Dependencies with Servicesâ, this pattern is explored in more detail.
Service Implementation Selection
When there are multiple providers, you donât necessarily want to use them all. Sometimes you want to filter and select an implementation based on certain characteristics.
Note
Itâs always the consumer deciding which service to use based on properties of the providers. Because providers should be completely unaware of each other, there is no way to favor a certain implementation from a providerâs perspective. What would happen if, for example, two providers designate themselves as default or best implementation? The logic to select the right service(s) is application-dependent and belongs to the consumer.
You have seen that the ServiceLoader
API itself is fairly limited.
Until now, we have iterated only over all existing service implementations.
What if we have multiple providers but are interested in only âthe bestâ implementation?
The Java module system canât possibly know what the best implementation is for your needs.
Each domain has its own requirements in that regard.
Therefore, itâs up to you to equip your service type with methods to discover the capabilities of a service and make decisions based on these methods.
This need not be complicated, and usually comes down to adding self-describing methods to a service interface.
For example, the Analyzer
service interface offers a getName
method.
ServiceLoader
doesnât know or care about this method, but we can use it in consumer modules to identify an implementation.
Besides selecting an algorithm by name, you can also think of describing different characteristics, for example, with getAccuracy
or getCost
methods.
This way, a consumer of the Analyzer
service can make a well-informed choice between implementations.
Thereâs no explicit support from the ServiceLoader
API necessary: it all boils down to designing self-describing interfaces.
Service Type Inspection and Lazy Instantiation
In some scenarios, the mechanism described previously is still not sufficient.
What if thereâs no method on the service interface to distinguish the right implementation?
Or instantiation of the services is expensive?
We would incur the cost of initialization for all service implementations just to find the right one, using a ServiceLoader
iteration.
In most scenarios, this is not an issue, but a solution exists for problematic cases.
With Java 9, ServiceLoader
is enhanced to support service implementation type inspection before instantiation.
Besides iterating over all provided instances as weâve done so far, itâs also possible to inspect a stream of ServiceLoader.Provider
descriptions.
The ServiceLoader.Provider
class makes it possible to inspect a service provider before requesting an instance.
The stream
method on ServiceLoader
returns a stream of ServiceLoader.Provider
objects to inspect.
Letâs look at an example based on EasyText again.
First we introduce our own annotation that can be used to select the right service implementation in Example 4-8.
Such an annotation can be part of the API module that is shared between providers and consumers.
The example annotation describes whether an Analyzer
is fast.
Example 4-8. Define an annotation to annotate the service implementation class (⥠chapter4/easytext-filtering)
package
javamodularity
.
easytext
.
analysis
.
api
;
import
java.lang.annotation.Retention
;
import
java.lang.annotation.RetentionPolicy
;
@Retention
(
RetentionPolicy
.
RUNTIME
)
public
@interface
Fast
{
public
boolean
value
()
default
true
;
}
We can now use this annotation to add metadata to a service implementation.
Here, we add it to an example Analyzer
:
@Fast
public
class
ReallyFastAnalyzer
implements
Analyzer
{
// Implementation of the analyzer
}
Now we just need some code to filter the Analyzer
s:
public
class
Main
{
public
static
void
main
(
String
args
[])
{
ServiceLoader
<
Analyzer
>
analyzers
=
ServiceLoader
.
load
(
Analyzer
.
class
);
analyzers
.
stream
()
.
filter
(
provider
->
isFast
(
provider
.
type
()))
.
map
(
ServiceLoader
.
Provider
::
get
)
.
forEach
(
analyzer
->
System
.
out
.
println
(
analyzer
.
getName
()));
}
private
static
boolean
isFast
(
Class
<?>
clazz
)
{
return
clazz
.
isAnnotationPresent
(
Fast
.
class
)
&&
clazz
.
getAnnotation
(
Fast
.
class
).
value
()
==
true
;
}
}
Through the type
method on Provider
, we get access to the java.lang.Class
representation of the service implementation, which we pass to the isFast
method for filtering.
The isFast
method checks for the presence of our @Fast
annotation, and checks the value explicitly for true
(which is the default value).
Analyzer
implementations that are not annotated as being fast are ignored, but services annotated @Fast
or @Fast(true)
are instantiated and invoked.
If you remove the filter
from the stream pipeline, all Analyzer
s will be invoked indiscriminately.
The examples in this chapter demonstrate that although the ServiceLoader
API is basic, the services mechanism is powerful.
Services are an important construct in the Java module system when modularizing code.
Module Resolution with Service Binding
Remember learning in âModule Resolution and the Module Pathâ that modules are resolved based on requires
clauses in module descriptors?
By recursively following all requires
relations starting from a root module, the set of resolved modules is built from modules on the module path.
During this process, missing modules are detected, giving the benefit of reliable configuration.
The application wonât start if a required module is missing.
Service provides
and uses
clauses add another dimension to the resolution process.
Whereas requires
clauses indicate strict compile-time relations between modules, service binding happens at run-time.
Because service provider and consumer modules both declaratively state their intentions in module descriptors, this information can be used during the module resolution process as well.
In theory, an application can start without any of its services being bound at run-time.
Calling ServiceLoader::load
wonât result in any instances.
Thatâs hardly useful, so the module system locates service provider modules on the module path at startup in addition to modules that are required.
When a module with a uses
clause is resolved, the module system locates all provider modules for the given service type on the module path and adds them to the resolution process.
These provider modules, and their dependencies, become part of the run-time module graph.
The implications of this extension to module resolution become clearer by looking at an example. In Figure 4-3 we look at our EasyText example again from the perspective of module resolution.
We assume there are five modules on the module path: cli
(the root module), api
, kincaid
, coleman
, and an imaginary module syllablecounter
.
Module resolution starts with cli
.
It has a requires relation to api
, so this module is added to the set of resolved modules.
So far, nothing new.
However, cli
also has a uses
clause for Analyzer
.
There are two provider modules on the module path providing implementations of this interface.
Hence the provider modules kincaid
and coleman
are added to the set of resolved modules.
Module resolution stops for cli
because it doesnât have any other requires
or uses
clauses.
For kincaid
, thereâs nothing left to add to the resolved modules.
The api
module that it requires has already been resolved.
With coleman
, things are more interesting.
Service binding caused coleman
to be resolved.
In this example, the coleman
module requires another module: syllablecounter
.
Therefore, syllablecounter
is resolved as well, and both modules are added to the run-time module graph.
If syllablecounter
itself would have had requires
(or even uses
!) clauses, these would be subject to module resolution as well.
Conversely, if syllablecounter
is not found on the module path, resolution fails, and the application wonât start.
Even though cli
, the consumer module, doesnât have any static knowledge of the coleman
provider module, it is still resolved with all its dependencies through service binding.
There is no way for a consumer to specify that it needs at least one implementation.
When no service provider modules are found, the application starts just as well.
Code using ServiceLoader
needs to account for this possibility.
Youâve already seen that many JDK service types have default implementations.
When thereâs a default implementation in the module exposing the service type, youâre guaranteed to always have at least one service implementation available.
In the example, module resolution also succeeds when coleman
isnât on the module path.
At run-time, the ServiceLoader::load
call finds the implementation only from kincaid
in that case.
However, as weâve explained, if coleman
is on the module path but syllablecounter
isnât, the application wonât start because of module-resolution failure.
Silently ignoring this problem would be possible for the module system, but runs counter to the mantra of reliable configuration based on module descriptors.
Services and Linking
In âLinking Modulesâ, you learned how to use jlink to create custom runtime images. We can create an image for the EasyText implementation with services as well. Based on what youâve learned in the previous chapter, we can come up with the following jlink command:
$ jlink --module-path mods/:$JAVA_HOME/jmods --add-modules easytext.cli \ --output image
jlink creates a directory image
, containing a bin
directory.
We can inspect the modules included in the image by using the following command:
$ image/bin/java --list-modules java.base@9 easytext.analysis.api easytext.cli
The api
and cli
modules are part of the image as expected, but what about the two analysis provider modules?
If we run the application this way, it starts correctly because service providers are optional.
But it is pretty useless without any analyzers.
jlink performs module resolution starting from the root module easytext.cli
.
All resolved modules are included in the resulting image.
However, the resolution process differs from the resolution done by the module system at startup, which we discussed in the previous section.
No service binding is done by jlink during module resolution.
That means service providers are not automatically included in the image based on uses
clauses.
Although this will certainly cause unexpected results for users who are not aware of this, it is a deliberate choice. Services are often used for extensibility. The EasyText application is a good example of this; new types of algorithms can be added by adding new service provider modules to the module path. Services used this way are not necessarily required to run the application. Which service providers you want to combine is an application-dependent concern. At build-time, there is no dependency to the service providers, and at link-time, itâs really up to the creator of the desired image to choose which service providers should be available.
Note
A more down-to-earth reason to not do automatic service binding in jlink is that java.base
has an enormous number of uses
clauses.
The providers for all these service types are in various other platform modules.
Binding all these services by default would result in a much larger minimum image size.
Without automatic service binding in jlink, you can create an image containing just java.base
and application modules, as in our example.
In general, automatic service binding can lead to unexpectedly large module graphs.
Letâs try to create a runtime image for the EasyText application, configured to run from the command line.
To include analyzers, we use the --add-modules
argument when executing jlink for each provider module we want to add:
$ jlink --module-path mods/:$JAVA_HOME/jmods \ --add-modules easytext.cli \ --add-modules easytext.analysis.coleman \ --add-modules easytext.analysis.kincaid \ --output image
$ image/bin/java --list-modules java.base@9 easytext.analysis.api easytext.analysis.coleman easytext.analysis.kincaid easytext.cli
This looks better, but we will still find a problem when starting the application:
$ image/bin/java -m easytext.cli input.txt
The application exits with an exception: java.lang.IllegalStateException: SyllableCounter not found
.
The kincaid
module uses another service of type SyllableCounter
.
This is a case where a service provider uses another service to implement its functionality.
We already know that jlink doesnât automatically include service providers, so the module containing the SyllableCounter
example wasnât included either.
We use --add-modules
once more to finally get a fully functional image:
$ jlink --module-path mods/:$JAVA_HOME/jmods \ --add-modules easytext.cli \ --add-modules easytext.analysis.coleman \ --add-modules easytext.analysis.kincaid \ --add-modules easytext.analysis.naivesyllablecounter \ --output image
The fact that jlink doesnât include service providers by default requires some extra work at link-time, especially when services use other services transitively. In return, it does give a lot of flexibility for fine-tuning the contents of a runtime image. Different images can serve different types of users, just by reconfiguring which service providers are included. In âFinding the Right Service Provider Modulesâ, weâll see that jlink offers additional options to discover and link relevant service provider modules.
The previous chapters covered the basics of the Java module system. Modularity is a lot about design and architecture, and this is where it really gets interesting. In the next chapter, we are going to look at patterns that improve the maintainability, flexibility, and reusability of systems built by using modules.
Get Java 9 Modularity now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.