Fundamentals 12 min read

Master Java SPI: Build Decoupled Services with Service Provider Interface

This guide explains Java's Service Provider Interface (SPI), detailing its purpose for decoupling implementations, step-by-step project setup, code examples, configuration files, and an in-depth analysis of ServiceLoader's internal mechanisms, enabling developers to create flexible, extensible Java applications.

JD Cloud Developers
JD Cloud Developers
JD Cloud Developers
Master Java SPI: Build Decoupled Services with Service Provider Interface

1. Introduction to SPI

SPI (Service Provider Interface) is a mechanism in Java that allows discovery of implementations, enabling decoupling between interfaces and their implementations. For example, JDBC uses SPI to load database drivers, and frameworks like Spring Boot and Dubbo also leverage it.

2. SPI Getting Started Example

2.1 Create Projects

Four Maven projects are created:

spi-interface – defines the Person interface.

spi-impl1 – provides the Teacher implementation.

spi-impl2 – provides the Student implementation.

spi-test – loads all implementations via SPI.

Project structure
Project structure

2.2 Define SPI Interface

package com.jd.spi;
public interface Person {
    String favorite();
}

2.3 Create Implementation 1 (Teacher)

2.3.1 Class

package com.jd.spi;
public class Teacher implements Person {
    public String favorite() {
        return "老师喜欢给学生上课";
    }
}

2.3.2 SPI configuration file

In resources/META-INF/services/com.jd.spi.Person add the fully qualified name of Teacher.

Teacher SPI config
Teacher SPI config

2.4 Create Implementation 2 (Student)

2.4.1 Class

package com.jd.spi;
public class Student implements Person {
    public String favorite() {
        return "学生喜欢努力学习";
    }
}

2.4.2 SPI configuration file

Similarly, add Student 's fully qualified name to resources/META-INF/services/com.jd.spi.Person.

Student SPI config
Student SPI config

2.5 Create Test Project

2.5.1 Maven dependencies

<dependencies>
    <dependency>
        <groupId>org.example</groupId>
        <artifactId>spi-interface</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
    <dependency>
        <groupId>org.example</groupId>
        <artifactId>spi-impl1</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
    <dependency>
        <groupId>org.example</groupId>
        <artifactId>spi-impl2</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
</dependencies>

2.5.2 Test class

package com.jd.spi;
import java.util.Iterator;
import java.util.ServiceLoader;
public class SPITest {
    public static void main(String[] args) {
        ServiceLoader<Person> loader = ServiceLoader.load(Person.class);
        for (Iterator<Person> it = loader.iterator(); it.hasNext(); ) {
            Person person = it.next();
            System.out.println(person.favorite());
        }
    }
}

Running the test prints:

Test output
Test output

Java's SPI mechanism discovers all Person implementations and invokes their favorite() method.

3. SPI Mechanism Principles

3.1 Core fields of ServiceLoader

public final class ServiceLoader<S> implements Iterable<S> {
    private static final String PREFIX = "META-INF/services/";
    private final Class<S> service;
    private final ClassLoader loader;
    private final AccessControlContext acc;
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
    private LazyIterator lookupIterator;
}

These fields are used to locate configuration files under META-INF/services.

3.2 ServiceLoader iterator

public Iterator<S> iterator() {
    return new Iterator<S>() {
        Iterator<Map.Entry<String,S>> knownProviders = providers.entrySet().iterator();
        public boolean hasNext() {
            if (knownProviders.hasNext()) return true;
            return lookupIterator.hasNext();
        }
        public S next() {
            if (knownProviders.hasNext()) return knownProviders.next().getValue();
            return lookupIterator.next();
        }
        public void remove() { throw new UnsupportedOperationException(); }
    };
}

The iterator ultimately calls lookupIterator.next().

3.3 lookupIterator.next() and nextService()

public S next() {
    if (acc == null) {
        return nextService();
    } else {
        PrivilegedAction<S> action = new PrivilegedAction<S>() {
            public S run() { return nextService(); }
        };
        return AccessController.doPrivileged(action, acc);
    }
}

private S nextService() {
    if (!hasNextService()) throw new NoSuchElementException();
    String cn = nextName;
    nextName = null;
    Class<?> c = null;
    try { c = Class.forName(cn, false, loader); }
    catch (ClassNotFoundException x) { fail(service, "Provider " + cn + " not found"); }
    if (!service.isAssignableFrom(c)) { fail(service, "Provider " + cn + " not a subtype"); }
    try {
        S p = service.cast(c.newInstance());
        providers.put(cn, p);
        return p;
    } catch (Throwable x) { fail(service, "Provider " + cn + " could not be instantiated", x); }
    throw new Error(); // cannot happen
}

3.4 hasNextService()

private boolean hasNextService() {
    if (nextName != null) return true;
    if (configs == null) {
        try {
            String fullName = PREFIX + service.getName();
            configs = (loader == null) ? ClassLoader.getSystemResources(fullName) : loader.getResources(fullName);
        } catch (IOException x) { fail(service, "Error locating configuration files", x); }
    }
    while ((pending == null) || !pending.hasNext()) {
        if (!configs.hasMoreElements()) return false;
        pending = parse(service, configs.nextElement());
    }
    nextName = pending.next();
    return true;
}

The process can be summarized as:

Create a ServiceLoader instance.

Create the lazy lookupIterator.

Use hasNextService to read all META-INF/services files for the interface and store their contents.

Load each implementation class via reflection and instantiate it.

Key requirements: the configuration file must reside under META-INF/services with the interface's fully qualified name, and each implementation must provide a no‑argument constructor.

4. Conclusion

This article introduced Java's SPI mechanism, demonstrated basic usage with a complete example, and examined its internal implementation. Mastering SPI enables developers to design flexible, decoupled systems and deepens their technical expertise.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaDependencyInjectionSPIServiceLoaderModularDesign
JD Cloud Developers
Written by

JD Cloud Developers

JD Cloud Developers (Developer of JD Technology) is a JD Technology Group platform offering technical sharing and communication for AI, cloud computing, IoT and related developers. It publishes JD product technical information, industry content, and tech event news. Embrace technology and partner with developers to envision the future.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.