10
Using services to decouple modules

This chapter covers

  • Improving project designs with services
  • Creating services, consumers, and providers in the JPMS
  • Using the ServiceLoader to consume services
  • Developing well-designed services
  • Deploying services in plain and modular JARs across different Java versions

Up to now, we represented relationships between modules with requires directives where the depending module has to reference each specific dependency by name. As section 3.2 explains in depth, this lies at the heart of reliable configuration. But sometimes you want a higher level of abstraction.

This chapter explores services in the module system and how to use them to decouple modules by removing direct dependencies between them. The first step to solving any problems with services is to get the basics down. Following that, we look at the details, particularly how to properly design services (section 10.3) and how to use the JDK’s API to consume them (section 10.4). (To see services in practice, check out the feature-services branch in ServiceMonitor's repository.)

By the end of this chapter, you’ll know how to design services well, how to write declarations for modules that use or provide services, and how to load services at run time. You can use these skills to connect with services in the JDK or third-party dependencies as well as to remove direct dependencies in your own project.

10.1 Exploring the need for services

If we were talking about classes instead of modules, would you be happy with always depending on concrete types? Or with having to instantiate each dependency in the class that needs it? If you like design patterns like inversion of control and dependency injection, you should be vigorously shaking your head at this point. Compare listings 10.1 and 10.2—doesn’t the second one look better? It allows the caller to pick the stream that gets to be awesome and even gives the caller the freedom to choose any InputStream implementation.

Listing 10.1 Depends on a concrete type and establishes the dependency

public class InputStreamAwesomizer {

       private final ByteArrayInputStream stream;  

       public AwesomeInputStream(byte[] buffer) {
               stream = new ByteArrayInputStream(buffer);  
       }

       // [... awesome methods ...]

}

Listing 10.2 Depends on an abstract type; caller establishes the dependency

public class InputStreamAwesomizer {

       private final InputStream stream;  

       public AwesomeInputStream(InputStream stream) {
               this.stream = stream;  
       }

       // [... awesome methods ...]

}

Another important benefit of depending on interfaces or abstract classes and letting someone else pick the concrete instance is that doing so inverts the direction of dependencies. Instead of high-level concepts (let’s say Department) depending on low-level details (Secretary, Clerk, and Manager), both can depend on an abstraction (Employee). As figure 10.1 shows, this breaks the dependency between high- and low-level concepts and thus decouples them.

c10_01.png

Figure 10.1 If a type establishes its own dependencies (top), users can’t influence them. If a type’s dependencies are passed during construction (bottom), users can pick the implementation that best fits their use case.

Turning back to modules, requires directives are much like the code in listing 10.1, just on a different level of abstraction:

  • Modules depend on other concrete modules.
  • There is no way for the user to exchange dependencies.
  • There is no way to invert the direction of dependencies.

Fortunately, the module system doesn’t leave it at that. It offers services, a way for modules to express that they depend on an abstract type or provide a concrete type that fulfills such a dependency, with the module system in the middle, negotiating between them. (If you’re now thinking about the service locator pattern, you’re spot on!) As you’ll see, services don’t perfectly solve all the mentioned issues, but they go a long way. Figure 10.4 shows two types of dependencies.

c10_02.png

Figure 10.2 If a module requires another (top), the dependency is fixed; it can’t be changed from the outside. On the other hand, if a module uses a service (bottom), the concrete implementation is chosen at run time.

10.2 Services in the Java Platform Module System

When we talk about a service in the context of the JPMS, it comes down to a specific type, usually an interface, that we want to use, but for which we don’t instantiate implementations. Instead, the module system pulls in implementations from other modules that said they would provide them and instantiates those implementations. This section shows in detail how that process works so you know what to put into module descriptors and how to get instances at run time as well as how that impacts module resolutions).

10.2.1 Using, providing, and consuming services

A service is an accessible type that one module wants to use and another module provides an instance of:

  • The module consuming the service expresses its requirement with a uses ${service}directive in its module descriptor, where ${service} is the fully qualified name of the service type.
  • The module providing the service expresses its offer with a provides ${service} with ${provider}directive, where ${service} is the same type as in the uses directive and ${provider} the fully qualified name of another class, which is one or the other of the following:
  • A concrete class that extends or implements ${service} and has a public, parameterless constructor (called a provider constructor)
  • An arbitrary type with a public, static, parameterless method provide that returns a type that extends or implements ${service} (called a provider method)

At run time, the depending module can use the ServiceLoader class to get all provided implementations of a service by calling ServiceLoader.load(${service}.class). The module system then returns a Provider<${service}> for each provider any module in the module graph declares. Figure 10.3 illustrates implementing a Provider.

c10_03.png

Figure 10.3 At the center of using services is a specific type, here called Service. The class Provider implements it, and the module containing it declares that with a provides — with directive. Modules consuming services need to declare that with a uses directive. At run time, they can then use the ServiceLoader to get instances of all providers for a given service.

