0%

Java Service Provider Interface

With interface oriented concept, we should put our focus on the service it provided. Say HttpServletRequest from jakarta.servlet-api, we have different implementation like tomcat, jetty and undertow etc. Both of them has HttpServletRequestImpl which implements HttpServletRequest interface. Most of time, we should invoke the service provided by the interface and does NOT care about its underlying.

Another example is the JDBC. We use java.sql.DriverManager to find and load implementations of java.sql.Driver. In our program, we did NOT know which underlying database will be connected. But only when they are in the classpath, we can dynamic load them in our Java program with the java.sql.Driver interface. This is called Service Provider Interface, aka SPI, in Java.

SPI enables developers to extend applications functionalities by adding services implementation, e.g. Jar files, to classpath without modifying the application code base.

Define the Service API

1
2
3
4
5
package tk.wangkexiong.api;

public interface IMyService {
void doJob();
}

And the wrapper with the set of services provided to application

1
2
3
4
5
package tk.wangkexiong.api;

public interface IMyServiceProvider {
IMyService getService();
}

Add build.gradle to package jar file

1
2
3
4
5
6
plugins {
id 'java'
}

group = 'tk.wangkexiong'
version = '1.0-SNAPSHOT'

Use the service in our application

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package tk.wangkexiong;

import tk.wangkexiong.api.IMyServiceProvider;

import java.nio.file.ProviderNotFoundException;
import java.util.ServiceLoader;

public class MyServiceLoader {
public static IMyServiceProvider provider() {
ServiceLoader<IMyServiceProvider> loader = ServiceLoader.load(IMyServiceProvider.class);

for (IMyServiceProvider provider : loader) {
return provider;
}

throw new ProviderNotFoundException("provider not found");
}
}

And the main entrance

1
2
3
4
5
public class App {
public static void main (String[] args) {
MyServiceLoader.provider().getService().doJob();
}
}

Add build.gradle to package jar

1
2
3
4
5
6
7
8
9
10
11
12
plugins {
id 'java'
}

group = 'tk.wangkexiong'
version = '1.0-SNAPSHOT'

dependencies {
implementation (
project(':service-api')
)
}

Service Implementation by vendor

1
2
3
4
5
6
7
8
9
import tk.wangkexiong.api.IMyService;


public class MyService implements IMyService {
@Override
public void doJob() {
System.out.println("MyServiceImpl1");
}
}

And the wrapper:

1
2
3
4
5
6
7
8
9
10
11
package tk.wangkexiong.impl;

import tk.wangkexiong.api.IMyService;
import tk.wangkexiong.api.IMyServiceProvider;

public class MyServiceProvider implements IMyServiceProvider {
@Override
public IMyService getService() {
return new MyService();
}
}

And the most important, follow SPI specification to add file named tk.wangkexiong.api.IMyServiceProvider under META-INF/services with content:

1
tk.wangkexiong.impl.MyServiceProvider

Add build.gradle to package jar file. If you followed the project file structure of standard Java project, which means above META-INF directory under src/main/resources, the :jar task will include above META-INF information. This will make the Java SPI package.

1
2
3
4
5
6
7
8
9
10
11
12
plugins {
id 'java'
}

group = 'tk.wangkexiong'
version = '1.0-SNAPSHOT'

dependencies {
implementation (
project(':service-api')
)
}

Run the application

1
2
3
4
5
6
$ gradlew :service-api:jar
$ gradlew :service-impl1:jar
$ gradlew :app:jar
$ cd app/build/libs
$ java -cp ./app-1.0-SNAPSHOT.jar;../../../service-api/build/libs/service-api-1.0-SNAPSHOT.jar;../../../service-impl1/build/libs/service-impl1-1.0-SNAPSHOT.jar tk.wangkexiong.App
MyServiceImpl1

Source code available at github