BT

InfoQ Homepage Articles Super Charge the Module Aware Service Loader in Java 11

Super Charge the Module Aware Service Loader in Java 11

Bookmarks

Key Takeaways

  • A Java module is a self-contained, self-describing component that hides internal details and provides interfaces, classes, and services for consumption by clients
  • A service is a set of well-known interfaces or classes (usually abstract). A service provider is a concrete implementation of a service. Java’s ServiceLoader is a facility to load service providers that implement a given service interface.
  • Java’s service loading mechanism can be extended through a library to reduce the boilerplate code and provide useful features like injecting service references and activating a given service provider.

If you had a chance to use the Simple Logging Facade for Java (SLF4J) in one of your Java projects, you know that it allows you, the end-user, to plug-in your choice of a logging framework like java.util.logging (JUL) or logback or log4j at deployment time. During development, you typically use the SLF4J API which provides an interface or an abstraction that you can use to log your application messages.

Say, during deployment you initially chose JUL as your logging framework but then you noticed that the logging performance is not up to par. Because your application is coded to the SLF4J interface, you can easily plug-in a highly performant logging framework like log4j without any code changes and redeploy your app. Your application is in essence an extensible application. It has the ability to pick a compatible logging framework that is available on the classpath at runtime through SLF4J.

An extensible application is one whose specific parts can be extended or enhanced without the need for code changes to the application’s core code base. In other words, the application allows for loose coupling by programming to interfaces and delegating the work to locate and load concrete implementations to a central framework.

Java’s answer to provide developers the ability to design and implement extensible applications without modifying the original code base came in the form of services and the ServiceLoader class--introduced in Java version 6. SLF4J uses this service loading mechanism to provide its plug-in model that we described earlier.

Of course, dependency injection or inversion of control frameworks are another way to achieve the same and more. But, we will focus on the native solution for the purpose of this article. To understand the ServiceLoader mechanism, we need to look at a few definitions in the context of Java:

  • Service: A service is a well known interface or class (usually abstract)
  • Service Provider: A service provider is a concrete implementation of a service
  • ServiceLoader: The ServiceLoader class is a facility to locate and load service providers that implement a given service

With those definitions in place, let’s see how we can build an extensible application. Say a fictitious ecommerce platform allows customers to choose from a list of payment service providers to be deployed on their site. The platform can be coded against a payment service interface with a mechanism to load the desired payment service provider. Developers and vendors can provide the payments feature with one or more specific implementations. Let’s begin by defining a payment service interface:

package com.mycommerce.payment.spi;

public interface PaymentService {
    Result charge(Invoice invoice);
}

At some point during the ecommerce platform’s startup, we would request a payment service from Java’s ServiceLoader class using code similar to this:


import java.util.Optional;
import java.util.ServiceLoader;

import com.mycommerce.payment.spi;

Optional<PaymentService> loadPaymentService() {
    return ServiceLoader
            .load(PaymentService.class)
            .findFirst();
}

The default ServiceLoader’s “load” method searches the application classpath with the default class loader. You can use the overloaded “load” method to pass a custom class loader to implement more sophisticated searches for service providers. In order for the ServiceLoader to locate service providers, the service providers should implement the service interface--in our case the PaymentService interface. Here’s an example payment service provider:


package com.mycommerce.payment.stripe;

public class StripeService implements PaymentService {
   
    @Override
    public Result charge(Invoice invoice) {
        // charge the customer and return the result.
        ...
        return new Result.Builder()
                .build();
    }
}

Next, the service provider should register itself by creating a provider configuration file, which has to be stored in the META-INF/services directory of the service provider's jar file. The name of the configuration file is the fully qualified class name of the service provider, in which each component of the name is separated by a period (.). The file itself should contain the fully qualified class names of the service providers, one per line. The file must be UTF-8 encoded as well. You can include comments in the file by beginning the comment line with the hash sign (#).

In our case, to register the StripeService as a service provider we have to create a file named “com.mycommerce.payment.spi.Payment” and add the following line:

com.mycommerce.payment.stripe.StripeService

Using the above setup and configuration the ecommerce platform can load new payment service providers as they become available without any code changes. Following this pattern would allow you to build extensible applications.

Now, 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.

Let’s take a look at how we can define and use services in the context of the new Java module system. Using the PaymentService that we defined earlier, let’s create the corresponding module descriptor:

module com.mycommerce.payment {
    exports com.mycommerce.payment.spi;
}

The main module of the ecommerce platform can now be coded against the payment service interface by configuring its module descriptor:

module com.mycommerce.main {
    requires com.mycommerce.payment;
 
    uses com.mycommerce.payment.spi.PaymentService;
}

Notice the “uses” keyword in the above module descriptor. This is how we notify Java about our intention to ask its ServiceLoader class to locate and load concrete implementations of the payment service interface. At some point during application startup (or at a later time), the main module would request a payment service from the ServiceLoader using code similar to this:


import java.util.Optional;
import java.util.ServiceLoader;

import com.mycommerce.payment.spi;

Optional<PaymentService> loadPaymentService() {
    return ServiceLoader
            .load(PaymentService.class)
            .findFirst();
}

We have to follow a few rules in order for a payment service provider to be located by the ServiceLoader. The obvious thing is for the service provider to implement the PaymentService interface. Next, the module descriptor of the payment service provider should specify its intention to provide the payment service to clients:


module com.mycommerce.payment.stripe {
    requires com.mycommerce.payment;

    exports com.mycommerce.payment.stripe;
 
    provides com.mycommerce.payment.spi.PaymentService
        with com.mycommerce.payment.stripe.StripeService;
}

As you can see, we used the “provides” 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. Note that multiple concrete implementations within a single module can provide the same service interface. A module can also provide multiple services.

So far so good, but when we start to implement a full-blown system using this new services mechanism, we will soon realize that we have to write boilerplate code each time we need to locate and load a service. It gets more tedious and a tad more complex when we have to run some initialization logic each time we load a service provider.

A typical thing to do is to refactor the boilerplate code into a utility class and add it as part of a common module shared by other modules in our application. While that’s a good first step, due to the strong encapsulation and reliable configuration guarantees provided by the Java module system, our utility method will fail to use the ServiceLoader class and load services.

Since the common module has no knowledge of the given service interface and since it doesn’t include the “uses” clause in its module descriptor, the ServiceLoader cannot locate providers that implement the service interface even though they might be present on the module path. Not only that, but if you add the “uses” clause to the common module descriptor, it defeats the purpose of encapsulation and worse, introduces circular dependency.

We will build a custom library called Susel to solve the issues mentioned above. The primary goal of this library is to assist developers in building modular and extensible applications that leverage the native Java module system. The library will remove the need for boilerplate code required to locate and load services. In addition, it enables service provider authors the ability to depend on other services that are automatically located and injected into the given service provider. Susel will also provide a simple activate lifecycle event that can be used by a service provider to configure itself and run some initialization logic.

First, let’s see how Susel solves the problem of locating services without an explicit “uses” clause in its module descriptor. Java’s Module class method, “addUses()”, provides a way to update a module and add a service dependence on a given service interface. The method was exclusively provided to support libraries like Susel that use the ServiceLoader class to locate services on behalf of other modules. Here’s how we can use this method:

var module = SuselImpl.class.getModule();
module.addUses(PaymentService.class);

As you can see, Susel gets a reference to its own module and updates itself to ensure that the requested service can be seen by the ServiceLoader. We should be aware of a couple of caveats when calling the “addUses()” method on the module API. Firstly, if the caller’s module is not the same module (“this”), an IllegalCallerException is thrown. Secondly, the method doesn’t work with unnamed modules and automatic modules.

We mentioned that Susel can locate and inject other services into a given service provider. Susel provides this functionality with the help of annotations and the associated metadata produced during build time. Let’s look at the annotations.