There are a lot of details to consider around services; but generally speaking, they’re a good abstraction and straightforward to use in practice, so let’s start with that. Settle in; going through the motions takes longer than typing out a requires or exports directive.

The ServiceMonitor application provides a perfect example for a good use of services. The Monitor class from the monitormodule needs a List<ServiceObserver> to contact the services it’s supposed to monitor. So far, Main has done this as follows:

private static Optional<ServiceObserver> createObserver(String serviceName) {
    return AlphaServiceObserver.createIfAlphaService(serviceName)
        .or(() -> BetaServiceObserver.createIfBetaService(serviceName));
}

It isn’t overly important how exactly the code works. What’s relevant is that it uses the concrete types AlphaServiceObserver from monitor.observer.alpha and BetaServiceObserver from monitor.observer.beta. Hence monitor needs to depend on those modules, and they need to export the corresponding packages—figure 10.4 shows the matching section of the module graph.

c10_04.png

Figure 10.4 Without services, the monitor module needs to depend on all other involved modules: observer, alpha, and beta, as shown in this partial module graph.

Now let’s turn this into services. First, the module creating those observers needs to declare that it plans to use a service. Start by using ServiceObserver for that, so monitor's module declaration looks like this:

module monitor {
    // [... truncated requires directives ...]
    // removed dependencies on monitor.observer.alpha and beta - yay!
    uses monitor.observer.ServiceObserver;
}

The second step is to declare the provides directives in the provider modules monitor.observer.alpha and monitor.observer.beta:

module monitor.observer.alpha {
    requires monitor.observer;
    // removed export of monitor.observer.alpha - yay!
    provides monitor.observer.ServiceObserver
        with monitor.observer.alpha.AlphaServiceObserver;
}

This doesn’t work, though—the compiler throws an error:

> The service implementation does not have
> a public default constructor:
>     AlphaServiceObserver

Provider constructors and provider methods need to be parameterless, but AlphaServiceObserver expects the URL of the service it’s supposed to observe. What to do? You could set the URL after creation, but that would make the class mutable, and raises the question of what to do if the service isn’t alpha. No, it’s cleaner to create a factory for observers that returns an instance only if the URL is correct and make that factory the service.

So, create a new interface, ServiceObserverFactory, in monitor.observer. It has a single method, createIfMatchingService, that expects the service URL and returns an Optional<ServiceObserver>. In monitor.observer.alpha and monitor.observer.beta, create implementations that do what the static factory methods on AlphaServiceObserver and BetaServiceObserver used to do. Figure 10.5 shows the corresponding portion of the module graph.

c10_05.eps

Figure 10.5 With services, monitor only depends on the module defining the service: observer. The providing modules, alpha and beta, are no longer directly required.

With those classes, you can provide and consume the ServiceObserverFactory as a service. The following listing shows the module declarations for monitor, monitor.observer, monitor.observer.alpha, and monitor.observer.beta.

Listing 10.3 Four modules that work with ServiceObserverFactory

module monitor {
    requires monitor.observer;  
    // [... truncated other requires directives ...]
    uses monitor.observer.ServiceObserverFactory;  
}

module monitor.observer {  
    exports monitor.observer;
}

module monitor.observer.alpha {
    requires monitor.observer;  
    provides monitor.observer.ServiceObserverFactory
        with monitor.observer.alpha.AlphaServiceObserverFactory;  
}

module monitor.observer.beta {
    requires monitor.observer; 
    provides monitor.observer.ServiceObserverFactory
        with monitor.observer.beta.BetaServiceObserverFactory; 
}

The final step is to get the observer factories in monitor. To that end, call ServiceLoader.load(ServiceObserverFactory.class), stream over the returned providers, and get the service implementations:

List<ServiceObserverFactory> observerFactories = ServiceLoader
    .load(ServiceObserverFactory.class).stream()
    .map(Provider::get)  
    .collect(toList());

And there you go: you have a bunch of service providers, and neither the consuming nor the providing modules know each other. Their only connection is that all have a dependency on the API module.

The platform modules also declare and use a lot of services. A particularly interesting one is java.sql.Driver, declared and used by java.sql:

$ java --describe-module java.sql

> java.sql
# truncated exports
# truncated requires
> uses java.sql.Driver

This way, java.sql can access all Driver implementations provided by other modules.

Another exemplary use of services in the platform is java.lang.System.LoggerFinder. This is part of a new API added in Java 9 and allows users to pipe the JDK’s log messages (not the JVM’s!) into the logging framework of their choice (say, Log4J or Logback). Instead of writing to standard out, the JDK uses LoggerFinder to create Logger instances and then logs all messages with them.

For Java 9 and later, logging frameworks can implement factories for loggers that use the framework’s infrastructure:

public class ForesterFinder extends LoggerFinder {  

    @Override
    public Logger getLogger(String name, Module module) {
        return new Forester(name, module);
    }

}

