Last week we looked at the exports
and requires
directives in more detail. This week we’ll cover services contained in modules. These are specified with the uses
and provides
directives.
If you missed the previous posts in this sequence, you can find them here:
- Java Modularity Part 1 – Introduction
- Java Modularity Part 2 – Keywords and Descriptors
- Java Modularity Part 3 – Requires and Exports
Services
A service is a class or application that provides some functionality to an application that calls it.
We can have different service implementations available, and allow our application to choose the most appropriate one at runtime.
We create the service by defining the interface. We then create the classes that implement that interface. These classes are called service providers. The application can use a loader to locate and load service providers as needed.
Our application code refers to the service only by its interface, not by the specific service providers. The service providers are deployed in the runtime environment. Our application chooses the most appropriate service provider it needs at that time.
When we define the service interface, we should conform to the Interface Segregation Principle (ISP) which says that an interface should be as small and cohesive as possible. We create narrow interfaces which are specific to one purpose.
Here’s a simple example of a service interface:
package com.incusdata.helloservice; public interface HelloService { public String hello(String name); }
We then create one or more implementation classes, such as:
public class EnglishHelloServiceImpl implements HelloService { public String hello(String name) { return "Hello " + name; } } public class ZuluHelloServiceImpl implements HelloService { public String hello(String name) { return "Sawubona " + name; } }
ServiceLoaders
Since Java 1.6, we can easily load different service provider implementations by using the java.util.ServiceLoader
class.
Here is a portion of application code that selects the most appropriate service provider from those available:
Iterable services = ServiceLoader.load(HelloService.class); HelloService helloService; for (HelloService hs : services) { if (...) { // some logic to choose the most appropriate provider helloService = hs; // use the selected service object here ... } }
Services as part of Java Modularity
Up to Java 9, we specified the service providers in text files in the META-INF/services
directory of the JAR
file containing the implementation classes.
From Java 9, we can use the module system to do this. Instead of text files, we use module descriptors.
We will have the following three components:
- The service interface is usually in a service interface module.
- The service implementations are provided by separate modules. These implementations are not in the service interface module. Usually each service implementation module will contain a single service implementation.
- The service client will use the service interface module to code against the service interface. It doesn’t know exactly which module will implement the service. The service providers are discovered at runtime depending on which service implementation modules are available on the Java module path.
Service Interface Module
The service interface module is just a normal Java module that exports the package containing the service interface.
Let’s place the previous HelloService
interface in a module. Here the com.incusdata.helloservice
module exports the com.incusdata.helloservice
package:
module com.incusdata.helloservice { exports com.incusdata.helloservice; }
Service Implementation Module
To create a Java module that implements a service interface, we must:
- Specify that the service interface module is required.
- Implement the service interface with a concrete Java class.
- Specify the service implementation class in the module descriptor.
Let’s create a service implementation module for the HelloService
interface. We hadn’t shown a package in the previous implementation code. Let’s assume the service provider class is in the com.acme.myservice
package. Our module descriptor would contain the following:
module com.acme.myservice { requires com.incusdata.helloservice; provides com.incusdata.helloservice.HelloService with com.acme.myservice.EnglishHelloServiceImpl; }
The module descriptor firstly requires
the service interface module. Then it provides
an implementation for the com.incusdata.helloservice.HelloService
interface with the class com.acme.myservice.EnglishHelloServiceImpl
.
Service Client Module
Once we have a service interface module and one or more service implementation modules, we can create a service client module that uses the service.
To use the service, the client module must specify that it uses
the service, as follows:
module com.incusdata.client { requires com.incusdata.helloservice; uses com.incusdata.helloservice.HelloService; }
The client module descriptor also requires the com.incusdata.helloservice
module which contains the service interface. Only the service interface module is required. The service implementation modules are looked up at runtime.
We decide what service implementation to use when loading/running the application. We do this by putting the service implementation module(s) into the module path. The service interface and client modules are then decoupled from the service implementation modules.
The service client module can look up a service provider implementation at runtime with the same code as before:
Iterable services = ServiceLoader.load(HelloService.class); ...
The returned Iterator
contains a list of all HelloService
implementations found in the modules found on the module path. The application can then iterate through the service implementations to find the one it wants to use.
What’s Next?
In the next post, we’ll look at how reflection works with modules.
Did this help? Please share your thoughts and comments.