© Alexandru Jecan  2017

Alexandru Jecan, Java 9 Modularity Revealed, https://doi.org/10.1007/978-1-4842-2713-8_6

6. Services

Alexandru Jecan

(1)Munich, Germany

A service is basically a piece of functionality defined by an interface or a class for which service providers exist. The role of services is to decouple tightly coupled modules and to allow loose coupling between service providers and service consumers. Using services in JDK 9 isn’t mandatory, but they offer a nice solution for having decoupled modules.

Suppose we want to test the brake systems for different types of cars. A brake service could define general guidelines, legal rules, and best practices for testing the brake systems. The car manufacturers could implement their own services for testing the brake systems in their cars, because each brake system is different from model to model. These services are called service providers , because they provide specific implementations for the brake service. Internal tools used by car manufactureres for vizualizing and analyzing the functions in a car could use the brake service. These are called service consumers because they use, or consume, the service.

The basic idea behind services in Jigsaw is that in a module we don’t want to expose our implementation class, but only something that’s being exposed through an interface. This leads to the following question: how can we implement this in the Java Plaform Module System of Java 9? The answer is straightforward: we can use an interface as a contract.

Before digging into that answer, let’s briefly look at what services are and how they work in Java 9. A service can consist of both interfaces and classes that specify the functionality of the service. A service provider implements a service. Multiple service providers can implement a service by providing custom implementations of it.

To provide a separation and a decoupling between service providers and service consumers, Java provides the ServiceLoader<S> class in package java.util of module java.base. This class wasn’t introduced in Java 9. It existed in Java since JDK 6 but has been enhanced in JDK 9 in order to support modules. Its role is to search, find, and load all the service providers for a service of type S. This is performed at runtime, not at compile-time. Application code invokes only the service and doesn’t refer to service providers.

Imagine that each three of the biggest German car manufacturers—Volkswagen, Daimler, and BMW—has its own service providers that implement a service called brake system. The service brake system provides the legal rules that a car brake system must fulfill. Because the brake systems produced by car manufacturers differ, each of them decided to provide its own implementations of the brake system rule by adhering to its prescription. Each manufacturer decided to build tools for being able to visualize the output generated by its brake systems. These tools act like service consumers and they’re aware only of the service brake system. They’re not aware of the service providers that implement the braking system service interface. What makes the interaction between service providers and service consumers possible, since they’re not aware of each other? Here comes the ServiceLoader in play. It makes the instances of services providers available to service consumers. In our case, ServiceLoader makes the different implementations of the brake system service available to the data visualizing tools.

Note

ServiceLoader’s role is to find and load all the service providers and make them accessible to service consumers.

Now let’s move to the modular world and explain how the concepts just presented fit in the new Java Platform Module System. In a modular context, we could use a provider module that has the role of registering a service on the service registry. Additionally we could make use of a consumer module that has the role of performing lookups for a service in the service registry.

Note

The service registry returns a service instance to the consumer module.

Even if we use modules, the workflow is the same as we know from the non-modular world. First, a provider registers a service or an interface. Second, a consumer searches a registry for any implementations of the interface. When any implementations are found, they can call upon the service via the interface without having to know about the concrete implementation.

The ServiceLoader API is used to decouple modules. A module should depend on an interface rather than on the implementation of another module. The implementation classes should not be exported. Instead, an interface should be exported. One of the strong features of the ServiceLoader API is that the system doesn’t need to know about all the service provider implementations right at compile-time. They’re computed only at runtime. So at compile-time this kind of dependency between modules doesn’t need to be declared inside module declarations.

Strong Coupling Between Modules

In Chapter 4 you learned what the requires and exports clauses mean. When a module requires another module, a strong coupling between the two modules occurs. If a module changes, then it may be necessary to adjust all the dependent modules. This not only makes the source code much harder to maintain, it can also considerably increase the time interval needed for implementing change requests in the code.

Let’s illustrate the strong coupling between modules with an explanatory example. We know from the module declaration of module java.rmi that it requires module java.logging. Module java.rmi has a dependency on module java.logging and also has access to the public types in the API exported by the java.logging module. As a result, there’s a strong and tight coupling between the modules java.rmi and java.logging. This has an important impact at both compile-time and runtime.

At compile-time, if the java.logging module isn’t found, the java.rmi module can’t be built. A module not found error will be thrown at compile-time.