But how can logging frameworks inform java.base of their LoggerFinder implementation? Easy: they provide the LoggerFinder service with their own implementation:

module org.forester {
    provides java.lang.System.LoggerFinder
        with org.forester.ForesterFinder;
}

This works because the base module uses LoggerFinder and then calls the ServiceLoader to locate LoggerFinder implementations. It gets a framework-specific finder, asks it to create Logger implementations, and then uses them to log messages.

This should give you a good idea of how to create and use services. On to the details!

10.2.2 Module resolution for services

If you’ve ever started a simple modular application and observed what the module system is doing (for example, with --show-module-resolution, as explained in section 5.3.6), you may have been surprised by the number of platform modules that are resolved. With a simple application like ServiceMonitor, the only platform modules should be java.base and maybe one or two more, so why are there so many others? Services are the answer.

Launching ServiceMonitor with --show-module-resolution shows a lot of service bindings:

$ java
    --show-module-resolution
    --module-path mods:libs
    --module monitor

> root monitor
> monitor requires monitor.observer
# truncated many resolutions
> monitor binds monitor.observer.beta
> monitor binds monitor.observer.alpha
> java.base binds jdk.charsets jrt:/jdk.charsets
> java.base binds jdk.localedata jrt:/jdk.localedata
# truncated lots of more bindings for java.base
# truncated rest of resolution

The module monitor binds the modules monitor.observer.alpha and monitor.observer.beta even though it doesn’t depend on either of them. The same happens to jdk.charsets, jdk.localedata, and many more due to java.base and other platform modules. Figure 10.6 shows the module graph.

c10_06.png

Figure 10.6 Service binding is part of module resolution: Once a module is resolved (like monitor or java.base), its uses directives are analyzed, and all modules that provide matching services (alpha and beta as well as charsets and localedata) are added to the module graph.

Excluding services with --limit-modules

Services and the --limit-modules option have an interesting interaction. As section 5.3.5 describes, --limit-modules limits the universe of observable modules to the specified ones and their transitive dependencies. This doesn’t include services! Unless modules providing services are transitively required by the modules listed after --limit-modules, they aren’t observable and won’t make it into the module graph. In that case, calls to ServiceLoader::load will often return empty-handed.

If you launch ServiceMonitor as when examining module resolution but limit the observable universe to modules depending on monitor, the output is much simpler:

$ java
    --show-module-resolution
    --module-path mods:libs
    --limit-modules monitor
    --module monitor
root monitor
# truncated monitor's transitive dependencies

That’s it: no services—neither observer factories nor the many services platform modules usually bind. Figure 10.7 shows this simplified module graph.

c10_07.eps

Figure 10.7 With --limit-modules monitor, the universe of observable modules is limited to monitor's transitive dependencies, which excludes the service providers resolved in figure 10.6.

Particularly powerful is the combination of --limit-modules and --add-modules: the former can be used to exclude all services and the latter to add back the desired ones. This allows you to try out different service configurations at launch without having to manipulate the module path.

10.3 Designing services well

As you saw in section 10.2, services are a play with four actors:

  • Service—In the JPMS, a class or an interface.
  • Consumer—Any piece of code that wants to use services.
  • Provider—A concrete implementation of the service.
  • Locator—The puppet master that, triggered by the consumer’s request, locates providers and returns them. In Java, this is the ServiceLoader.

The ServiceLoader is provided by the JDK (we take a closer look at it in section 10.4), but when you’re creating services, the other three classes are your responsibility. Which types do you choose for services (see section 10.3.1), and how do you best design them (section 10.3.2)? Isn’t it weird that consumers depend on ugly global state (section 10.3.3)? How should the modules containing services, consumers, and providers be related to one another (section 10.3.4)? To design well-crafted services, you need to be able to answer these questions.

We’ll also look into using services to break cyclic dependencies between modules (section 10.3.5). Last but not least—and this is particularly interesting for those who plan to use services on different Java versions—we discuss how services work across plain and modular JARs (section 10.3.6).

10.3.1 Types that can be services

A service can be a concrete class (even a final one), an abstract class, or an interface. Although only enums are excluded, using a concrete class (particularly a final one) as a service is unconventional—the entire point is that the module is supposed to depend on something abstract. Unless a specific use case requires it, a service should always be an abstract class or an interface.

10.3.2 Using factories as services

Let’s go back to the first try at refactoring the service observer architecture to use JPMS services in section 10.2.1. That didn’t go well. Using the ServiceObserver interface as the service and its implementations AlphaServiceObserver and BetaServiceObserver as providers had a number of problems:

  • Providers need parameterless provider methods or constructors, but the classes we wanted to use needed to be initialized with a concrete state that wasn’t meant to be mutated.
  • It would have been awkward for observer instances, which can handle either the alpha or beta API, to decide whether they’re suitable for a specific network service. I prefer creating instances in their correct state.
  • The service loader caches providers (more on that in section 10.4), so depending on how you use the API, there may be only one instance per provider: in this case, one AlphaServiceObserver and one BetaServiceObserver.

