What is Java SPI
Java SPI (Service Provider Interface) is a service provider interface that is a service discovery and loading mechanism provided by Java, allowing developers to define multiple implementations for an interface and dynamically discover and load these implementations at runtime.
The core of the Java SPI mechanism is that it provides a way for service providers to offer specific implementation classes for a certain interface according to the SPI conventions. These implementation classes are placed in specific locations, such as the <span>META-INF/services</span> directory, and specified through configuration files. When these services are needed, the Java runtime environment can automatically scan these directories to find and load the corresponding implementation classes, thus achieving dynamic discovery and loading of services.
The main uses of Java SPI include:
-
Service providers can provide extension points for frameworks or libraries without modifying business code.
-
Allows dynamic insertion or replacement of component implementations at runtime, encouraging loose coupling design principles.
-
Allows third-party extensions and replacements of components in core libraries, enriching the Java ecosystem and providing great flexibility for developers.
In Java, SPI is widely used in various frameworks and libraries for extensions, such as Servlet container initialization, type conversion, logging, etc. Through the SPI mechanism, Java applications can easily integrate and use third-party service implementations without modifying business code, thus improving the scalability and maintainability of the software.
Difference Between SPI and API
The main differences between SPI and API lie in their definition methods, invocation methods, flexibility, dependencies, and purposes.
-
Definition Method: API is actively written by developers and made available for other developers to use, while SPI is an interface defined by the framework or library provider for third-party developers to implement.
-
Invocation Method: API uses direct calls to interface methods to utilize functionality, while SPI specifies concrete implementation classes through configuration files, which are then automatically loaded and invoked by the framework or library.
-
Flexibility: The implementation classes of API must be determined at compile time and cannot be dynamically replaced; while the implementation classes of SPI can be dynamically loaded and replaced at runtime based on the contents of the configuration files.
-
Dependency Relationship: API is dependent on the caller, meaning the application needs to include the library where the API resides to use its functionality; while SPI is dependent on the called party, meaning the framework or library needs to include the library of the third-party implementation class to load and invoke it.
-
Purpose: API is typically used to describe programming interfaces provided externally by libraries, frameworks, operating systems, services, etc., allowing developers to call corresponding functionalities to implement their applications. SPI defines a plugin-based architecture, allowing developers to define interfaces and provide different implementations through service providers, primarily aimed at allowing systems to discover and load specific service providers at runtime, thus achieving dynamic extension and replacement capabilities.
In summary, API is a specification that describes how to interact with a component; while SPI is a mechanism used to dynamically discover and load components that implement specific interfaces.
Implementation Process
Directory Structure
sa-auth Parent Project
-- sa-auth-bus Business Project
-- sa-auth-plugin Project defining SPI interfaces
-- sa-auth-plugin-ldap Simulated third-party library implementation project
1. Create a pom project named <span>sa-auth</span> in IDEA, the pom is as follows
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.16.RELEASE</version>
</parent>
<groupId>com.vijay</groupId>
<artifactId>cs-auth</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>cs-auth</name>
<packaging>pom</packaging>
<description>cs-auth</description>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.compilerVersion>1.8</maven.compiler.compilerVersion>
</properties>
<modules>
<module>cs-auth-plugin</module>
<module>cs-auth-bus</module>
<module>cs-auth-plugin-ldap</module>
</modules>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
</dependency>
<dependency>
<groupId>com.vijay</groupId>
<artifactId>cs-auth-plugin</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
2. Then create <span>sa-auth-plugin</span>, the pom is as follows
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.vijay</groupId>
<artifactId>cs-auth</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>cs-auth-plugin</artifactId>
<name>cs-auth-plugin</name>
<description>cs-auth-plugin</description>
</project>
3. <span>sa-auth-bus</span>, the pom is as follows
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.vijay</groupId>
<artifactId>cs-auth</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>cs-auth-bus</artifactId>
<name>cs-auth-bus</name>
<description>cs-auth-bus</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.vijay</groupId>
<artifactId>cs-auth-plugin</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.1.1.RELEASE</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
4. <span>sa-auth-plugin-ldap</span>, the pom is as follows
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.vijay</groupId>
<artifactId>cs-auth</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>cs-auth-plugin-ldap</artifactId>
<name>cs-auth-plugin</name>
<description>cs-auth-plugin-ldap</description>
<dependencies>
<dependency>
<groupId>com.vijay</groupId>
<artifactId>cs-auth-plugin</artifactId>
</dependency>
</dependencies>
</project>
5. The created project structure is as follows

6. Open <span>sa-auth-plugin</span>, define the SPI interface
package com.vijay.csauthplugin.service;
/**
* Plugin SPI Interface
*
* @author vijay
*/
public interface AuthPluginService {
/**
* Login authentication
*
* @param userName Username
* @param password Password
* @return Authentication result
*/
boolean login(String userName, String password);
/**
* AuthPluginService Name which is for conveniently finding AuthPluginService instance.
*
* @return AuthServiceName marks an AuthPluginService instance.
*/
String getAuthServiceName();
}

