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:
- CurrencyNameProvider: provides localized currency symbols for the Currency class.
- LocaleNameProvider: provides localized names for the Locale class.
- TimeZoneNameProvider: provides localized time zone names for the TimeZone class.
- DateFormatProvider: provides date and time formats for a specified locale.
- NumberFormatProvider: provides monetary, integer and percentage values for the NumberFormat class.
- Driver: as of version 4.0, the JDBC API supports the SPI pattern. Older versions uses the Class.forName() method to load drivers.
- PersistenceProvider: provides the implementation of the JPA API.
- JsonProvider: provides JSON processing objects.
- JsonbProvider: provides JSON binding objects.
- Extension: provides extensions for the CDI container.
- ConfigSourceProvider: provides a source for retrieving configuration properties.
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.