This made it impractical to directly create the instance we needed, so we used a factory instead. As it turns out, that wasn’t a special case.

Whether it’s the URL to connect to or the name of the logger, it’s common for a consumer to want to configure the services it uses. The consumer might also like to create more than one instance of any specific service provider. Taken together with the service loader’s requirement for parameterless construction and its freedom to cache instances, this makes it impractical to make the used type, ServiceObserver or Logger, the service.

Instead, it’s common to create a factory for the desired type, like ServiceObserverFactory or LoggerFinder, and make it the service. According to the factory pattern, factories have the sole responsibility to create instances in the correct state. As such, it’s often straightforward to design them so they have no state of their own and you don’t particularly care how many of them there are. This makes factories a great fit for the peculiarities of the ServiceLoader.

And they have at least two further bonuses:

  • If instantiating the desired type is expensive, having a factory for it as the service makes it easiest for consumers to control when instances are created.
  • If it’s necessary to check whether a provider can handle a certain input or configuration, the factory can have a method indicating that. Alternatively, its methods can return a type indicating that creating an object wasn’t possible (for example, an Optional).

I want to show you two examples for selecting services depending on their applicability to a certain situation. The first comes from ServiceMonitor, where ServiceObserverFactory doesn’t have a method create(String) returning a ServiceObserver, but does have a createIfMatchingService(String) method returning an Optional<ServiceObserver>. This way, you can throw any URL at any factory and the return value informs you whether it could handle it.

The other example doesn’t use the ServiceLoader, but rather uses a similar API deep in the JDK, the ServiceRegistry. It was created exclusively for Java’s ImageIO API, which uses it to locate an ImageReader for a given image depending on its codec, for example, JPEG or PNG.

Image IO locates readers by requesting implementations of the abstract class ImageReaderSpi from the registry, which returns instances of classes like JPEGImageReaderSpi and PNGImageReaderSpi. It then calls canDecodeInput(Object) on each ImageReaderSpi implementation, which returns true if the image uses the right codec as indicated by the file header. Only when an implementation returns true will Image IO call createReaderInstance(Object) to create an actual reader for the image. Figure 10.8 shows using a factory.

ImageReaderSpi acts as a factory service, where canDecodeInput is used to select the correct provider and createReaderInstance is used to create the needed type: an ImageReader. As section 10.4.2 shows, there’s an alternative approach to selecting a suitable provider.

In summary, you should routinely consider not picking the type you want to use as a service, but instead choosing another type, a factory, that returns instances of what you want to use. That factory should require no state of its own to function correctly. (This also makes it much easier to implement it in a thread-safe manner if that’s relevant for your use case.) See factories as a way to separate the original requirements for the type you want to use from the service infrastructure’s specific requirements instead of mixing them in one type.

c10_08.png

Figure 10.8 Making the desired type the service often doesn’t go well with the JDK’s peculiarities. Instead, consider designing a factory that creates instances in the correct configuration, and make it the service.

10.3.3 Isolating consumers from global state

Code calling ServiceLoader::load is inherently hard to test because it depends on the global application state: the modules with which the program was launched. That can easily become a problem when the module using a service doesn’t depend on the module providing it (as should be the case), because then the build tool won’t include the providing module in the test’s module path.

Manually preparing the ServiceLoader for a unit test so that it returns a specific list of service providers requires some heavy lifting. That’s anathema to unit tests, which are supposed to run in isolation and on small units of code.

Beyond that, the call to ServiceLoader::load doesn’t usually solve any problem the application’s user cares about. It’s just a necessary and technical step toward such a solution. This puts it on a different level of abstraction than the code that uses the received service providers. Friends of the single responsibility principle would say such code has two responsibilities (requesting providers and implementing a business requirement), which seems to be one too many.

These properties suggest that code handling service loading shouldn’t be mixed with code implementing the application’s business requirements. Fortunately, keeping them separate isn’t too complicated. Somewhere the instance that ends up using the providers is created, and that’s usually a good place to call ServiceLoader and then pass the providers. ServiceMonitor follows the same structure: it creates all instances required to run the app in the Main class (including loadingServiceObserver implementations) and then hands off to Monitor, which does the actual work of monitoring services.

Listings 10.4 and 10.5 show a comparison. In listing 10.4, IntegerStore does the heavy service lifting itself, which mixes responsibilities. This also makes code using IntegerStore hard to test, because tests have to be aware of the ServiceLoader call and then make sure it returns the desired integer makers.

In listing 10.5, IntegerStore is refactored and now expects the code constructing it to deliver a List<IntegerMaker>. This makes its code focus on the business problem at hand (making integers) and removes any dependency on the ServiceLoader and thus the global application state. Testing it is a breeze. Somebody still has to deal with loading services, but a create... method that’s called during application setup is a much better place for that.