At runtime, if java.logging isn’t found, no application that depends on java.rmi can start. This happens because the dependency of java.rmi on java.logging isn’t resolved. The following error will be thrown:

Error occurred during initialization of VM
java.lang.module.ResolutionException: Module java.logging not found, required by java.rmi
        at java.lang.module.Resolver.fail(java.base@9-ea/Resolver.java:841)
        at java.lang.module.Resolver.resolve(java.base@9-ea/Resolver.java:154)
        at java.lang.module.Resolver.resolveRequires(java.base@9-ea/Resolver.java:116)
        at java.lang.module.Configuration.resolveRequiresAndUses(java.base@9-ea/Configuration.java:311)
        at java.lang.module.ModuleDescriptor$1.resolveRequiresAndUses(java.base@9-ea/ModuleDescriptor.java:2483)
        at jdk.internal.module.ModuleBootstrap.boot(java.base@9-ea/ModuleBootstrap.java:272)
        at java.lang.System.initPhase2(java.base@9-ea/System.java:1927)

Using Services in JDK 9

This section describes what services are in Java 9 and how we can use them to prevent tight coupling between modules. Project Jigsaw can use the service registry as a layer of communication for the interaction between modules.

Modules can register their implementation class as a service in the service registry. These modules are called service provider modules. Their main role is to provide implementations of an interface. The service consumer modules use services that implement the interface that’s defined in the service registry. They don’t deal with the implementation classes that implement the interface defined in the service registry. Service consumer modules obtain objects from the service registry that implement the interface. In this way, they’re able to successfully call methods on this interface.

Note

A service consumer module and a service provider module don’t have a dependency upon each other.

The interface defined in the service registry implemented by the corresponding class represents the interaction between service providers and service consumers. The service registry instantiates the classes and afterwards provides this instance to the service consumer. The service consumer only has to know about the interface. It will return an object that implements the interface. It can also call methods on this object.

Note

A service can be declared as an abstract class or as an interface. However, it’s better from a design point of view to use an interface rather than an abstract class. When using an abstract class instead of an interface, a public static provider method has to be defined.

Let’s look at the syntax of the uses and provides clauses, which are mandatory in order to be able to implement the concepts present throughout this chapter.

Providing and Consuming Services

In this section you’ll learn how to consume and provide services in JDK 9. We present the clauses used in the module declarations to declare that a module provides a service implementation and that a module uses a service.

Providing a Service

he Java Platform Module System introduced a new construct called provides in the module descriptor in order for a module to be able to declare that it provides and exposes a service implementation for a specific service. Figure 6-1 illustrates its syntax.

A431534_1_En_6_Fig1_HTML.gif
Figure 6-1. The provides with clause

The provides with clause takes two parameters :

  • <interface_name> represents the name of the service interface. It specifies the name of the service for which the current module provides an implementation. The service could be either a class or an interface.

  • <class_name> represents the name of the class. It specifies the name of the class that implements the service interface. This class must be present in the current module. If it is not present in the current module, we get a compilation error.

A module uses the provides clause to inform the ServiceLoader that it provides an implementation of a service. Without this knowledge, the ServiceLoader wouldn’t have been able to load the service provider because it wouldn’t have been aware of it existence.

Note

JDK 9 allows you to have a service implementation as an interface. This wasn’t possible in previous versions of Java.

Jigsaw also allows for a single module to provide an implementation for a service and to also consume that service. But it does not allow for more than one provides statement to specify the same interface in a module declaration. A module declaration like this will never compile:

module myModule {
        provides myInterface with firstClass;
        provides myInterface with secondClass;
}

We mentioned previously the notion of consuming a service. The next subsection explains what this means and how it can be declared in Jigsaw.

Consuming a Service

In JDK 9, a module can explicitly declare that it consumes a service. For this, the service has to be discovered. Therefore, the uses clause was introduced in JDK 9 in the module declaration. It takes an interface as a parameter.

Figure 6-2 shows the syntax of the uses clause.

A431534_1_En_6_Fig2_HTML.gif
Figure 6-2. The uses clause

When should we use this clause? This clause should be used in modules that define a ServiceLoader<interface_name>, which loads service providers for the service with the name <interface_name>. If our module uses the ServiceLoader class to load services, then it’s mandatory to declare this in the module’s declaration using the uses clause, followed by the name of the service interface used.

