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:
- 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.
- Extention: 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 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.