Structuring the application into different layers isn’t all we need for building a plug-in architecture; we also need a way for detecting and loading the actual plug-in implementations. The service loader mechanism of the Java platform comes in handy for that. If you have never worked with the service loader API, it’s definitely recommended to study its extensive JavaDoc description:

A service is a well-known interface or class for which zero, one, or many service providers exist. A service provider (or just provider) is a class that implements or subclasses the well-known interface or class. A ServiceLoader is an object that locates and loads service providers deployed in the run time environment at a time of an application’s choosing.

Having been a supported feature of Java since version 6, the service loader API has been been reworked and refined to work within modular environments when the Java Module System was introduced in JDK 9.

In order to retrieve service implementations via the service loader, a consuming module must declare the use of the service in its module descriptor. For our purposes, the GreeterFactory contract is a perfect examplification of the service idea. Here’s the descriptor of the Greeter application’s app module, declaring its usage of this service:

1 2 3 4 5 module com . example . greeter . app { exports com . example . greeter . app ; requires com . example . greeter . api ; uses com . example . greeter . api . GreeterFactory ; }

The module descriptor of each greeter plug-in must declare the service implementation(s) which it provides. E.g. here is the module descriptor of the English greeter implementation:

1 2 3 4 5 6 module com . example . greeter . en { requires com . example . greeter . api ; requires com . example . greeter . dateutil ; provides com . example . greeter . api . GreeterFactory with com . example . greeter . en . EnglishGreeterFactory ; }

From within the app module, the service implementations can be retrieved via the java.util.ServiceLoader class.

When using the service loader in layered applications, there’s one potential pitfall though, which mostly will affect existing applications which are migrated: in order to access service implementations located in a different layer (specifically, in an ancestor layer of the loading layer), the method load(ModuleLayer, Class<?>) must be used. When using other overloaded variants of load() , e.g. the commonly used load(Class<?>) , those implementations won’t be found.

Hence the code for loading the greeter implementations from within the app layer could look like this:

1 2 3 4 5 6 7 8 9 10 private static List < GreeterFactory > getGreeterFactories () { ModuleLayer appLayer = App . class . getModule (). getLayer (); return ServiceLoader . load ( appLayer , GreeterFactory . class ) . stream () . map ( p -> p . get ()) . sorted (( gf1 , gf2 ) -> gf1 . getLanguage (). compareTo ( gf2 . getLanguage ())) . collect ( Collectors . toList ()); }

Having loaded the list of greeter factories, it doesn’t take too much code to display a list with all available implementations, expect a choice by the user and invoke the greeter for the chosen language. This code which isn’t too interesting is omitted here for the sake of brevity and can be found in the accompanying example source code repo.