This means that inside the module that declares the uses clause, a ServiceLoader is used, as in the following example:

Iterable<interface_name> ourInterfaces = ServiceLoader.load(interface_name.class);

Here, a ServiceLoader for services of type interface_name has been used.

Note

The service declared with the uses clause doesn’t have to reside in the same module. It can also reside in another module provided that there is readability between the two modules .

Retrieving a ServiceLoader

We’ve already seen how to obtain a service loader. It can be done using the load() method , which comes in four flavours, according to the JDK 9 API specification:

  • public static <S> ServiceLoader<S> load(Class<S> service)

    This method creates a new service loader for the given service type. It uses the context class loader of the current thread.

  • public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader)

    This method creates a new service loader and uses the given class loader to locate service providers for the service. Providers are located first in named modules and then in unnamed modules. Providers are located in all named modules of the class loader or to any class loader reachable via parent delegation.

  • public static <S> ServiceLoader<S> load(ModuleLayer layer, Class<S> service)

    This method creates a new service loader for the given service type to load service providers from modules in the given module layer and its ancestors. It doesn’t locate providers in unnamed modules.

  • public static <S> ServiceLoader<S> loadInstalled(Class<S> service)

    This method creates a new service loader for the given service type. It uses the platform class loader.

After we retrieve a ServiceLoader, we can iterate over all the service providers using the iterate() method. Another option would be to call the stream() method, which returns a stream to lazily load available providers. The syntax of the stream() method is as follows:

Stream<ServiceLoader.Provider<S>> stream()

Until now, we’ve seen how to retrieve a ServiceLoader and also how to provide and to consume a service. It’s time to see a practical example. We’ll use an example using one service consumer and one service provider. Then we’ll expand the example and show how to add more service providers.

Using One Consumer and One Provider

This section shows a simple example to illustrate the concepts presented earlier. Suppose we have three modules:

  • Module com.apress.moduleA contains a simple interface called ServiceExample.

  • Module com.apress.providerA defines a service provider that contains the class ServiceExampleImplementation1, which implements the interface from module com.apress.moduleA, ServiceExample.

  • Module com.apress.consumer defines a service consumer that creates a new service loader for the ServiceExample service and uses this service.

Listing 6-1 shows the interface ServiceExample, defined inside the module com.apress.moduleA.

Listing 6-1. The Interface ServiceExample from the Module com.apress.moduleA
package com.apress.moduleA.interfaces;

public interface ServiceExample {

        String printHelloWorld();
}

The module descriptor of the module com.apress.moduleA is depicted in Listing 6-2. The package com.apress.moduleA.interfaces, where the interface is located, is exported.

Listing 6-2. The Module Descriptor of the Module com.apress.moduleA
module com.apress.moduleA {

        exports com.apress.moduleA.interfaces;
}

Until now we’ve only defined an interface inside of a module. Next we’ll define the provider module. Listing 6-3 shows the implementation class of the interface from the module com.apress.providerA.

Listing 6-3. The Implementation Class of Interface ServiceExample from Module com.apress.providerA
package com.apress.providerA;

import com.apress.moduleA.interfaces.ServiceExample;

public class ServiceExampleImplementation1 implements ServiceExample {

    public ServiceExampleImplementation() {
    }


    @Override
    public String printHelloWorld() {


        return "Hello World from ServiceExampleImplementation1";
    }
}

In Listing 6-4 you see the module descriptor of module com.apress.providerA.

Listing 6-4. The Module Descriptor of Module com.apress.providerA
module com.apress.providerA {
         requires com.apress.moduleA;
         provides com.apress.moduleA.interfaces.ServiceExample with com.apress.providerA.ServiceExampleImplementation1;
}

The module descriptor states that it provides an implementation of the ServiceExample interface with the class ServiceExampleImplementation1. This means that inside the module we have a class called ServiceExampleImplementation1 that implements the interface ServiceExample. The module descriptor also requires the com.apress.moduleA module because it has to access the interface in order to be able to implement it.

Listing 6-5 shows the content of module com.apress.consumer.

Listing 6-5. The Main Class of Module com.apress.consumer
package com.apress.consumer;

import com.apress.moduleA.interfaces.ServiceExample;
import java.util.ServiceLoader;


public class Main {