Listing 10.4 Hard to test due to too many responsibilities

public class Integers {

    public static void main(String[] args) {
        IntegerStore store = new IntegerStore();
        List<Integer> ints = store.makeIntegers(args[0]);
        System.out.println(ints);
    }

}

public class IntegerStore {

    public List<Integer> makeIntegers(String config) {  
        return ServiceLoader
            .load(IntegerMaker.class).stream()  
            .map(Provider::get) 
            .map(maker -> maker.make(config))  
            .distinct()
            .sorted()
            .collect(toList());
    }

}

public interface IntegerMaker {

    int make(String config);

}

Listing 10.5 Rewritten to improve its design and testability

public class Integers {

    public static void main(String[] args) {
        IntegerStore store = createIntegerStore();
        List<Integer> ints = store.makeIntegers(args[0]);
        System.out.println(ints);
    }

    private static IntegerStore createIntegerStore() {
        List<IntegerMaker> makers = ServiceLoader
            .load(IntegerMaker.class).stream()  
            .map(Provider::get) 
            .collect(toList());
        return new IntegerStore(makers);
    }

}

public class IntegerStore {

    private final List<IntegerMaker> makers;

    public IntegerStore(List<IntegerMaker> makers) {
        this.makers = makers;  
    }

    public List<Integer> makeIntegers(String config) {  
        return makers.stream()
            .map(maker -> maker.make(config))
            .distinct()
            .sorted()
            .collect(toList());
    }

}

public interface IntegerMaker {

    int make(String config);

}

Depending on the particular project and requirements, you may have to pass providers more than one method or constructor call, wrap it into another object that defers loading until the last moment, or configure your dependency-injection framework, but it should be doable. And it’s worth the effort—your unit tests and colleagues will thank you.

10.3.4 Organizing services, consumers, and providers into modules

With the service’s type, design, and consumption settled, the question emerges: how can you organize the service and the other two actors, consumers and providers, into modules? Services obviously need to be implemented, and to provide value, code in modules other than the one containing the service should be able implement the service. That means the service type must be public and in an exported package.

The consumer doesn’t have to be public or exported and hence may be internal to its module. It must access the service’s type, though, so it needs to require the module containing the service (the service, not the classes implementing it). It isn’t uncommon for the consumer and service to end up in the same module, as is the case with java.sql and Driver as well as java.base and LoggerFinder.

Finally, we come to providers. Because they implement the service, they have to read the module defining it—that much is obvious. The interesting question is whether the providing type should become part of the module’s public API beyond being named in a provides directive.

A service provider must be public, but there’s no technical requirement for exporting its package—the service loader is fine with instantiating inaccessible classes. Thus, exporting the package containing a provider needlessly enlarges a module’s API surface. It also invites consumers to do things they’re not supposed to, like casting a service to its real type to access additional features (analogous to what happened with URLClassLoader; see section 6.2.1). I hence advise you to not make service providers accessible.

In summary (see also figure 10.9)

  • Services need to be public and in an exported package.
  • Consumers can be internal. They need to read the module defining the service or may even be part of it.
  • Providers must be public but shouldn’t be in an exported package, to minimize misuse and API surface. They need to read the module defining the service.
c10_09.png

Figure 10.9 Visibility and accessibility requirements for consumers, services, and providers

10.3.5 Using services to break cyclic dependencies

When working with a code base that’s split into subprojects, there always comes a point where one of them becomes too large and we want to split it into smaller projects. Doing so requires some work, but given enough time to disentangle classes, we can usually accomplish the goal. Sometimes, though, the code clings together so tightly that we can’t find a way to cut it apart.

A common reason is cyclic dependencies between classes. There could be two classes importing each other, or a longer cycle involving a number of classes where each imports the next. However you ended up with such a cycle, if you’d prefer to have some of its constituting classes in one project and some in another, it’s a problem. This is true even without the module system, because build tools usually don’t like cyclic dependencies either; but the JPMS voices its own strong disagreement.

What can you do? Because you’re reading the chapter about services, it may not surprise you to learn that services can help. The idea is to invert one of the dependencies in the cycle by creating a service that lives in the depending module. Here’s how to do it, step by step (see also figure 10.10):

  1. Look at the cycle of module dependencies, and identify which dependency you’d like to invert. I’ll call the two involved modules depending (the one that will have the requires directive) and depended. Ideally, depending uses a single type from depended. I’ll focus on that special case—if there are more types, repeat the following steps for each of them.
  2. In depending, create a service type, and extend the module declaration with a uses directive for that type.
  3. In depending, remove the dependency on depended. Take note of the resulting compile errors, because depended's type is no longer accessible. Replace all references to it with the service type:
  • Update imports and class names.
  • Method calls should require no changes.
  • Constructor calls won’t work out of the box because you need the instances from depended. This is where the ServiceLoader comes in: use it to replace constructions of depended's type by loading the service type you just created.
  1. In depended, add a dependency to depending so the service type becomes accessible. Provide that service with the type that originally caused the trouble.
