A long long (long?) time ago, I wrote a post about the ServiceLoader
.
In short, the Service Loader allows to separate an API and its implementations in different JARs.
The client code depends on the API only, while at runtime, the implementation(s) that is (are) on the classpath will be used.
This is great way to decouple the client code from the implementing one.
For example, the ServiceLoader
is used by SLF4J:
one adds the slf4j-api
on the classpath at compilation time, while any single implementation (e.g. slf4j-simple
or logback
) can be set on the classpath at runtime.
This is a poster child of the service loader’s usage, cleanly separating between the contract and its implementation(s).
A sample project
To illustrate this, let’s implement out own logging project:
- A logging APILogService.java
public interface LogService { void log(String message); }
- A single implementation that writes on stdoutLogStdOut.java
public class LogStdOut implements LogService { @Override public void log(String message) { System.out.println(message); } }
- A client that calls the API, and makes use of the service loader mechanismClient.java
public class Client { public static void main(String[] args) { ServiceLoader<LogService> loader = ServiceLoader.load(LogService.class); for (LogService service : loader) { service.log("Log written by " + service.getClass()); } } }
The magic happens because the client contains a service loader configuration file fulfilling several constraints:
- It’s located in
/META-INF/services
- Its name is the fully-qualified name of the interface
- It contains the fully-qualified class name of the implementing class:/META-INF/services/ch.frankel.blog.serviceloader.log.LogService
ch.frankel.blog.serviceloader.log.stdout.LogStdOut
Migrating to the Java Platform Module System
As I’ve written before, migrating a non-trivial application to the Java Platform Module System is not always possible. I don’t think a lot has changed since that time. However, the concept behind the module system itself is quite straightforward.
In regard to our sample project, the following steps are required.
- Modularize the API
-
In order for other modules - implementation(s) and client - to use the API, the package containing the
LogService
interface needs to be exported.module-info.javamodule log.api { exports ch.frankel.blog.serviceloader.log; }
- Modularize the client
-
The client sits at the boundary of the module dependency tree. It just requires the API module.
module-info.javamodule log.client { requires log.api; }
- Modularize the implementation
-
The implementation needs the API. It also should export the package containing the implementation, so it can be used in other modules.
However, this is not enough. Java 9 replaces how the Service Loader works, from the
META-INF/services
folder to a module-specific implementation.For that, the module-info syntax offers two keywords:
provides
to reference the interface andwith
to specify the implementation:module-info.javaimport ch.frankel.blog.serviceloader.log.LogService; import ch.frankel.blog.serviceloader.log.stdout.LogStdOut; module log.stdout { requires log.api; exports ch.frankel.blog.serviceloader.log.stdout; provides LogService with LogStdOut; }
Interestingly enough, only the configuration changes: the Service Loader code itself in the client doesn’t change.
Conclusion
Jigsaw makes it possible to continue reaping the benefit of the Service Loader.
While the configuration changes - from the META-INF/services
folder to the compiled module-info
, the API stays the same.
There might be some hardships regarding the migration to the module system, but the Service Loader shouldn’t be one of them.