The @ServiceReference annotation is used to mark a public method in the referer class (service provider) that will be used by Susel to inject the specified service. The annotation takes an optional cardinality parameter. Cardinality is used by Susel to decide on the number of services to inject and whether or not the requested service is mandatory or optional.

public @interface ServiceReference {
    /**
     * Specifies the service cardinality being requested by the referer.
     * Default value is {@link Cardinality#ONE}
     *
     * @return the service cardinality being requested by the referer.
     */
    Cardinality cardinality() default Cardinality.ONE;
}

The @Activate annotation is used to mark a public method in the service provider class that will be used by Susel to activate an instance of the service provider. Typical use case to hook into this event would be to initialize key aspects like configuration of a service provider.


public @interface Activate {}

Susel provides a tool that uses reflection to build the metadata about a given module. The tool reads the module descriptor to identify service providers, and for each service provider, the tool scans for methods decorated with @ServiceReference and @Activate annotations, and creates a metadata entry. The tool then saves the metadata items in a file called susel.metadata. The file will be placed under META-INF folder and packaged with the jar. Now, at runtime, when a module requests Susel for service providers implementing a specific service interface, Susel executes the following steps:

  • Call Susel module’s addUses() method to enable the ServiceLoader to locate the requested service
  • Call the ServiceLoader to get the service providers iterator
  • For each service provider, load and get the metadata of the module holding the service provider
  •  Locate the metadata item corresponding to the service provider
  • For each service reference specified in the metadata item repeat from step 1
  • If the optional activate event is registered, invoke the activation by passing the global context
  • Return the list of fully loaded service providers

Here’s a high level code snippet that executes the above steps:

public <S> List<S> getAll(Class<S> service) {
    List<S> serviceProviders = new ArrayList<>();
       
    // Susel's module should indicate the intention to use the given
    // service so that the ServiceLoader can lookup the requested

    // service providers
    SUSEL_MODULE.addUses(service);

    // Pass the application module layer that typically loads Susel
    var iterator = ServiceLoader.load(SUSEL_MODULE.getLayer(), service);
    for (S serviceProvider : iterator) {
        // Load metadata to inject references and activate service
        prepare(serviceProvider);
        serviceProviders.add(serviceProvider);
    }
   
    return serviceProviders;
}

Notice how we use the overloaded load() method of the ServiceLoader class to pass the application module layer. This overloaded method, introduced in Java 9, creates a new service loader for the given service interface and loads service providers from modules in the given module layer and its ancestors.

It’s worth mentioning that Susel uses metadata files in order to avoid heavy reflection during application runtime to identify service references and activation methods while locating and loading service providers. On a side note, while Susel may share a few traits from OSGI (a mature and powerful module system available in the Java ecosystem) and/or an IoC framework, the goal of Susel is to enhance the service loading mechanism available through the native Java module system and reduce the boilerplate code required to locate and invoke services.

Let’s take a look at how we can leverage Susel in our payment services example. Say we implement a payment service using Stripe. Here’s the code snippet showcasing Susel’s annotations in action:

package com.mycommerce.payment.stripe;

import io.github.udaychandra.susel.api.Activate;
import io.github.udaychandra.susel.api.Context;
import io.github.udaychandra.susel.api.ServiceReference;

public class StripeService implements PaymentService {
    private CustomerService customerService;
    private String stripeSvcToken;
   
    @ServiceReference
    public void setCustomerService(CustomerService customerService) {
        this.customerService = customerService;
    }
   
    @Activate
    public void activate(Context context) {
        stripeSvcToken = context.value("STRIPE_TOKEN");
    }

    @Override
    public Result charge(Invoice invoice) {
        var customer = customerService.get(invoice.customerID());
        // Use the customer service and stripe token to call Stripe
        // service and charge the customer
        ...
        return new Result.Builder()
                .build();
    }
}

We have to call Susel’s tool in order to generate the metadata during the build phase. A ready to use gradle plugin was made available to automate this step. Let’s take a look at the sample build.gradle file that automatically configures this tool to be invoked during the build phase.

