Unveiling The Power of Service Loader of Java 11

Loading the concrete implementations of API at runtime

Sugandha Sapra
6 min readMay 21, 2021
Image Credit: Ross Sneddon@Unsplash

The interconnection of interdependency between two modules is defined by the interface between them because any two modules cannot be connected if there exists no interface between them.

Includehelp

The Service Loader allows separating an API and its implementations in different JARs. The client code depends on the API only, while at runtime, the implementation(s) that is (are) on the classpath will be used. This is a great way to decouple the client code from the implementing one. Of course, dependency injection or inversion of control frameworks is another way to achieve the same and more. But, we will focus on the native solution for this article.

Let’s understand the mechanism and the key terms which will be focussing throughout the post with the help of an example. We will be illustrating by building the “Greeter” app, a small CLI utility, that can produce greetings in different languages.

  • Service: A service is a well-known interface or class (usually abstract) for which zero, one, or many service providers exist. If the service is one interface, then it is often termed as a service provider interface(SPI).

Here is the Greeter service, which is to be implemented by each language service provider:

Greeter.java
  • Service Provider: A service provider is a concrete implementation of a service

Here’s an example greeter service provider:

EnglishGreeter.java
  • ServiceLoader: The ServiceLoader class gives a facility to locate and load service providers that implement a given service in the run time environment at a time of an application

The client code for loading the greeter implementations from within the application layer could look like this :

App.java

In this post, I’ll be covering how we can load all the implementations :

  1. Within the same module
  2. In a multi-module project through service provider configuration file
  3. In a multi-module project through Java 9 modules

Within the same module

When an API and its implementations are existing in the same module as shown below, we can load our service providers through a provider configuration file.

project-structure

We put this file in the resource directory META-INF/services. The file name is the fully-qualified name of the SPI and its content is the fully-qualified name of the SPI implementation. So, creating a file in resources/META-INF/services with the name com.core.api.Greeter and declare all implementations of Greeter in it, will yield the desired result.

com.core.impl.HindiGreeter
com.core.impl.EnglishGreeter
com.core.impl.FrenchGreeter
com.core.impl.SpanishGreeter

In a multi-module project

In this case, the API and its implementations are existing in different modules. I’ve used Gradle as a build tool in this post. Following is the project structure which can be viewed using gradle -q project

project-structure
  • greeter-api : It comprises a Greeter interface
  • greeters : It comprises multilingual greeters i.e english-greeter,french-greeter,hindi-greeter,spanish-greeter. All of which are separate gradle projects.
  • Add greeter-api dependency to the build.gradle of both client and service provider modules.
compile(project(":greeter-app:greeter-api"))

There are two ways through which we can discover the implementations

  • Service Provider Configuration File
  • Java 9 Modules

Service Provider Configuration File

  • As we did in the previous section, create a file underMETA-INF/services with the name of SPI, com.core.spi.Greeter.The content of the file is the fully qualified class name of the SPI implementation. Following is the content in the english-greeter project’s configuration file.
com.core.impl.EnglishGreeter

In fact, we can provide as many modules as we need for the service provider and make them available in the classpath of the main app.

Java 9 Modules

With the introduction of the module system in Java 9, the services mechanism has been enhanced to support the strong encapsulation and configuration provided by modules.

A Java module is a self-contained, self-describing component that hides internal details and provides interfaces, classes, and services for consumption by clients.

We will be modularizing the API, the implementations & the client through module descriptor file i.e module-info.java.

  • Modularizing the API

For other modules — implementation(s) and client — to use the API, the package containing the Greeter interface needs to be exported as shown below.

module-info.java
  • Modularizing the Implementations

We will use theprovides keyword to specify the service provided by this module. The with keyword is used to mention the concrete class that implements the given service interface.

This will be the descriptor file for english-greeter project.

module-info.java
  • Modularizing the client

The common module knows the given service interface as it includes the usesclause in its module descriptor, the ServiceLoader can locate providers that implement the service interface present on the module path. This is how we notify Java about our intention to ask its ServiceLoader class to locate and load concrete implementations of the Greeter interface.

module-info.java
  • Add the following task to the root projectsbuild.gradle to treat each jar as a module and make them available in the module path.
allprojects {
plugins.withType(JavaPlugin).configureEach {
java {
modularity.inferModulePath = true
}
}
}

Application Execution

There are many ways through which we can discover our providers in the client module classpath. Some of them are as follows :

  • Copy all the providers’ jar inside a common path (/build/libs/depends) and execute the jar as shown below :
java -cp greeter-app/greeters/build/libs/depends/*:greeter-app/greeter-api/build/libs/greeter-api-1.0-SNAPSHOT.jar:greeter-app/build/libs/greeter-app–1.0-SNAPSHOT.jar com.core.main.GreeterApp
  • We can explicitly add all the provider’s dependency in the build.gradleof a client module.As a turnaround, we created a Gradle task to avoid the pain of adding all the provider’s dependency as shown below.
rootProject.allprojects.each {
if (it.name.matches("[a-zA-Z]+-greeter")) {
compile(project(":greeter-app:greeters:" + it.name));
}
}

We can see that our provider is loaded and upon execution, we get the expected output as shown below.

output

Now that we have explored the Java Service Loader mechanism through this post, it should be clear to see how this can be used to dynamically load all the concrete implementations of any service.

The code is available over Github.

Hope you guys have enjoyed this post. Happy Coding!!!

--

--