        public static void main(String[] args) {
                Iterable<ServiceExample> services = ServiceLoader.load(ServiceExample.class);
                for(ServiceExample serviceExample : services) {
                                System.out.println(serviceExample.printHelloWorld());
                }
        }
}

The Main class obtains instances of ServiceExample using the ServiceLoader from the java.util package. This is done by creating a new service loader for the ServiceExample type inside the Main class of the com.apress.consumer module. All instances of type ServiceExample are retrieved by calling the load() method with the ServiceExample.class parameter. Finally, we iterate through them and call the printHelloWorld() method on them.

Listing 6-6 shows the module-info.java file of the module com.apress.consumer.

Listing 6-6. The Module Descriptor of Module com.apress.consumer
module com.apress.consumer {
         requires com.apress.moduleA;
         uses com.apress.moduleA.interfaces.ServiceExample;
}

Module com.apress.consumer requires module com.apress.moduleA because it needs access to the interface to call the corresponding method on it. Additionally, it specifies that it uses the interface ServiceExample. This tells the module system that the module com.apress.consumer wants to consume instances of the com.apress.moduleA.interfaces.ServiceExample interface.

Finally, we compile the modules specified using the following command:

javac -d output --module-source-path src $(find . -name "*.java")

Then we run the following command:

java --module-path output -m com.apress.consumer/com.apress.consumer.Main

The output is printed inside the console:

Hello World from ServiceExampleImplementation1

We saw in this example how to define a simple service provider, a service consumer, and an interface in a separate module for the communication between the service provider and the service consumer. Both service provider and service consumer require only the interface. This means there’s a dependency between the service provider and the interface and respectively a dependency between the service consumer and the interface. It’s important to remember that there’s no dependency between the service provider and the service consumer. As a consequence, we don’t have tight coupling between those two modules.

Note

You can find the source code for this example in the directory /ch06/oneConsumerOneProvider.

Using One Consumer and Two Providers

Until now we’ve had only one service provider, but we can define many service providers and at the same time keeping the loosely coupled relation between the service providers at the service consumers. We’ll illustrate this concept with an example by defining another provider module.

Listing 6-7 shows the implementation class of the interface ServiceExample in the module com.apress.providerB.

Listing 6-7. The Implementation Class of Interface ServiceExample from Module com.apress.providerB
package com.apress.providerB;

import com.apress.moduleA.interfaces.ServiceExample;

public class ServiceExampleImplementation2 implements ServiceExample {

    public ServiceExampleImplementation2() {
    }


    @Override
    public String printHelloWorld() {
        return "Hello World from ServiceExampleImplementation2";
    }
}

Listing 6-8 shows the module descriptor of module com.apress.providerB .

Listing 6-8. The Module Descriptor of Module com.apress.providerB
module com.apress.providerB {
         requires com.apress.moduleA;
         provides com.apress.moduleA.interfaces.ServiceExample with com.apress.providerB.ServiceExampleImplementation2;
}

The module descriptor of module com.apress.providesB provides an implementation of the ServiceExample interface with the class ServiceExampleImplementation2.

By compiling and running the modules, the following result is printed in the console:

Hello World from ServiceExampleImplementation2
Hello World from ServiceExampleImplementation1

In this example, we defined two provider modules and saw how they interact in the context of the module system. None of the provider modules has a dependency upon the consumer module.

In our example, neither the provider modules nor the consumer module exports their packages. In this way, they’re encapsulated and can’t be accessed from outside. Nonetheless, Jigsaw has the capability to instantiate classes of type ServiceExampleImplementation1 because it implements the interface ServiceExample, which is defined using the provides directive inside the module-info.java of module com.apress.providerB.

Note

You can find the source code for this example in the directory /ch06/oneConsumerTwoProviders .

Summary

In this chapter we talked about services. Services are used to decouple modules by specifying a contract in form of an interface. They allow loose coupling between service consumer and service providers. The concept of loose coupling is very important in software development, especially when we’re talking about large software applications. We showed how to declare that a module provides a service by using the new construct provides … with inside the module descriptor module-info.java. Afterwards, we talked about how to declare that a module consumes a service by using the new construct uses inside the module descriptor. Further, we discussed how to retrieve a ServiceLoader.

We looked at two examples of how we can define one service consumer and one service provider, respectively one service consumer and two service providers.

In Chapter 7 you’ll learn about the Jlink tool, which lets us create custom runtime images that contain only the modules we need.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.218.31.165