plugins {
    id "java"
    id "com.zyxist.chainsaw" version "0.3.0"
    id "io.github.udaychandra.susel" version "0.1.2"
}

dependencies {
    compile "io.github.udaychandra.susel:susel:0.1.2"
}

Notice how we used two custom plugins to work with the Java module system and Susel. The chainsaw plugin helps gradle build modular jars. Susel plugin helps in creating and packaging the metadata about the service providers.

Finally, let’s take a look at a code snippet that bootstraps Susel during application startup and retrieves a payment service provider from Susel:

package com.mycommerce.main;

import com.mycommerce.payment.spi.PaymentService;
import io.github.udaychandra.susel.api.Susel;

public class Launcher {
    public static void main(String... args) {
        // Ideally config should be loaded from an external source
        Susel.bootstrap(Map.of("STRIPE_TOKEN", "dev_token123"));
        ...
        // Susel will load the Stripe service provider that it finds
        // in its module layer and prepares the service for use by clients
        var paymentService = Susel.get(PaymentService.class);       
        paymentService.charge(invoice);
    }
}

We have now reached the point where we can build the modular jars using gradle and run the sample application. Here’s the command to run:


java --module-path :build/libs/:$JAVA_HOME/jmods \
     -m com.mycommerce.main/com.mycommerce.main.Launcher

To support the Java module system, new options have been added to existing command tools like “java”. Let’s take a look at the new options that we used in the above command:

-p or --module-path is used to tell Java to look into specific folders that contain java modules.

-m or --module is used to specify the module and the main class used to start the application.

When you start developing applications using the Java module system, you can take advantage of the module resolution strategy to create special distributions of the Java Runtime Environment (JRE). These custom distributions or runtime images contain only those modules that are required to run your application. Java 9 introduced a new assembly tool called jlink that can be used to create custom runtime images. However, one should be aware of how module resolution is done by this tool compared to the module resolution done during runtime by the ServiceLoader. Since service providers are almost always considered optional, jlink doesn’t automatically resolve modules that contain service providers based on the “uses” clauses. jlink has a few options that can be invoked to help us resolve service provider modules:

--bind-services is used to let jlink resolve all service provider modules and their dependencies.

--suggest-providers is used to ask jlink to suggest providers that implement the service interfaces included in the module path.

It is recommended to make use of the --suggest-providers and add only those modules that make sense for your particular use case instead of blindly adding all available providers using --bind-services. Let’s see the --suggest-providers switch in action using our payment services example:

"${JAVA_HOME}/bin/jlink" --module-path "build/libs" \
    --add-modules com.mycommerce.main \
    --suggest-providers com.mycommerce.payment.PaymentService

The output of the above command would look something similar to this:

Suggested providers:
  com.mycommerce.payment.stripe provides
  com.mycommerce.payment.PaymentService used by
  com.mycommerce.main

Equipped with this knowledge you can now create a custom image and package all the modules that are required to run your application and load the desired service providers.

Conclusion

We described the service loading mechanism in Java and the changes made to it in order to support the native Java module system. We then looked at an experimental library called Susel that can assist developers in building modular and extensible applications that leverage the native Java module system. The library removes the boilerplate code required to locate and load services. In addition, it enables service provider authors the ability to depend on other services that are automatically located and injected into the given service providers.

About the Author

Uday Tatiraju is a principal engineer at Oracle with over a decade of experience in ecommerce platforms, search engines, backend systems, and web and mobile programming.

Rate this Article

Adoption
Style

Hello stranger!

You need to Register an InfoQ account or or login to post comments. But there's so much more behind being registered.

Get the most out of the InfoQ experience.

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

Community comments

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

BT

Is your profile up-to-date? Please take a moment to review and update.

Note: If updating/changing your email, a validation request will be sent

Company name:
Company role:
Company size:
Country/Zone:
State/Province/Region:
You will be sent an email to validate the new email address. This pop-up will close itself in a few moments.