API vs SPI: Concepts, Implementation, and Real‑World Java Examples

This article explains the difference between APIs and Java SPI, describes how SPI enables plug‑in development through ServiceLoader, and illustrates its practical use with JDBC driver loading and Flink table factories, providing code snippets and implementation steps for backend developers.

Big Data Technology & Architecture
Big Data Technology & Architecture
Big Data Technology & Architecture
API vs SPI: Concepts, Implementation, and Real‑World Java Examples

API vs SPI

API (Application Programming Interface) defines how a client calls functionality provided by a platform or framework, while SPI (Service Provider Interface) is a contract that allows a service provider to extend a system by implementing interfaces that the system discovers and loads dynamically.

SPI Implementation Principle

To use SPI, a provider must:

Implement the service interface.

Create a UTF‑8 text file named with the fully‑qualified interface name under META-INF/services and list the implementation class names (one per line).

Invoke java.util.ServiceLoader.load() to discover and instantiate the implementations.

The following code excerpts show the essential parts of ServiceLoader used by SPI.

private static final String PREFIX = "META-INF/services/";
// The class or interface representing the service being loaded
private final Class<S> service;
// The class loader used to locate, load, and instantiate providers
private final ClassLoader loader;
// The access control context taken when the ServiceLoader is created
private final AccessControlContext acc;
// Cached providers, in instantiation order
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// The current lazy‑lookup iterator
private LazyIterator lookupIterator;

Loading a service is performed via the static load() methods and the constructor that validates the service interface.

public static <S> ServiceLoader<S> load(Class<S> service) {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader) {
    return new ServiceLoader<>(service, loader);
}

private ServiceLoader(Class<S> svc, ClassLoader cl) {
    service = Objects.requireNonNull(svc, "Service interface cannot be null");
    loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
    acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
    reload();
}

public void reload() {
    providers.clear();
    lookupIterator = new LazyIterator(service, loader);
}

The iterator returned by ServiceLoader.iterator() first checks the cached providers and then lazily loads new ones via LazyIterator.

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 inner LazyIterator performs the actual discovery by reading the configuration files and instantiating classes via reflection.

private class LazyIterator implements Iterator<S> {
    Class<S> service;
    ClassLoader loader;
    Enumeration<URL> configs = null;
    Iterator<String> pending = null;
    String nextName = null;
    private LazyIterator(Class<S> service, ClassLoader loader) {
        this.service = service;
        this.loader = loader;
    }
    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;
    }
    private S nextService() {
        if (!hasNextService()) throw new NoSuchElementException();
        String cn = nextName; nextName = null;
        try {
            Class<?> c = Class.forName(cn, false, loader);
            if (!service.isAssignableFrom(c)) fail(service, "Provider " + cn + " not a subtype");
            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();
    }
    // ...
}

SPI in JDBC

JDBC uses SPI to load database drivers. Modern JDBC simply calls DriverManager.getConnection(), which internally triggers ServiceLoader.load(Driver.class) to discover all java.sql.Driver implementations.

private static void loadInitialDrivers() {
    AccessController.doPrivileged(new PrivilegedAction<Void>() {
        public Void run() {
            ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
            Iterator<Driver> driversIterator = loadedDrivers.iterator();
            try { while (driversIterator.hasNext()) { driversIterator.next(); } } catch (Throwable t) { }
            return null;
        }
    });
}

The MySQL driver JAR contains a file META-INF/services/java.sql.Driver with the following content:

com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver

SPI in Flink

Flink’s Table API uses SPI to discover TableFactory implementations. The factory interface is defined as:

/**
 * A factory to create different table‑related instances from string‑based properties.
 * This factory is used with Java's Service Provider Interfaces (SPI) for discovering.
 */
@PublicEvolving
public interface TableFactory {
    Map<String, String> requiredContext();
    List<String> supportedProperties();
}

Flink’s TableFactoryService.findSingleInternal() loads all factories via ServiceLoader and then filters them based on the provided properties.

private static <T extends TableFactory> T findSingleInternal(
        Class<T> factoryClass,
        Map<String, String> properties,
        Optional<ClassLoader> classLoader) {
    List<TableFactory> tableFactories = discoverFactories(classLoader);
    List<T> filtered = filter(tableFactories, factoryClass, properties);
    if (filtered.size() > 1) {
        throw new AmbiguousTableFactoryException(filtered, factoryClass, tableFactories, properties);
    } else {
        return filtered.get(0);
    }
}
private static List<TableFactory> discoverFactories(Optional<ClassLoader> classLoader) {
    try {
        List<TableFactory> result = new LinkedList<>();
        ClassLoader cl = classLoader.orElse(Thread.currentThread().getContextClassLoader());
        ServiceLoader.load(TableFactory.class, cl).iterator().forEachRemaining(result::add);
        return result;
    } catch (ServiceConfigurationError e) {
        LOG.error("Could not load service provider for table factories.", e);
        throw new TableException("Could not load service provider for table factories.", e);
    }
}

These examples demonstrate how SPI provides a clean, decoupled way to extend backend systems without modifying existing APIs.

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.

JavaFlinkBackend DevelopmentJDBCAPISPIServiceLoader
Big Data Technology & Architecture
Written by

Big Data Technology & Architecture

Wang Zhiwu, a big data expert, dedicated to sharing big data technology.

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.