7. Implement the SPI interface in <span>cs-auth-plugin-ldap</span> and package it into a jar, simulating an externally provided plugin jar
Implement the introduced <span>cs-auth-plugin</span> package’s SPI interface <span>package com.vijay.csauthplugin.ldap</span>;
package com.vijay.csauthplugin.ldap;
import com.vijay.csauthplugin.service.AuthPluginService;
/**
* @author vijay
*/
public class LdapProviderImpl implements AuthPluginService {
@Override
public boolean login(String userName, String password) {
return "vijay".equals(userName) && "123456".equals(password);
}
@Override
public String getAuthServiceName() {
return "LdapProvider";
}
}
Create a <span>META-INF/services</span> directory under the resources directory, and create a file named with the fully qualified name of the SPI interface class <span>com.vijay.csauthplugin.service.AuthPluginService</span>, writing the fully qualified name of the implementation class <span>com.vijay.csauthplugin.ldap.LdapProviderImpl</span> in the file

<span>cs-auth-plugin-ldap</span> is packaged into a jar
8. Open <span>cs-auth-plugin-bus</span>
Create a <span>plugin</span> package under the project, adding a default implementation of the plugin <span>DefaultProviderImpl</span>
package com.vijay.bus.plugin;
import com.vijay.csauthplugin.service.AuthPluginService;
/**
* Default plugin implementation
*
* @author vijay
*/
public class DefaultProviderImpl implements AuthPluginService {
@Override
public boolean login(String userName, String password) {
return "vijay".equals(userName) && "123456".equals(password);
}
@Override
public String getAuthServiceName() {
return "DefaultProvider";
}
}
Create a <span>META-INF/services</span> directory under the resources directory and create a file named with the fully qualified name of the SPI interface class <span>com.vijay.csauthplugin.service.AuthPluginService</span>, with the content being the fully qualified name of <span>DefaultProviderImpl</span> <span>com.vijay.bus.plugin.DefaultProviderImpl</span>

Custom Class Loader
package com.vijay.bus.plugin;
import java.net.URL;
import java.net.URLClassLoader;
/**
* Custom Class Loader
*
* @author vijay
*/
public class PluginClassLoader extends URLClassLoader {
public PluginClassLoader(URL[] urls) {
super(urls);
}
/**
* @param url Path
*/
public void addzURL(URL url) {
super.addURL(url);
}
}
Define a class to load external jar packages
package com.vijay.bus.plugin;
import java.io.File;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* Load specified directory jar packages
* @author vijay
*/
public class ExternalJarLoader {
/**
* Load external jar packages
*
* @param externalDirPath Jar package directory
*/
public static void loadExternalJars(String externalDirPath) {
File dir = new File(externalDirPath);
if (!dir.exists() || !dir.isDirectory()) {
throw new IllegalArgumentException("Invalid directory path");
}
List<URL> urls = new ArrayList<>();
File[] listFiles = dir.listFiles();
if (Objects.nonNull(listFiles) && listFiles.length > 0) {
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
try {
for (File file : listFiles) {
if (file.getName().endsWith(".jar")) {
urls.add(file.toURI().toURL());
}
}
PluginClassLoader customClassLoader = new PluginClassLoader(urls.toArray(new URL[0]));
Thread.currentThread().setContextClassLoader(customClassLoader);
} catch (Exception e) {
e.printStackTrace();
Thread.currentThread().setContextClassLoader(contextClassLoader);
}
}
}
}
Add the class loader in the startup class
package com.vijay.bus;
import com.vijay.bus.plugin.ExternalJarLoader;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author vijay
*/
@SpringBootApplication
public class CsAuthBusApplication {
public static void main(String[] args) {
String jarPath = "/Users/vijay/Downloads/build/plugin";
ExternalJarLoader.loadExternalJars(jarPath);
SpringApplication.run(CsAuthBusApplication.class, args);
}
}
Create a plugin provider class <span>PluginProvider</span>, providing implementation classes for Spring Boot injection
package com.vijay.bus.plugin;
import com.vijay.csauthplugin.service.AuthPluginService;
import java.util.ServiceLoader;
/**
* Plugin provider
*
* @author vijay
*/
public class PluginProvider {
/**
* Provide a plugin for injection (default returns the external directory plugin, returns the default plugin when there are no plugins in the external directory)
*
* @return Specific plugin implementation
*/
public static AuthPluginService getAuthPluginService() {
ServiceLoader<AuthPluginService> defaultLoad = ServiceLoader.load(AuthPluginService.class);
AuthPluginService plugin = null;
for (AuthPluginService authPluginService : defaultLoad) {
if (authPluginService instanceof DefaultProviderImpl) {
plugin = authPluginService;
} else {
return authPluginService;
}
}
return plugin;
}
}
Create a conf package under the project, injecting implementation classes into Spring Boot
package com.vijay.bus.conf;
import com.vijay.bus.plugin.PluginProvider;
import com.vijay.csauthplugin.service.AuthPluginService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author vijay
*/
@Configuration
public class PluginConfig {
@Bean
public AuthPluginService authPluginService() {
return PluginProvider.getAuthPluginService();
}
}
Create a controller package under the project, define controller interfaces, and call for testing
package com.vijay.bus.controller;
import com.vijay.csauthplugin.service.AuthPluginService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
/**
* @author vijay
*/
@RestController
public class TestController {
@Resource
private AuthPluginService authPluginService;
@GetMapping("test")
public Object test() {
return new HashMap() {{
put("name", authPluginService.getAuthServiceName());
put("login", authPluginService.login("vijay", "123456"));
}};
}
}
Complete structure

Request the interface, test the implementation

At this point, the return is the default implementation. Place the simulated third-party package from the <span>cs-auth-plugin-ldap</span> project into the external jar loading directory, restart the project, and then make a request

The implementation is now the simulated jar’s implementation
Source: juejin.cn/post/7395433541482823715