Friday, June 15, 2012

Modular services with OpenJDK Jigsaw

Services are a simple but effective way to decouple interface and implementation.

Services in the classpath universe

The class java.util.ServiceLoader was introduced in Java SE 6 and formalized a pattern that many developers were already implementing prior to SE 6 (especially for JSR implementations).

ServiceLoader provides a simple way to bind a Java interface, a service interface, to an instance of a Java class, a service provider class, that implements the interface. Such classes are declared in files located in the META-INF/services directory, where the file name is the fully qualified name of the service interface. The contents of a META-INF/services file contains one or more lines, each line of which declares the fully qualified class name of a service provider class implementing the service interface.

Using ServiceLoader one can lazily iterate over all service instances. All META-INF/services files visible to the class loader, that is used to get those the files (see ClassLoader.getResources), are parsed, service provider classes declared in those files are loaded, and then those classes are instantiated.

It's a neat little class that helps decouple interface and implementation.

That is not to say there are no problems with it.

The Java compiler knows nothing about META-INF/services files, so, unless an IDE groks those files and helps the developer, runtime errors can sneak in, for example if the service interface and/or a service provider class name does not exist, perhaps there was a spelling mistake, perhaps a class was renamed or moved to another package, or a service provider class does not implement the service interface.

A service provider class may have dependencies on third party libraries. The developer has to ensure the classpath is set up correctly, otherwise be prepared for errors such as NoClassDefFoundError.

Bundling multiple jar files into one uber jar may result in missing service provider classes if two or more jars contain the same META-INF/services files.

For the latter two issues maven is your friend. It can ensure the classpath is set up correctly. The maven shade plugin is smart enough to combine META-INF/services files.

Services in the modular universe

The Java multiverse is expanding with OpenJDK Jigsaw to embrace the modular universe.

Developers can choose to stay in the classpath universe or take a quantum leap into the modular universe, where the classpath no longer exists, and modularity become a first class citizen of the Java language, compilation and runtime.

It is important to stress that the JDK has always maintained backwards compatibility between releases, nothing is taken away from the developer, thus the classpath universe will not collapse into a singularity, no impending entropy death is to be anticipated.

In the modular universe less can be more, since the JDK itself is being modularized using the module system. Smaller JDK installations are possible (no need for CORBA?, fine with me!).

Services too become a first class citizen of the Java language, compilation and runtime. No more META-INF/services files!

Recently i have been delving into the design and implementation of services in Jigsaw. While there is an implementation in place it is recognized as a temporary solution to get something mostly working with the JDK usage of ServiceLoader.

The Jigsaw team have come up with an alternative cleaner approach that works well in modular universe. For more information see this email and corresponding presentation. However, in this blog i don't want to get into the details of that email and instead want to describe the basics of how to use services in the modular universe (the presentation is helpful in that respect and i will be reusing terms defined in that presentation).

I have have pushed a simple example to GitHub. If you have available a recent build of Jigsaw then it is possible to compile and execute this example. (See here for a recent developer preview or if you are on the Mac you can also select more recent builds created and uploaded by Henri Gomez.)

This example consists of four modules all within the same src directory:
  • A service interface module, mstringer, exporting a service interface for transforming strings
  • Two service provider modules, mhasher and mrotter, that declare service provider classes that produce an MD5 checksum and a ROT13 transformation of a string respectively; and
  • A service consume module, mapp, that creates service instances (using ServiceLoader) and transforms strings.
This example can be loaded in NetBeans. Expect to see some warnings and red squiggly lines since NetBeans currently understands nothing about Java modularity! However, the project is still editable and the targets in ant build8.xml file can be executed to build and run the project. Each module corresponds to a separate source package folder:


The above image presents a good example of modular source layout. Each module, in a directory name corresponding to the module name, contains a Java source file module-info.java, in the default package location, that is the module declaration, then there are Java source files in packages. Think of module source layout as a level of indirection of the source layout in the classpath universe. The javac compiler has been modified to recognize the modular source layout and is capable of compiling multiple modules under one source directory.

The module declaration for the service interface module mstringer is:

module mstringer@1.0 {
    exports stringer;
}


This module declaration declares that all publicly accessible Java classes in the stringer package of module mstringer are visible and accessible to any code in a dependent module. There is only one Java class which is the service interface:

package stringer;

public interface StringTransformer { 

    String description(); 
    String transform(String s);
}

Note that there is nothing specific to this module that says it has anything to do with services. Any non-final Java class can become a service interface.

The module declaration for the service provider module mrotter is:

module mrotter@1.0 {
    requires mstringer; 
    provides service stringer.StringTransformer
            with rotter.RotterStringTransformer;

}

This module declaration declares a dependency on the mstringer module since it's service provider classe will implement the service interface, stringer.StringTransformer.

Now we get into some service specifics. This module provides a service provider class rotter.RotterStringTransformer that implements the service interface stringer.StringTransformer. In effect the "provides <S> with <I>" is a very simple binding language: bind this interface to that implementation.

Note that the package rotter is never exported. This means that code in any other module cannot see and access the class RotterStringTransformer. In the modular universe service provider classes can remain private to the service provider module.

Also note that, although not present in this example, the module could require other modules that are needed for the implementation of the service interface.

The module declaration for the service provider module mhasher is very similar.

Finally the module declaration for the service consumer module mapp is:

module mapp@1.0 {
    requires mstringer;
    requires service stringer.StringTransformer;
    class app.Main;
}

This module declaration declares a dependency on the mstringer module since it will use service instances that implement stringer.StringTransformer.

The module declares it requires the service interface with "requires <S>". This informs the module system to include any service provider modules that provide for the service interface, and most importantly any dependencies of those modules, in the dependency resolution and linking phases, which occur both at compile time and when modules are installed into a library.

The module also declares an entry point, a class with a static main(String[] args) method that is invoked when the module is executed.

Notice that service consumer module mapp knows nothing about the service provider modules mrotter and mhasher. They are decoupled. New services can be installed and old ones removed without necessarily affecting mapp.

See the execution code here. The class ServiceLoader is being used even though there are no longer any META-INF/services files. The approach that is being proposed in the email linked to previously ensures that the use of ServiceLoader in the modular universe is much easier to grok (hint: it does not matter what class loader is used as long as it is a module class loader).

Since javac indirectly knows about services (by way of the module system) errors can be produced at compile time if, for example, a service provider class does not implement the corresponding service interface.

Hopefully this blog entry and example will help developers get started playing around with modular services and Jigsaw. I have deliberately avoided going into the details of compilation, installation and execution. Take a closer look at the ant build script to understand how the Java command line tools are being used.

Dependency injection in the modular universe

The modular service clauses and the use of ServiceLoader is really a very simple form binding and explicit dependency injection.

What if a module could declare richer bindings to be consumed by dependent modules? Could Guice like modules and bindings be supported such that the module system could create appropriate injectors for consuming modules? If a module defines an entry point perhaps that could be instantiated by the dependency injection system thereby allowing for annotation-based injection out-of-the-box?

Lots of questions!

No comments:

Post a Comment