c10_10.png

Figure 10.10 Using services to break dependency cycles in four steps: Pick a dependency, introduce a service on the depending end, use that service on the depending end, and provide the service on the depended end.

Success! You just inverted the dependency between depending and depended (now the latter depends on the former) and thus broke the cycle. Here are a few further details to keep in mind:

  • The type in depended that depending used may not be a good candidate for a service. If that’s so, consider creating a factory for it, as explained in section 10.3.2, or look for another dependency you can replace.
  • Section 10.3.3 explores the problem with sprinkling ServiceLoader calls all over a module; that issue applies here. Maybe you need to refactor depending's code to minimize the number of loads.
  • The service type doesn’t have to be in depending. As section 10.3.4 explains, it can live in any module. Or, rather, in almost any module—you don’t want to put it in one that recreates the cycle, for example in depended.
  • Most important, try to create a service that stands on its own and is more than just a cycle breaker. There may be more providers and consumers than just the two modules involved so far.

10.3.6 Declaring services across different Java versions

Services aren’t new. They were introduced in Java 6, and the mechanisms designed back then still work today. It makes sense to look at how they operate without modules and particularly how they work across plain and modular JARs.

Declaring services in META-INF/services

Before the module system entered the picture, services worked much the same as they do now. The only difference is that there were no module declarations to declare that a JAR uses or provides a service. On the using side, that’s fine—all code could use every service it wanted. On the providing side, though, JARs had to declare their intentions, and they did so in a dedicated directory in the JAR.

To have a plain JAR declare a service, follow these simple steps:

  1. Place a file with the service’s fully qualified name as the filename in META-INF/services.
  2. In the file, list all fully qualified names of classes that implement the service.

As an example, let’s create a third ServiceObserverFactory provider in the newly envisioned plain JAR monitor.observer.zero. To do so, you first need a concrete class ZeroServiceObserverFactory that implements ServiceObserverFactory and has a parameterless constructor. That’s analogous to the alpha and beta variants, so I don’t need to discuss it in detail.

A plain JAR has no module descriptor to declare the services it provides, but you can use the META-INF/services directory for that: put a simple text file monitor.observer.ServiceObserverFactory (the fully qualified name of the service type) in the directory, with the single line monitor.observer.zero.ZeroServiceObserverFactory (the fully qualified name of the provider type). Figure 10.11 shows what that looks like.

c10_11.png

Figure 10.11 To declare service providers without module declarations, the folder META-INF/services needs to contain a plain text file with the name of the service and a single line per provider.

I promise you this works, and the ZeroServiceObserverFactory is properly resolved when Main streams all observer factories. But you’ll have to take my word for it until we’ve discussed how plain and modular JARs' services interact. That’s next.

Compatibility across JARs and paths

Because the service loader API was around before the module system arrived in Java 9, there are compatibility concerns. Can consumers in plain and modular JARs use services the same way? And what happens with providers across different kinds of JARs and paths?

For service consumers, the picture is simple: explicit modules can use the services they declare with uses directives; automatic modules (see section 8.3) and the unnamed module (section 8.2) can use all existing services. In summary, on the consumer side, it just works.

For service providers, it’s a little more complicated. There are two axes with two expressions each, leading to four combinations:

  • Kind of JAR: plain (service declaration in META-INF/services) or modular (service declaration in module descriptor)
  • Kind of path: class or module

No matter which path a plain JAR ends up on, the service loader will identify and bind services in META-INF/services. If the JAR is on the class path, its content is already part of the unnamed module. If it’s on the module path, service binding results in the creation of an automatic module. This triggers the resolution of all other automatic modules, as described in section 8.3.2.

Now you know why you could try out monitor.observer.zero, a plain JAR providing its service in META-INF/services, with the modularized ServiceMonitor application. And it doesn’t matter which path I choose; it works from both without further ado.

Launching ServiceMonitor from the class path leads to no useful output, because no observer factory can be found—unless you add monitor.observer.zero to the mix. With its provider definition in META-INF/services, it’s well suited to work from the unnamed module, and indeed it does—unlike the alpha and beta providers.

10.4 Accessing services with the ServiceLoader API

Despite the fact that the ServiceLoader has been around since Java 6, it hasn’t seen wide adoption, but I expect that with its prominent integration into the module system, its use will increase considerably. To make sure you know your way around its API, we explore it in this section.

As usual, the first step is to get to know the basics, which in this case won’t take long. The service loader does have some idiosyncrasies, though, and to make sure they won’t trip you up, we’ll discuss them too.

10.4.1 Loading and accessing services

Using the ServiceLoader is always a two-step process:

  1. Create a ServiceLoader instance for the correct service.
  2. Use that instance to access service providers.

