Java 9 Module Services
This deep dive into Java 9 module services tackles how the ServiceLoader class has changed, how to browse the classpath, and how it all ties together in Jigsaw.
Join the DZone community and get the full member experience.
Join For FreeJava has had a ServiceLoader class for a long time. It was introduced in 1.6, but a similar technology was in use from around Java 1.2. Some software components used it, but the use was not widespread. It can be used to modularize an application (even more) and provide a means to extend an application using some kind of plug-ins that the application does not depend on at compile time. Also, the configuration of these services is very simple: Just put it on the class/module path. We will examine the details.
The service loader can locate implementations of some interfaces. In EE environments, there are other methods to configure implementations. In non-EE environments, Spring has become ubiquitous, and it has a similar, though identical, solution to a similar, but not identical, problem.
Inversion of Control (IoC) and Dependency Injections (DI) provided by Spring are the solutions to the configuration of the wiring of different components and are industry best practices when it comes to separating the wiring description/code from the actual implementation of functionalities that classes have to perform.
As a matter of fact, Spring also supports the use of the service loader so you can wire an implementation located and instantiated by the service loader. You can find a short and nicely written article about that here.
ServiceLoader is more about how to find the implementation before we can inject it into the components that need it. Junior programmers sometimes mistakenly mix the two, and it is not without reason: They are strongly related.
Perhaps because of this, most applications, at least those that I have seen, do not separate the wiring and the finding of the implementation. These applications usually use Spring configuration for both finding and wiring — and this is just... OK. Although this is a simplification, we should live with and be happy with it. We should not separate the two functions just because we can. Most applications do not need to separate these. They neatly sit on a simple line of the XML configuration of a Spring application.
We should program on a level of abstraction that is needed, but never more abstract.
Yes, this sentence is a paraphrase of a saying that is attributed to Einstein. If you think about it, you could also realize that this statement is nothing more than the principle KISS (keep it simple and stupid).
The code, not you.
ServiceLoader finds the implementation of a certain class — not all the implementations that may be on the classpath. It finds only those that are “advertised”. (I will tell later what “advertised” means.) A Java program cannot traverse through all the classes that are on the classpath... or can it?
Browsing the Classpath
This section is a little detour, but it is important to understand why ServiceLoader works the way it does, even before we discuss how it works.
Java code cannot query the classloader to list all the classes that are on the classpath. You may say I lie because Spring does browse classes and automatically finds the implementation candidates.
Spring actually cheats, and I will tell you how it does.
For now, accept that the classpath cannot be browsed. If you look at the documentation of the class ClassLoader, you do not find any method that would return the array, stream, or collection of classes. You can get the array of the packages, but you cannot get the classes — even from the packages.
The reason for it is the level of abstraction in how Java handles the classes. The class loader loads the classes into the JVM, and the JVM does not care from where. It does not assume that the actual classes are in files. There are a lot of applications that load classes not from a file. As a matter of fact, most applications load some classes from some different media. Also your programs, you just may not know it.
Have you ever used Spring, Hibernate, or some other framework? Most of these frameworks create proxy objects during run-time and then loads these objects from memory using a special class loader. The class loader cannot tell you if there will ever be a new object created by the framework it supports. The classpath, in this case, is not static. There is no such thing as a classpath for these special class loaders. They find classes dynamically.
Okay, that's enough detail.
But again: How does Spring find classes? Spring actually makes a bold assumption. It assumes that the class loader is a special one: URLClassLoader. (And as Nicolai Parlog writes in his article, that is not true with Java 9 anymore.) It works with a classpath that contains URLs, and it can return the array of URLs.
ServiceLoader does not make such an assumption and, as such, it does not browse the classes.
How Does ServiceLoader Find a Class?
The ServiceLoader can find and instantiate classes that implement a specific interface. When we call the static method ServiceLoader.load(interfaceKlass), it returns a “list” of classes that implement this interface. I used “list” between quotes because, technically, it returns an instance of ServiceLoader, which itself implements Iterable so we can iterate over the instances of the classes that implement the interface. The iteration is usually done in a for loop, invoking the method load() following the (:) colon.
To successfully find the instances, the JAR files that contain the implementations should have a special file in the directory META-INF/service that has the fully qualified name of the interface. Yes, the name has dots in it, and there is no specific file name extension. Nevertheless, it has to be a text file. It has to contain the fully qualified name of the class that implements the interface in that JAR file.
The ServiceLoader invokes the ClassLoader method findResources to get the URLs of the files and reads the names of the classes. Then, it asks the ClassLoader to load those classes again. The classes should have a public zero-argument constructor so that the ServiceLoader can instantiate each one.
Having those files contain the name of the classes in order to piggyback the class loading and instantiation using the resource load works, but it is not very elegant.
Java 9, while keeping the annoying META-INF/services solution, introduced a new approach. With the introduction of Jigsaw, we have modules, and modules have module descriptors. A module can define a service that a ServiceLoader can load, and a module can also specify what services it may need to load via the ServiceLoader. This way, the discovery of the implementation of the service interface moves from textual resources to Java code. The advantage of it is that coding errors related to incorrect names can be identified during compile time or module load time to fail faster.
To make things more flexible, or just to make them uselessly more complex (future will tell), Java 9 also works if the class is not an implementation of the service interface but does have a public static provider() method that returns an instance of the class that implements the interface. (By the way: In this case, the provider class even may implement the service interface if it wants, but it is generally a factory, so why would it? Mind SRP.)
Sample Code
You can download a multi-module Maven project from https://github.com/verhas/module-test.
This project contains three modules: Consumer, Provider, and ServiceInterface. The consumer calls the ServiceLoader and consumes the service, which is defined by the interface javax0.serviceinterface.ServiceInterface in the module ServiceInterface and implemented in the module Provider. The structure of the code can be seen in the following picture:
The module-info files contain the declarations:
module Provider {
requires ServiceInterface;
provides javax0.serviceinterface.ServiceInterface
with javax0.serviceprovider.Provider;
}
module Consumer {
requires ServiceInterface;
uses javax0.serviceinterface.ServiceInterface;
}
module ServiceInterface {
exports javax0.serviceinterface;
}
Here, I will tell you some of the stupid mistakes I made while I created this very simple example so that you can learn from them. First of all, there is a sentence in the Java 9 JDK documentation in the ServiceLoader that reads:
In addition, if the service is not in the application module, then the module declaration must have a requires directive that specifies the module which exports the service.
I do not know what it wants to say, but what it means to me is not true. Maybe I misinterpreted this sentence, which is likely.
Looking at our example, the Consumer module uses something that implements the javax0.serviceinterface.ServiceInterface interface. This something is actually the Provider module and the implementation in it, but it is decided only during run time and can be replaced by any other fitting implementation.
Thus, it needs the interface and must have the requires directive in the module info file requiring the ServiceInterface module. It does not have to require the Provider module! The Provider module similarly depends on the ServiceInterface module and has to require it. The ServiceInterface module does not require anything. It only exports the package that contains the interface.
It is also important to note that neither the Provider nor the Consumer modules are required to export any package. Provider provides the service declared by the interface and implemented by the class named after the with keyword in the module info file. It provides this single class for the world and nothing else. To provide only this class, it would be redundant to export the package containing it, and it would possibly unnecessarily open the classes that may happen in the same package but are internal modules.
Consumer is invoked from the command line using the –m option, and it also does not require the module to export any package. The command like to start the program is:
java -p Consumer/target/Consumer-1.0.0-SNAPSHOT.jar:
ServiceInterface/target/ServiceInterface-1.0.0-SNA
PSHOT.jar:Provider/target/Provider-1.0.0-SNAPSHOT.
jar -m Consumer/javax0.serviceconsumer.Consumer
And it can be executed after a successful mvn install command. Note that the Maven compiler plugin has to be at least version 3.6. Otherwise, the ServiceInterface-1.0.0-SNAPSHOT.jar will be on the classpath instead of the module path during compilation, which will fail when it does not find the module-info.class file.
What Is the Point?
The ServiceLoader can be used when an application is wired with some modules only during run-time. A typical example is an application with plugins. I myself ran into this exercise when I ported ScriptBasic for Java from Java 7 to Java 9. The BASIC language interpreter can be extended by classes containing public static methods, and they have to be annotated as BasicFunction. The last version required that the host application embedding the interpreter list all the extension classes calling an API in the code.
This is superfluous and not needed.
The ServiceLoader can locate service implementations for which the interface (ClassSetProvider) is defined in the main program, and then the main program can call the service implementations one after the other and register the classes returned in the sets. That way, the host application does not need to know anything about the extension classes — it is enough that the extension classes are put on the module path and that each provides the service.
The JDK itself also uses this mechanism to locate loggers. The new Java 9 JDK contains the System.LoggerFinder class that can be implemented as a service by any module, and if there is an implementation that the ServiceLoader can find, the method System.getLogger() will find that. This way, logging is not tied to the JDK and is not tied to a library during compile time. It is enough to provide the logger during run-time — the application, the libraries the application uses, and the JDK will all use the same logging facility.
With all these changes in the service loading mechanism, and making it part of the language rather than being piggy-backed on resource loading, one may hope that this type of service discovery will gain momentum and will be used in a broader scale as it was used before.
Published at DZone with permission of Peter Verhas, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments