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 extend 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 its 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 the 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 provider implementations and put them in an internal cache.

3. SPI Samples in the 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 se**rvice 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 BigDecimal ask;
    private BigDecinal bid;
    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 delegates 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 final class ExchangeRate {

    private static final String DEFAULT_PROVIDER = "com.baeldung.rate.spi.YahooFinanceExchangeRateProvider";

    //All providers
    public static List<ExchangeRateProvider> providers() {
        List<ExchangeRateProvider> services = new ArrayList<>();
        ServiceLoader<ExchangeRateProvider> loader = ServiceLoader.load(ExchangeRateProvider.class);
        loader.forEach(services::add);
        return services;
    }

    //Default provider
    public static ExchangeRateProvider provider() {
        return provider(DEFAULT_PROVIDER);
    }

    //provider by name
    public static ExchangeRateProvider provider(String providerName) {
        ServiceLoader<ExchangeRateProvider> loader = ServiceLoader.load(ExchangeRateProvider.class);
        Iterator<ExchangeRateProvider> it = loader.iterator();
        while (it.hasNext()) {
            ExchangeRateProvider provider = it.next();
            if (providerName.equals(provider.getClass().getName())) {
                return provider;
            }
        }
        throw new ProviderNotFoundException("Exchange Rate provider " + providerName + " not found");
    }
}

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.

Note 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. Run this command in the root of the java-spi module:

mvn clean package

Then we run our application with the Java command without taking into account the provider. Run this command in hte root of the java-spi module:

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

The result of the above command will be empty because no providers was found.

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

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:./exchange-rate-impl/target/exchange-rate-impl-1.0.0-SNAPSHOT.jar:./exchange-rate-impl/target/depends/* com.baeldung.rate.app.MainApp

We can see that our provider is loaded and it will print the output of the ExchangeRate app.

NOTE: To provide multiple dependencies in the class path you have to choose between two different separators based on the operating system**:** 

  • For Linux, the separator is colon(:).
  • For Windows the separator is semicolon(;).

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.


« 上一篇: AutoFactory介绍
» 下一篇: Java每周,问题227