Let’s have a quick look at each step so you know the options. Also check table 10.1 for an overview of all the ServiceLoader methods.

Table 10.1 ServiceLoader API at a glance
Return type Method name Description
Methods to create a new service loader for the given type
ServiceLoader<S> load(Class<S>) Loads providers starting from the current thread’s context class loader
ServiceLoader<S> load(Class<S>, ClassLoader) Loads providers starting from the specified class loader
ServiceLoader<S> load(ModuleLayer, Class<S>) Loads providers starting from modules in the given module layer
ServiceLoader<S> loadInstalled(Class<S>) Loads providers from the platform class loader
Methods to access service providers
Optional<S> findFirst() Loads the first available provider
Iterator<S> iterator() Returns an iterator to lazily load and instantiate available providers
Stream<Provider<S>> stream() Returns a stream to lazily load available providers
void reload() Clears this loader’s provider cache so all providers will be reloaded

Ways to create a ServiceLoader

The first step, creating a ServiceLoader instance, is covered by its several static load methods. The simplest one just needs an instance of Class<S> for the service you want to load (this is called a type token, in this case for type S):

ServiceLoader<TheService> loader = ServiceLoader.load(TheService.class);

You only need the other load methods if you’re juggling several class loaders or module layers (see section 12.4); that’s not a common case, so I won’t go into it. The API docs for the corresponding overloads have you covered.

One other method gets a service loader: loadInstalled. It’s interesting here because it has a specific behavior: it ignores the module path and class path and only loads services from platform modules, meaning only providers found in JDK modules will be returned.

Accessing service providers

With a ServiceLoader instance for the desired service in hand, it’s time to start using those providers. There are two and a half methods for doing that:

  • Iterator<S> iterator() lets you iterate over the instantiated service providers.
  • Optional<S> findFirst() uses iterator to return the first provider if any were found (this is a convenience method, so I only count it as a half).
  • Stream<Provider<S>> stream() lets you stream over service providers, which are wrapped into a Provider instance. (What’s up with that? Section 10.4.2 explains.)

If you have specific laziness/caching needs (see section 10.4.2 for more), you may want to keep the ServiceLoader instance around. But in most cases that isn’t necessary and you can immediately start iterating over or streaming the providers:

ServiceLoader
    .load(TheService.class)
    .iterator()
    .forEachRemaining(TheService::doTheServiceThing);

In case you’re wondering about the inconsistency between iterator listing S and stream listing Provider<S>, it has historic reasons: although iterator has been around since Java 6, stream and Provider were only added in Java 9.

One detail that’s obvious when you think about it but still easily overlooked is that there may not be a provider for a given service. Iterator and stream may be empty, and findFirst may return an empty Optional. If you filter by capabilities, as described in sections 10.3.2 and 10.4.2, ending with zero suitable providers is even more likely.

Make sure your code either handles that case gracefully and can operate without the absent service or fails fast. It’s annoying if an application ignores an easily detectable error and keeps running in an undesired and unexpected state.

10.4.2 Idiosyncrasies of loading services

The ServiceLoader API is pretty simple, but don’t be fooled. A few important things are going on behind the curtains, and you need to be aware of them when using the API for anything beyond a basic "Hello, services!" example. This concerns the service loader’s laziness, its concurrency capabilities (or lack thereof), and proper error handling. Let’s go through these one by one.

Laziness and picking the right provider

The service loader is as lazy as possible. Called on a ServiceLoader<S> (where S is the service type with which ServiceLoader::load was called), its iterator method returns an Iterator<S> that finds and instantiates the next provider only when hasNext or next is called.

The stream method is even lazier. It returns a Stream<Provider<S>> that not only lazily finds providers (like iterator) but also returns Provider instances, which further defer service instantiation until their get method is called. Their type method gives access to a Class<? extends S> instance for their specific provider (meaning the type implementing the service, not the type that is the service).

Accessing the provider’s type is useful to scan annotations without having an actual instance of the class. Similar to what we discussed toward the end of section 10.3.2, this gives you a tool to pick the right service provider for a given configuration but without the possible performance impact of instantiating it first. That’s if the class is annotated to give you some indication of the provider’s suitability.

Continuing the ServiceMonitor example of ServiceObserver factories being applicable to specific REST service generations, the factories can be annotated with @Alpha or @Beta to indicate the generation they were created for:

Optional<ServiceObserverFactory> alphaFactory = ServiceLoader
    .load(ServiceObserverFactory.class).stream()
    .filter(provider -> provider.type().isAnnotationPresent(Alpha.class))
    .map(Provider::get)
    .findFirst();

Here, Provider::type is used to access Class<? extends ServiceObserver>, which you then ask with isAnnotationPresent whether it was annotated with @Alpha. Only when Provider::get is called is a factory instantiated.

