<

I just announced the new Spring 5 modules in REST With Spring:

>> CHECK OUT THE COURSE

1. Overview

Java 6 has introduced a feature for discovering and loading implementations matching a given interface: Service Provider Interface (SPI).

In this tutorial, we’ll introduce the components of Java SPI and show how we can apply it to a practical use case.

2. Terms and Definitions of Java SPI

Java SPI defines four main components

2.1. Service

A well-known set of programming interfaces and classes that provide access to some specific application functionality or feature.

2.2. Service Provider Interface

An interface or abstract class that acts as a proxy or an endpoint to the service.

If the service is one interface, then it is the same as a service provider interface.

Service and SPI together are well-known in the Java Ecosystem as API.

2.3. Service Provider

A specific implementation of the SPI. The Service Provider contains one or more concrete classes that implement or extends the service type.

A Service Provider is configured and identified through a provider configuration file which we put in the resource directory META-INF/services. The file name is the fully-qualified name of the SPI and his content is the fully-qualified name of the SPI implementation.

The Service Provider is installed in the form of extensions, a jar file which we place in the application classpath, the java extension classpath or user-defined classpath.

2.4. ServiceLoader

At the heart of the SPI is the ServiceLoader class. This has the role of discovering and loading implementations lazily. It uses the context classpath to locate providers implementations and put them in an internal cache.

3. SPI Samples in Java Ecosystem

Java provides many SPIs, here are some samples of the service provider interface and the service that it provides:

4. Showcase: a Currency Exchange Rates Application

Now that we understand the basics, let’s describe the steps that are required to set up an exchange rate application.

To highlight these steps, we need to use at least three projects: exchange-rate-api, exchange-rate-impl, and exchange-rate-app.

In sub-section 4.1., we’ll cover the Service, the SPI, and the ServiceLoader through the module exchange-rate-api, then in sub-section 4.2. we’ll implement our service provider in the exchange-rate-impl module, and finally, we’ll bring everything together in sub-section 4.3 through the module exchange-rate-app.

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

4.1. Building our API

We start by creating a maven project called exchange-rate-api. It’s good practice that the name ends with the term api, but we can call it whatever.

Then we create a model class for representing rates currencies:

package com.baeldung.rate.api;

public class Quote {
    private String currency;
    private LocalDate date;
    ...
}

And then we define our Service for retrieving quotes by creating the interface QuoteManager:

package com.baeldung.rate.api

public interface QuoteManager {
    List<Quote> getQuotes(String baseCurrency, LocalDate date);
}

Next, we create an SPI for our service:

package com.baeldung.rate.spi;

public interface ExchangeRateProvider {
    QuoteManager create();
}

And finally, we need to create a utility class ExchangeRate.java that can be used by client code. This class delegate to ServiceLoader.

First, we invoke the static factory method load() to get an instance of ServiceLoader:

ServiceLoader<ExchangeRateProvider> loader = ServiceLoader .load(ExchangeRateProvider.class);

And then we invoke the iterate() method to search and retrieve all available implementations.

Iterator<ExchangeRateProvider> = loader.iterator();

The search result is cached so we can invoke the ServiceLoader.reload() method in order to discover newly installed implementations:

Iterator<ExchangeRateProvider> = loader.reload();

And here’s our utility class:

public class ExchangeRate {

    ServiceLoader<ExchangeRateProvider> loader = ServiceLoader
      .load(ExchangeRateProvider.class);
 
    public Iterator<ExchangeRateProvider> providers(boolean refresh) {
        if (refresh) {
            loader.reload();
        }
        return loader.iterator();
    }
}

Now that we have a service for getting all installed implementations, we can use all of them in our client code to extend our application or just one by selecting a preferred implementation.

It is to be noted, that this utility class is not required to be part of the api project. Client code can choose to invoke ServiceLoader methods itself.

4.2. Building the Service Provider

Let’s now create a Maven project named exchange-rate-impl and we add the API dependency to the pom.xml:

<dependency>
    <groupId>com.baeldung</groupId>
    <artifactId>exchange-rate-api</artifactId>
    <version>1.0.0-SNAPSHOT</version>
</dependency>

Then we create a class that implements our SPI:

public class YahooFinanceExchangeRateProvider 
  implements ExchangeRateProvider {
 
    @Override
    public QuoteManager create() {
        return new YahooQuoteManagerImpl();
    }
}

And here the implementation of the QuoteManager interface:

public class YahooQuoteManagerImpl implements QuoteManager {

    @Override
    public List<Quote> getQuotes(String baseCurrency, LocalDate date) {
        // fetch from Yahoo API
    }
}

In order to be discovered, we create a provider configuration file:

META-INF/services/com.baeldung.rate.spi.ExchangeRateProvider

The content of the file is the fully qualified class name of the SPI implementation:

com.baeldung.rate.impl.YahooFinanceExchangeRateProvider

4.3. Putting it Together

Finally, let’s create a client project called exchange-rate-app and add the dependency exchange-rate-api to the classpath:

<dependency>
    <groupId>com.baeldung</groupId>
    <artifactId>exchange-rate-api</artifactId>
    <version>1.0.0-SNAPSHOT</version>
</dependency>

At this point, we can call the SPI from our application:

ExchangeRate.providers().forEach(provider -> ... );

4.4. Running the application

Let’s now focus on building all of our modules:

mvn clean package

Then we run our application with the Java command without taking into account the provider:

java -cp ./exchange-rate-api/target/exchange-rate-api-1.0.0-SNAPSHOT.jar:./exchange-rate-app/target/exchange-rate-app-1.0.0-SNAPSHOT.jar com.baeldung.rate.app.MainApp

Now we’ll include our provider in java.ext.dirs extension and we run the application again:

java -Djava.ext.dirs=$JAVA_HOME/jre/lib/ext:./exchange-rate-impl/target:./exchange-rate-impl/target/depends -cp ./exchange-rate-api/target/exchange-rate-api-1.0.0-SNAPSHOT.jar:./exchange-rate-app/target/exchange-rate-app-1.0.0-SNAPSHOT.jar com.baeldung.rate.app.MainApp

We can see that our provider is loaded.

5. Conclusion

Now that we have explored the Java SPI mechanism through well-defined steps, it should be clear to see how to use the Java SPI to create easily extensible or replaceable modules.

Although our example used the Yahoo exchange rate service to show the power of plugging-in to other existing external APIs, production systems don’t need to rely on third-party APIs to create great SPI applications.

The code, as usual, can be found over on Github.

I just announced the new Spring 5 modules in REST With Spring:

>> CHECK OUT THE LESSONS

Leave a Reply

Be the First to Comment!

avatar
  Subscribe  
Notify of