To top off the laziness, a ServiceLoader instance caches the providers loaded so far and always returns the same ones. It does have a reload method, though, which empties the cache and will trigger new instantiations on the next call to iterate, stream, or findFirst.

Using concurrent ServiceLoaders

ServiceLoader instances aren’t thread-safe. If several threads need to operate concurrently on a set of service providers, either each of them needs to make the same ServiceLoader::load call, thus getting its own ServiceLoader instance, or you must make one call for all of them and store the results in a thread-safe collection.

Handling errors when loading services

All kinds of things can go wrong when the ServiceLoader tries to locate or instantiate service providers:

  • A provider may not fulfill all requirements. Maybe it doesn’t implement the service type or doesn’t have a suitable provider method or constructor.
  • A provider constructor or method can throw an exception or (in case of a method) return null.
  • A file in META-INF/services may violate the required format or not be processed for other reasons.

And those are just the obvious problems.

Because loading is done lazily, load can’t throw any exception. Instead, the iterator’s hasNext and next methods, as well as the stream processing and the Provider methods, can throw errors. These will all be of type ServiceConfigurationError, so catching that error lets you handle all problems that can occur.

Summary

  • The service architecture is made up of four parts:
  • The service is a class or an interface.
  • The provider is a concrete implementation of the service.
  • The consumer is any piece of code that wants to use a service.
  • The ServiceLoader creates and returns an instance of each provider of a given service to consumers.
  • Requirements and recommendations for the service type are as follows:
  • Any class or interface can be a service, but because the goal is to provide maximum flexibility to consumers and providers, it’s recommended to use interfaces (or, at the least, abstract classes).
  • Service types need to be public and in an exported package. This makes them part of their module’s public API, and they should be designed and maintained appropriately.
  • The declaration of the module defining a service contains no entry to mark a type as a service. A type becomes a service by consumers and providers using it as one.
  • Services rarely emerge randomly, but are specifically designed for their purpose. Always consider making the used type not the service but a factory for it. This makes it easier to search for a suitable implementation as well as to control when instances are created and in which state.
  • Requirements and recommendations for providers are as follows:
  • Modules providing services need to access the service type, so they must require the module containing it.
  • There are two ways to create a service provider: a concrete class that implements the service type and has a provider constructor (a public, parameterless constructor), or a type with a provider method (a public, static, parameterless method called provide) that returns an instance implementing the service type. Either way, the type must be public, but there’s no need to export the package containing it. On the contrary, it’s advisable not to make the providing type part of a module’s public API.
  • Modules providing services declare that by adding a provides ${service} with ${provider} directive to their descriptor.
  • If a modular JAR is supposed to provide services even if placed on the class path, it also needs entries in the META-INF/services directory. For each provides ${service} with ${provider} directive, create a plain file called ${service} that contains one line per ${provider} (all names must be fully qualified).
  • Requirements and recommendations for consumers are as follows:
  • Modules consuming services need to access the service type, so they must require the module containing it. They shouldn’t require the modules providing that service, though—on the contrary, that would be against the main reason to use services in the first place: to decouple consumers and providers.
  • There’s nothing wrong with service types and the service’s consumers living in the same module.
  • Any code can consume services regardless of its own accessibility, but the module containing it needs to declare which services it uses with a uses directive. This allows the module system to perform service binding efficiently and makes module declarations more explicit and readable.
  • Modules are consumed by calling ServiceLoader::load and then iterating or streaming over the returned instances by calling either iterate or stream. It’s possible that will be providers are found, and consumers must handle that case gracefully.
  • The behavior of code that consumes services depends on global state: which provider modules are present in the module graph. This gives such code undesirable properties like making it hard to test. Try to push service loading into setup code that creates objects in their correct configuration (for example, your dependency injection framework), and always allow regular provider code to pass service providers to consuming classes (for example, during construction).
  • The service loader instantiates providers as late as possible. Its stream method even returns a Stream<Provider<S>>, where Provider::type can be used to access the Class instance for the provider. This allows searching for a suitable provider by checking class-level annotations without instantiating the provider yet.
  • Service-loader instances aren’t thread-safe. If you use them concurrently, you have to provide synchronization.
  • All problems during loading and instantiating providers are thrown as ServiceConfigurationError. Due to the loader’s laziness, this doesn’t happen during load, but later in iterate or stream when problematic providers are encountered. Always be sure to put the entire interaction with ServiceLoader into a try block if you want to handle errors.
  • Here are some points about module resolution and more:
  • When module resolution processes a module that declares the use of a service, all modules providing that service are resolved and thus included in the application’s module graph. This is called service binding, and together with the use of services in the JDK, it explains why by default even small apps use a lot of platform modules.
  • The command-line option --limit-modules, on the other hand, does no service binding. As a consequence, providers that aren’t transitive dependencies of the modules given to this option don’t make it into the module graph and aren’t available at run time. The option can be used to exclude services, optionally together with --add-modules to add some of them back.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.133.142.2