11
Refining dependencies and APIs

This chapter covers

  • Handling dependencies that are part of a module’s API
  • Aggregating and refactoring modules without breaking clients
  • Defining optional dependencies
  • Writing code in the face of absent dependencies
  • Exporting packages to selected modules only

Chapter 3 explains how requires and exports directives are the basis for readability and accessibility. But these mechanisms are strict: every module has to be explicitly required, all required modules have to be present for the application to compile and launch, and exported packages are accessible to all other modules. This suffices for the majority of use cases, but there’s still a significant portion in which these solutions are too broad.

The most obvious use case is optional dependencies, which a module wants to compile against but which aren’t necessarily present at run time. Spring, for example, does this with the Jackson databind library. If you run a Spring application and want to use JSON as a data-transfer format, you can get support for that by dropping in the Jackson artifact. If, on the other hand, that artifact is absent, Spring is still happy—it doesn’t support JSON then. Spring uses Jackson but doesn’t require it.

Regular requires directives don’t cover this use case, though, because the modules would have to be present for the application to launch. Services can be the solution in some such cases, but using them for all optional dependencies would lead to many awkward and complex implementations. Hence, plainly expressing that a dependency isn’t required at run time is an important feature; section 11.2 shows how the JPMS implements it.

Another use case where the module system’s strictness can become a hindrance is refactoring modules over time. In any decently sized project, the architecture evolves as time goes by, and developers will want to merge or split modules. But then what happens to code that depends on the old modules? Wouldn’t it be missing functionality (if it was split off into a new module) or even entire modules (if they were merged)? Fortunately the module system offers a feature, called implied readability, that can be of use here.

Although the requires and exports mechanisms we know so far make for a comparatively simple mental model, they offer no elegant solutions for use cases that don’t fit into their one-size-fits-all approaches. In this chapter, we look into such specific use cases and explore the solutions the module system offers.

By the time you’ve worked through it, you’ll be able to use more refined mechanisms to access dependencies and export functionality. This will allow you to, among other things, express optional dependencies (section 11.2), refactor modules (section 11.1), and share code between a defined set of modules while keeping it private from other code (section 11.3).

11.1 Implied readability: Passing on dependencies

In section 3.2, we explored in depth how requires directives establish dependencies between modules and how the module system uses them to create reads edges (eventually resulting in the module graph, as sections 3.4.1 and 3.4.2 show). In section 3.3, you saw that accessibility is based on these edges, and to access a type, the accessing module must read the module containing the type (the type must also be public and the package exported, but that isn’t relevant here).

In this section, we’ll look at another way to give modules access to other modules. We’ll start by discussing a motivating use case before I introduce the new mechanism and develop some guidelines for how best to use it. Toward the end, you’ll see how powerful it is and how it can help with much more than the initial examples.

Check out ServiceMonitor’s feature-implied-readability branch for the code accompanying this section.

11.1.1 Exposing a module’s dependencies

When it comes to the interplay between requires directives and accessibility, there’s a fine detail to observe: The requires directives create reads edges but the edges are a prerequisite for accessibility. Doesn’t that beg the question of which other mechanisms can establish readability and thus unlock access to types? This is more than theoretical pondering—approaching the situation from a practical angle, we end up in the same place.

Let’s turn back to the ServiceMonitor application, particularly the modules monitor.observer and monitor.observer.alpha. Assume that a new module, let’s call it monitor.peek, wants to use monitor.observer.alpha directly. It has no need for monitor.observer or the service architecture you created in the previous chapter. Can monitor.peek just require monitor.observer.alpha and start using it?

ServiceObserver observer = new AlphaServiceObserver("some://service/url");
DiagnosticDataPoint data = observer.gatherDataFromService();

It looks like it needs the types ServiceObserver and DiagnosticDataPoint. Both are in monitor.observer, so what happens if monitor.peek doesn’t require monitor.observer? It can’t access its types, resulting in compile errors. As you saw when we discussed the encapsulation of transitive dependencies in section 3.3.2, this is a feature of the module system.

Here it’s an impediment, though. Without the types from monitor.observer, monitor.observer.alpha is effectively useless; and every module that wants to use it has to read monitor.observer as well. (This is shown in figure 11.1.) Does every module using monitor.observer.alpha have to require monitor.observer, too?

That’s not a comfortable solution. If only there was another mechanism to establish readability and thus unlock access to types.

c11_01.png

Figure 11.1 The module peek uses observer.alpha, which uses types from observer in its public API. If peek doesn’t require observer (left), it can’t read its types, making observer.alpha useless. With regular requires directives, the only way around that is to have peek also require observer (right), which becomes cumbersome when more modules are involved.

c11_02.eps

Figure 11.2 Three modules are involved in the problem of exposed dependencies: the innocent one that provides some types (exposed; right), the guilty one using those types in its public API (exposing; middle), and the impacted one having to accesses the innocent’s types (depending; left).

What happens in the previous example is common. A module exposing depends on some module exposed, but uses types from exposed in its own public API (as defined in section 3.3). In such cases, exposing is said to expose its dependency on exposed to its clients because they also need to depend on exposed in order to use exposing.

To make talking about this situation a little less confusing, make sure you understand these definitions in figure 11.2. I’ll stick to these terms when describing the involved modules:

  • The module exposing its dependency is called the exposing module.
  • The module that is exposed as a dependency is the exposed module.
  • The module depending on that mess is called the depending module.

Many examples can be found in the JDK. The java.sql module, for example, contains a type java.sql.SQLXML (used by java.sql.Connection, among others), which uses types from the java.xml module in its public methods. The type java.sql.SQLXML is public and in an exported package, so it’s part of the API of java.sql. That means in order for any depending module to properly use the exposing java.sql, it must read the exposed java.xml as well.

11.1.2 The transitive modifier: Implying readability on a dependency

Looking at the situation, it’s clear that the developers of the exposing module are the ones who need to solve this problem. After all, they decide to use the exposed module’s types in their own API, forcing the modules depending on them to read the exposed modules.

The solution for these situations is to use a requires transitivedirective in the exposing module’s declaration. If exposing declares requires transitive exposed, then any module reading exposing will implicitly also read exposed. The effect is called implied readability: reading exposing implies reading exposed. Figure 11.3 shows this directive.

c11_03.eps

Figure 11.3 When exposing uses a requires transitive directive to depend on exposed, reading exposing implies readability of exposed. As a consequence, modules like depending (left) can read exposed even if they only require exposing.

The use of implied readability is obvious when looking at a module declaration or descriptor. With the skills you learned in section 5.3.1, you can look into java.sql. The following listing shows that the dependency on java.xml is marked with transitive.

Listing 11.1 java.sql module descriptor: implies readability of java.xml and java.logging

$ java --describe-module java.sql

> [email protected]
> exports java.sql
> exports javax.sql
> exports javax.transaction.xa
> requires java.base mandate
> requires java.logging transitive  
> requires java.xml transitive  
> uses java.sql.Driver

Likewise, the dependency on java.logging is marked transitive. The reason is the public interface java.sql.Driver and its method Logger getParentLogger(). It exposes the type java.util.logging.Logger from java.logging in the public API of java.sql, so java.sql implies readability of java.logging. Note that although java --describe-module puts transitive last, the module declaration expects the modifier to come between requires and the module name (requires transitive ${module}).

Going back to the motivating example of how to make monitor.observer.alpha usable without depending modules also having to require monitor.observer, the solution is now obvious—use requires transitive to declare the dependency of monitor.observer.alpha on monitor.observer:

module monitor.observer.alpha {
    requires transitive monitor.observer;
    exports monitor.observer.alpha;
}

When exploring reliable configuration and missing dependencies in section 3.2.2, you discovered that although the run time requires all dependencies (direct and indirect) to be observable, the compiler only mandates that for direct ones. This means you can compile your module against exposing without its dependencies being present. Now, how does implied readability fit into this?

That’s regardless of whether you use types from exposed, which might at first seem overly strict. But remember from section 3.4.1 that modules are resolved and the module graph is built before the code is compiled. The module graph is the basis for compilation, not the other way around, and mutating it based on the encountered types would go against the goal of reliable configuration. The module graph must hence always contain transitive dependencies.

11.1.3 When to use implied readability

As you’ve seen, implied readability reduces the need for explicit requires directives in depending modules. This can be a good thing, but I want to return to something I only mentioned in passing before. Implied readability goes against a feature of the module system: the encapsulation of transitive dependencies discussed in section 3.2.2. With two opposing requirements (strictness versus convenience) and two features to fulfill them (requires versus requires transitive), it’s important to carefully consider the trade-offs.

The situation is similar to visibility modifiers. For convenience’s sake, it would be easy to make every class, every field, and every method public. We don’t do that, though, because we know that exposing less reduces the contact surface between different parts of the code and makes modification, replacement, and reuse easier. And like making a type or member public, exposing a dependency becomes part of that module’s public API, and clients may rely on the fact that readability is implied. This can make evolving the module and its dependencies more difficult, so it shouldn’t be undertaken lightly.

Other use cases are aggregation, decomposition, and merging of modules, all of which we’ll discuss in section 11.1.5. Before that, I want to explore a similar use case that may warrant another solution.

So far, the assumption has been that the exposing module can’t operate without exposed. Interestingly enough, that isn’t always the case. The exposing module could implement utility functions based on the exposed module that only code that’s already using the exposed module would call.

Say a library uber.lib offers utility functions based on com.google.common. Then only users of Guava had a use for uber.lib. In such cases, optional dependencies may be the way to go; see section 11.2.

11.1.4 When to rely on implied readability

You’ve seen how implied readability allows a module to “pass on” readability of exposed dependencies. We discussed considerations that go into deciding when to use that feature. That was from the perspective of the developer writing the exposing module.

Now, let’s switch perspective and look at this from the point of view of the depending module: the one to which readability of the exposed module is passed on. To what extent should it rely on implied readability? At what point should it instead require the exposed module?

As you saw when we first explored implied readability, java.sql exposes its dependency on java.logging. That begs the question, should modules using java.sql also require java.logging? Technically, such a declaration isn’t needed and may seem redundant.

That’s also true for the motivating example of monitor.peek, monitor.observer, and monitor.observer.alpha: in the final solution, monitor.peek uses types from both other modules but only requires monitor.observer.alpha, which implies readability of monitor.observer. Should it also explicitly require monitor.observer? And if not, just not in that specific example, or never?

To decide when to rely on a dependency implying readability on a module or when to require that module directly, it makes sense to turn back to one of the core promises of the module system: reliable configuration (see section 3.2.1). Using requires directives makes code more reliable by making dependencies explicit, and you can apply that principle here to make a decision by asking a different question.

  • If the answer is negative, removing the code that uses the exposing module also removes the dependency on the exposed module. We could say that the exposed module was only used on the boundary between the depending and the exposing modules. In that case, there’s no need to explicitly requiring it, and relying on implied readability is fine.
  • If, on the other hand, the answer is positive, then the exposed module is used on more than just the boundary to the exposing module. Accordingly, it should be explicitly depended on with a requires directive.

Figure 11.4 illustrates visualizes these two options.

c11_04.eps

Figure 11.4 Two cases of implied readability, involving depending, exposing, and exposed modules. Where the two boxes touch, the depending module uses exposing, on which it explicitly depends. Both use the exposed module (striped area). But the degree of use can differ: The depending module may only use it on the boundary to exposed (top), or it may use the types internally to implements its own features (bottom).

Looking back on the example of java.sql, you can answer the question based on how the depending module, let’s say it’s monitor.persistence, uses java.logging:

  • It may only need to read java.logging, so it’s able to call java.sql.Driver.getParentLogger(), change the logger’s log level, and be done with it. In this case, its interaction with java.logging is limited to the boundary between monitor.persistence and java.sql, and you’re in the sweet spot for implied readability.
  • Alternatively, monitor.persistence may use logging throughout its own code. Then, types from java.logging appear in many places, independently of Driver, and can no longer be considered limited to the boundary. In that case, monitor.persistence should explicitly require java.logging.

A similar juxtaposition can be made for the example from the ServiceMonitor application. Does monitor.peek, which requires monitor.observer.alpha, only use types from monitor.observer to create a ServiceObserver? Or does it have a use for the types from the monitor.observer module independently of its interaction with monitor.observer.alpha?

11.1.5 Refactoring modules with implied readability

At first glance, implied readability looks like a small feature that solves a specific use case. Interestingly, though, it isn’t limited to that case! On the contrary, it unlocks some useful techniques that help with refactoring modules.

The motivation for using these techniques is often to prevent changes in modules that depend on the one(s) that are being refactored. If you have total control over all clients of a module and compile and deploy them all at once, then you can change their module declarations instead of doing something more complicated. But often you can’t—for example, when developing a library—so you need a way to refactor modules without breaking backward compatibility.

Representing module families with aggregator modules

Let’s say your application has a couple of core modules that almost any other module must depend on. You could, of course, copy-paste the necessary requires directives into every module declaration, but that’s rather tedious. Instead, you can use implied readability to create a so-called aggregator module.

An aggregator module contains no code and implies readability with requires transitive on all of its dependencies. It’s used to create a coherent set of modules that other modules can easily depend on by just requiring the aggregator module.

The ServiceMonitor application is a little small to justify creating an aggregator module; but for the sake of an example, let’s decide that monitor.observer and monitor.statistics are its core API. In that case, you can create monitor.core as follows:

module monitor.core {
    requires transitive monitor.observer;
    requires transitive monitor.statistics;
}

Now, all other modules can depend on monitor.core and get readability of monitor.observer and monitor.statistics for free. Figure 11.5 visualizes this example.

c11_05.eps

Figure 11.5 The aggregator module core (left) contains no code and uses requires transitive directives to refer to the aggregated modules observer and statistics (right), which contain the functionality. Thanks to implied readability, clients of the aggregator module can use the APIs of the aggregated modules.

There is, of course, no reason to limit aggregation to core functionality. Every family of modules that cooperate to implement a feature is a candidate to get an aggregator module that represents it.

But wait: don’t aggregator modules bring clients into a situation where they internally use APIs of modules they don’t explicitly depend on? This can be seen as conflicting with what I said when discussing when to rely on implied readability: that it should be used on the boundary to other modules. But I think the situation is subtly different here.

Aggregator modules have a specific responsibility: to bundle the functionality of related modules into a single unit. Modifying the bundle’s content is a pivotal conceptual change. “Regular” implied readability, on the other hand, often manifests between modules that aren’t immediately related (as with java.sql and java.logging), where the implied module is used more incidentally (although it’s still API-breaking to change it; see section 15.2.4).

If you’re into object-oriented programming terminology, you can compare this to association, aggregation, and composition (the comparison is far from perfect, and the terms don’t neatly align, but if you know the terminology, it should give you some intuition):

  • Regular requires directives create an uncomplicated association between the two involved modules.
  • Using requires transitive turns this into an aggregation where one module makes the other part of its API.
  • Aggregator modules are then similar to composition in the sense that the involved modules’ lifecycles are coupled—the aggregator module has no raison d’être of its own. This doesn’t quite hit the nail on the head, though, because in a true aggregation, the referenced modules have no purpose of their own—with aggregator modules, on the other hand, they typically do.

Given these categories, I’d say that requiring an aggregation’s exposed dependencies is governed by the guideline introduced in section 11.1.4, whereas depending on a composition’s exposed dependencies is always okay. To not make matters more complicated than they need to be, I won’t use the terms aggregation and composition in the rest of the book; I’ll stick to implied readability and aggregator modules.

Service binding, as explained in section 10.1.2, also tarnishes the illusion of aggregator modules being perfect placeholders. Here, the problem is that if a composing module provides a service, binding will pull it into the module graph but, of course, not the aggregator module (because it doesn’t declare to provide that service), and hence not the other composing modules. Think these cases through before creating aggregator modules.

Refactoring modules by splitting them up

I’m sure you’ve been in a situation where you realized that what you once thought of as a simple feature has grown into a more complex subsystem. You’ve improved and extended it again and again, and it’s a little tangled; so, to clean up the code base, you refactor it into smaller parts that interact in a better-defined way while keeping its public API stable.

Suppose the simple feature had its own module to begin with, and the new solution would use several modules. What happens with code that depends on the original module? If that disappears, the module system will complain about missing dependencies.

With what we just discussed, why not keep the original module and turn it into an aggregator? This is possible as long as all of the original module’s exported packages are now exported by the new modules. (Otherwise, depending on the new aggregator module doesn’t grant accessibility to all types of its former API.)

module monitor.statistics {
    requires transitive monitor.statistics.averages;
    requires transitive monitor.statistics.medians;
    requires transitive monitor.statistics.percentiles;
}
c11_06.png

Figure 11.6 Before refactoring, the statistics module contains a lot of functionality (left). It’s then decomposed into three smaller modules that contain all the code (right). To not mandate changes in modules depending on statistics, it isn’t removed, but is instead turned into an aggregator module that implies readability of the modules it was split into.

If you want clients to replace their dependency on the old module with more specific requires directives on the new ones, consider deprecating the aggregator:

@Deprecated
module my.shiny.aggregator {
    // ...
}

11.1.6 Refactoring modules by merging them

Although probably less often than splitting up a module that has outgrown its roots, you may occasionally want to merge several modules into one. As before, removing the now-technically useless modules may break clients; and as before, you can use implied readability to fix that problem: keep the empty old modules around, and make sure the old module declaration has as its only line a requires transitive on the new module.

@Deprecated
module monitor.observer.alpha {
    requires transitive monitor.observer;
}

@Deprecated
module monitor.observer.beta {
    requires transitive monitor.observer;
}
c11_07.png

Figure 11.7 Before refactoring, the observation code is shared between the three modules alpha, beta, and observer (left). Afterward, all functionality is in observer, and the hollowed modules alpha and beta imply readability on it in order to not require their clients to change (right).

11.2 Optional dependencies

In section 3.2, you saw that the module system uses requires directives to implement reliable configuration by making sure dependencies are present at compile and run time. But as we discussed at the end of section 2.3, after looking at the ServiceMonitor application for the first time, this approach can be too inflexible.

There are cases where code ends up using types that don’t have to be present at run time—they may be, but they don’t have to be. As it stands, the module system either requires them to be present at launch time (when you use a requires directive) or doesn’t allow access at all (when you don’t use it).

In this section, I’ll show you a couple of examples in which this strictness leads to problems. Then I’ll introduce the module system’s solution: optional dependencies. Coding against them isn’t trivial, though, so we’ll take a close look at that as well. By the end of this section, you’ll be able to code against modules that aren’t required to be present at run time. The branch feature-optional-dependencies in ServiceMonitor’s repository demonstrates how to use optional dependencies.

11.2.1 The conundrum of reliable configuration

Assume that there’s an advanced statistics library containing a stats.fancy module that can’t be present on the module path for each deployment of the ServiceMonitor application. (The reason is irrelevant, but let’s say it’s a licensing issue.)

You want to write code in monitor.statistics that uses types from the fancy module, but for that to work, you need to depend on it with a requires directive. But if you do that, the module system wouldn’t let the application launch if stats.fancy isn’t present. Figure 11.8 shows this deadlock. (If this case seems familiar, it’s because we looked at it before from another angle. I’ll tell you where when we come full circle in a few minutes.)

c11_08.png

Figure 11.8 The conundrum of reliable configuration: either the module system doesn’t grant statistics access to stats.fancy because statistics doesn’t require the access (left), or statistics does require access, which means stats.fancy must always be present for the application to launch (right).

Another example would be a utility library—let’s call it uber.lib—that integrates with a handful of other libraries. Its API offers functionality that builds on them and thus exposes their types. So far, that may make it look like an open-and-shut case for implied readability, as discussed in section 11.1, but things can be seen in another light.

Let’s play this through with the example of com.google.common, which uber.lib integrates with. The maintainers of uber.lib may assume that nobody who isn’t already using Guava is ever going to call the Guava portion of their library. This makes sense in certain cases. Why would you call a method in uber.lib that creates a nice report for a com.google.common.graph.Graph instance if you don’t have such a graph?

For uber.lib, that means it can function perfectly without com.google.common. If Guava makes it into the module graph, clients may call into that portion of the uber.lib API. If it doesn’t, they won’t, and the library will be fine as well. You can say that uber.lib never needs the dependency for its own sake.

With the features we’ve explored so far, such an optional relationship can’t be implemented. According to the readability and accessibility rules from chapter 3, uber.lib has to require com.google.common to compile against its types and thus force all clients to always have Guava on the module path when launching their application.

If uber.lib integrates with a handful of libraries, it would make clients depend on all of them even though they may never use more than one. That’s not a nice move from uber.lib, so its maintainers will be looking for a way to mark their dependencies as being optional at run time. As the next section shows, the module system has them covered.

11.2.2 The static modifier: Marking dependencies as optional

When a module needs to be compiled against types from another module but doesn’t want to depend on it at run time, you can use a requires staticdirective to establish this optional dependency. For two modules depending and optional, where depending’s declaration contains the line requires static optional, the module system behaves differently at compile and launch time:

  • At compile time, optional must be present or there will be an error. During compilation, optional is readable by depending.
  • At launch time, optional may be absent, and that will cause neither an error nor a warning. If it’s present, it’s readable by depending.

Table 11.1 compares this behavior with a regular requires directive. Note that although the module system doesn’t issue an error, the runtime still may. Optional dependencies make run-time errors like NoClassDefFoundError much more likely because classes that a module was compiled against can be missing. In section 11.2.4, you’ll see code that prepares for that eventuality.

Table 11.1 A comparison of how requires and requires static behave at compile and launch time for present and missing dependencies. The only difference lies in how they treat missing dependencies at launch time (far-right column).
Dependency present Dependency missing
Compile time Launch time Compile time Launch time
requires Reads Reads Error Error
requires static Reads Reads Error Ignores

As an example, let’s create an optional dependency from monitor.statistics to stats.fancy. For that, you use a requires static directive:

module monitor.statistics {
    requires monitor.observer;
    requires static stats.fancy;
    exports monitor.statistics;
}

If stats.fancy is missing during compilation, you get an error when the module declaration is compiled:

> monitor.statistics/src/main/java/module-info.java:3:
>     error: module not found: stats.fancy
>         requires static stats.fancy;
>                              ^
> 1 error

At launch time, on the other hand, the module system doesn’t care whether stats.fancy is present.

The module descriptor for uber.lib declares all dependencies as optional:

module uber.lib {
    requires static com.google.common;
    requires static org.apache.commons.lang;
    requires static org.apache.commons.io;
    requires static io.vavr;
    requires static com.aol.cyclops;
}

Now that you know how to declare optional dependencies, two questions remain to be answered:

  • Under what circumstances will the dependency be present?
  • How can you code against an optional dependency?

We’ll answer both questions next, and when we’re finished, you’re all set to use this handy feature.

11.2.3 Module resolution of optional dependencies

As discussed in section 3.4.1, module resolution is the process that, given an initial module and a universe of observable modules, builds a module graph by resolving requires directives. When a module is being resolved, all modules it requires must be observable. If they are, they’re added to the module graph; otherwise, an error occurs. A little later I wrote this about the graph:

It’s important to note that modules that did not make it into the module graph during resolution aren’t available later during compilation or execution, either.

This is where we come full circle. The first time I mentioned a fancy statistics library was in the section when I explained why it may sometimes be necessary to explicitly add a module to the module graph. I didn’t talk about optional dependencies in particular (and this isn’t the only use case for that option), but the general idea was the same as now: the fancy statistics module isn’t strictly required and hence isn’t automatically added to the module graph. If you want to have it in there, you must use the --add-modules option—either naming the specific module or using ALL-MODULE-PATH.

c11_09.png

Figure 11.9 Both sides show similar situations. Both cases involve three modules A, B, and C, where A strictly depends on B and optionally depends on C. At left, A is the initial module, leading to a module graph without C because optional dependencies aren’t resolved. At right, C was forced into the graph with the use of the command-line option --add-modules, making it the second root module. It’s hence resolved and readable by A.

Maybe you tripped over the phrase that during module resolution, optional dependencies “are mostly ignored.” Why mostly? Well, if an optional dependency makes it into a graph, the module systems adds a reads edge. So if the fancy statistics module is in the graph (maybe due to a regular requires, maybe due to an --add-modules), any module optionally depending on it can read it. This ensures that its types can be accessed straight away.

11.2.4 Coding against optional dependencies

Optional dependencies require a little more thought when you’re writing code against them, because this is what happens when monitor.statistics uses types in stats.fancy but the module isn’t present at run time:

Exception in thread "main" java.lang.NoClassDefFoundError:
    stats/fancy/FancyStats
        at monitor.statistics/monitor.statistics.Statistician
            .<init>(Statistician.java:15)
        at monitor/monitor.Main.createMonitor(Main.java:42)
        at monitor/monitor.Main.main(Main.java:22)
Caused by: java.lang.ClassNotFoundException: stats.fancy.FancyStats
        ... many more

Oops. You usually don’t want your code to do that.

Generally speaking, when the code that’s currently being executed references a type, the JVM checks whether it’s already loaded. If not, it tells the class loader to do that; and if that fails, the result is a NoClassDefFoundError, which usually crashes the application or at least fails out of the chunk of logic that was being executed.

This is something JAR hell was famous for (see section 1.3.1). The module system wants to overcome that problem by checking declared dependencies when launching an application. But with requires static, you opt out of that check, which means you can end up with a NoClassDefFoundError after all. What can you do against that?

Before looking into solutions, you need to see whether you really have a problem. In the case of uber.lib, you expect to use types from an optional dependency only if the code calling into the library already uses them, meaning class loading already succeeded. In other words, when uber.lib is called, all required dependencies must be present or the call wouldn’t have been possible. So you don’t have a problem after all, and you don’t need to do anything. Figure 11.10 illustrates this case.

c11_10.png

Figure 11.10 By assumption, calling uber.lib only makes sense when clients already use types from the optional dependency. As a consequence, all execution paths (squiggly lines) that rely on the optional dependency being available for uber.lib (top two) have already passed through client code that also relied on that dependency (striped areas). If that didn’t fail, uber.lib won’t fail, either.

The general case is different, though, as shown in figure 11.11. It may well be the module with the optional dependency that first tries to load classes from a dependency that might not be present so the risk of a NoClassDefFoundError is very real.

c11_11.png

Figure 11.11 In the general case, it isn’t guaranteed that the client code calling a module like statistics has already established the optional dependency. In that case, execution paths (squiggly lines) may first encounter the dependency in the statistics module (striped area) and will fail if the optional dependency is absent.

c11_12.png

Figure 11.12 To ensure that a module like statistics, which has an optional dependency, is stable regardless of that dependency’s presence, checkpoints are required. Based on whether the dependency is present, the code branches execution paths (squiggly lines) either into code that uses that dependency (striped area) or into other code that doesn’t.

The module system offers an API to check whether a module is present. I won’t go into details of how it works yet, because you lack some of the prerequisites that you need to understand the code. So you’ll have to wait for (or skip ahead to) section 12.4.2 to see for yourself that a utility method like the following can be implemented:

public static boolean isModulePresent(String moduleName) {
    // ...
}

Calling this method with an argument like "stats.fancy" will return whether that module is present. If called with the name of a regular dependency (simple requires directive), the result will always be true because otherwise the module system wouldn’t have let the application launch.

If called with the name of an optional dependency (requires static directive), the result will be either true or false. If an optional dependency is present, the module system established readability, so it’s safe to go down an execution path that uses types from the module. If an optional dependency is absent, choosing such a path will lead to a NoClassDefFoundError, so a different one has to be found.

11.3 Qualified exports: Limiting accessibility to specific modules

Whereas the previous two sections show how to refine dependencies, this one introduces a mechanism that allows a finer API design. As discussed in section 3.3, a module’s public API is defined by exporting packages with exports directives, in which case every module reading the exporting one can access all public types in those packages at compile and at run time. This lies at the heart of strong encapsulation, which section 3.3.1 explains in depth.

With what we’ve discussed so far, you have to choose between strongly encapsulating a package or making it accessible to everybody all the time. To handle use cases that don’t easily fit into that dichotomy, the module system offers two less-candid ways to export a package: qualified exports, which we’ll look at now; and open packages, which section 12.2 introduces, because they’re related to reflection. As before, I’ll start with examples before introducing the mechanism. By the end of this section, you’ll be able to more precisely expose APIs than is possible with regular exports directives. Look at the branch feature-qualified-exports in ServiceMonitor’s repository to see how qualified exports pan out.

11.3.1 Exposing internal APIs

The best examples showing that exports directives can be too general come from the JDK. As you saw in section 7.1, only one platform module exports a sun.* package and few export com.sun.* packages. But does that mean all other packages are only used within the module they’re declared in?

Far from it! Many packages are shared among modules. Here are some examples:

  • Internals of the base module java.base are used all over the place. For example, java.sql (providing the Java Database ConnectivityAPI [JDBC]) uses jdk.internal.misc, jdk.internal.reflect, and sun.reflect.misc. Security-relevant packages like sun.security.provider and sun.security.action are used by java.rmi (Remote Method Invocation API [RMI]) or java.desktop (AWT and Swing user interface toolkits, plus accessibility, multimedia, and JavaBeans APIs).
  • The java.xml module defines the Java API for XML Processing (JAXP), which includes the Streaming API for XML (StAX), the Simple API for XML (SAX), and the W3C Document Object Model (DOM) API. Six of its internal packages (mostly prefixed with com.sun.org.apache.xml and com.sun.org.apache.xpath) are used by java.xml.crypto (API for XML cryptography).
  • Many JavaFX modules access internal packages of javafx.graphics (mostly com.sun.javafx.*), which in turn uses com.sun.javafx.embed.swing from javafx.swing (integrating JavaFX and Swing), which in turn uses seven internal packages from java.desktop (like sun.awt and sun.swing), which …

I could go on, but I’m sure you get my point. This poses a question, though: how does the JDK share these packages among its modules without exporting them to everybody else?

Although the JDK surely has the strongest use case for a more targeted export mechanism, it isn’t the only one. This situation occurs every time a set of modules wants to share functionality between them without exposing it. This can be the case for a library, a framework, or even a subset of modules from a larger application.

This is symmetrical to the problem of hiding utility classes before the module system was introduced. As soon as a utility class has to be available across packages, it has to be public; but before Java 9, that meant all code running in the same JVM could access it. Now you’re up against the case that you want to hide a utility package, but as soon as it has to be available across modules, it must be exported and can thus be accessed by all modules running in the same JVM—at least with the mechanisms you’ve used so far. Figure 11.13 illustrates this symmetry.

c11_13.png

Figure 11.13 (Left) The situation before Java 9, where as soon as a type is public (like FancyUtil in package util), it can be accessed by all other code. (Right) A similar situation with modules, but on a higher level, where as soon as a package is exported (like util in utils.fancy), it’s accessible to all other modules.

11.3.2 Exporting packages to modules

The exportsdirective can be qualified by following it up with to ${modules}, where ${modules} is a comma-separated list of module names (no placeholders are allowed). To the modules named in an exports to directive, the package will be exactly as accessible as with a regular exports directive. To all other modules, the package will be as strongly encapsulated as if there were no exports at all. This situation is shown in figure 11.14.

c11_14.png

Figure 11.14 The module owner uses a qualified export to make the package pack accessible only to the privileged module. To privileged, it’s just as accessible as if a regular export were used; but other modules, like regular, can’t access it.

As a hypothetical example, let’s say all observer implementations in the ServiceMonitor application need to share some utility code. The first question is where to put those types. All observers already depend on monitor.observer because it contains the ServiceObserver interface they implement, so why not put it there? Okay, they end up in the package monitor.observer.utils.

Now comes the interesting part. Here’s the module declaration of monitor.observer that exports the new package only to the implementation modules:

module monitor.observer {
    exports monitor.observer;
    exports monitor.observer.utils
        to monitor.observer.alpha, monitor.observer.beta;
}

Whereas monitor.observer is exported to everybody, monitor.observer.utils will be accessible only by the modules monitor.observer.alpha and monitor.observer.beta.

This example demonstrates two interesting details:

  • The modules to which a package is exported can depend on the exporting module, creating a cycle. Thinking about it, unless implied readability is used, this must be the case: how else would the module to which a package is exported read the exporting one?
  • Whenever a new implementation wants to use the utilities, the API module needs to be changed, so it gives access to this new module. Although letting the exporting module control what can access the packages is kind of the whole point of qualified exports, it can still be cumbersome.

As a real-world example, I’d like to show you the qualified exports that java.base declares—but there are 65 of them, so that would be a little overwhelming. Instead, let’s look at the module descriptor of java.xml with java --describe-module java.xml (as described in section 5.3.1):

> module [email protected]
# everything but qualified exports are truncated
> qualified exports com.sun.org.apache.xml.internal.utils
>     to java.xml.crypto
> qualified exports com.sun.org.apache.xpath.internal.compiler
>     to java.xml.crypto
> qualified exports com.sun.xml.internal.stream.writers
>     to java.xml.ws
> qualified exports com.sun.org.apache.xpath.internal
>     to java.xml.crypto
> qualified exports com.sun.org.apache.xpath.internal.res
>     to java.xml.crypto
> qualified exports com.sun.org.apache.xml.internal.dtm
>     to java.xml.crypto
> qualified exports com.sun.org.apache.xpath.internal.functions
>     to java.xml.crypto
> qualified exports com.sun.org.apache.xpath.internal.objects
>     to java.xml.crypto

This shows that java.xml lets java.xml.cryptoand java.xml.ws use some of its internal APIs.

Now that you know about qualified exports, I can clear up a small mystery that we left behind in section 5.3.6 when we analyzed the module system’s logs. There you saw messages like these:

> Adding read from module java.xml to module java.base
> package com/sun/org/apache/xpath/internal/functions in module java.xml
>     is exported to module java.xml.crypto
> package javax/xml/datatype in module java.xml
>     is exported to all unnamed modules

I didn’t explain why the log talks about exporting to a module, but with what we just discussed, that should be clear now. As you saw in the recent example, java.xml exports com.sun.org.apache.xpath.internal.functions to java.xml.crypto, which is exactly what the second message says. The third message exports javax.xml.datatype to “all unnamed modules,” which looks a little weird but is the module system’s way of saying that the package is exported without further qualification and hence is accessible to every module reading java.xml, including the unnamed module.

  • If a module that declares a qualified export is compiled and the target module isn’t present in the universe of observable modules, the compiler will issue a warning. It’s not an error because the target module is mentioned but not required.
  • It isn’t allowed to use a package in an exportsand in an exports to directive. If both directives were present, the latter would be effectively useless, so this situation is interpreted as an implementation error and thus results in a compile error.

11.3.3 When to use qualified exports

A qualified export allows modules to share a package between them without making it available to all other modules in the same JVM. This makes qualified exports useful for libraries and frameworks that consist of several modules and want to share code without clients being able to use it. They will also come in handy for large applications that want to restrict dependencies on specific APIs.

Qualified exports can be seen as lifting strong encapsulation from guarding types in artifacts to guarding packages in sets of modules. This is illustrated by figure 11.15.

c11_15.png

Figure 11.15 (Left) How a public type in a non-exported package can be accessed by other types in the same module but not by types from other modules. (Right) A similar situation, but on a higher level, where qualified exports are used to make a package in one module available to a defined set of modules while keeping it inaccessible to unprivileged ones.

Say you’re designing a module. When should you favor qualified over unqualified exports? To answer that, we have to focus on the core benefit of qualifying exports: controlling who uses an API. Generally speaking, this becomes more important the further the package in question is from its clients.

Suppose you have a small to medium-sized application made out of a handful of modules (not counting dependencies) that’s maintained by a small team and compiled and deployed all at once. In that case, it’s comparatively easy to control which module uses which API; and if something goes wrong, it’s easy to fix it because everything is under your control. In this scenario, the benefits of qualified exports have little impact.

At the other end of the spectrum is the JDK, which is used by literally every Java project in the world and has an extreme focus on backward compatibility. Having code “out there” depend on an internal API can be problematic and is hard to fix, so the need to control who accesses what is great.

The most obvious line separating these two extremes is whether you can freely change the package’s clients. If you can, because you’re developing the module and all its client modules, regular exports are a good way to go. If you can’t, because you maintain a library or framework, only the API that you want clients to use and that you’re willing to maintain should be exported without qualification. Everything short of that, particularly internal utilities, should only be exported to your modules.

The line gets blurred in larger projects. If a big code base is maintained over years by a large team, you may technically be able to change all clients when doing so becomes necessary due to an API change, but it can be painful. In such cases, using qualified exports not only prevents accidental dependencies on internal packages, but also documents which clients an API was designed for.

11.3.4 Exporting packages on the command line

What if the use of internal APIs wasn’t foreseen (or, more likely, wasn’t intended) at the time of writing? What if code absolutely has to access types the containing module doesn’t export, qualified or not? If the module system were adamant about these rules, many applications wouldn’t compile or launch on Java 9+; but if it were an easy way to circumvent strong encapsulation, it would hardly be “strong,” thus losing its benefits. Middle ground was found by defining command-line options that can be used as an escape hatch but are too cumbersome to become a ubiquitous fix.

In addition to the exports to directive, there’s a command-line option with the exact same effect that can be applied to the compiler and run-time commands: with --add-exports ${module}/${package}=${accessing-modules}, the module system exports ${package} of $module to all modules named in the comma-separated list ${accessing-modules}>. If ALL-UNNAMED is among them, code from the unnamed module can also read that package.

Normal accessibility rules as presented in section 3.3 apply—for a module to access a type this due to an --add-exports option, the following conditions must be fulfilled:

  • The type has to be public.
  • The type has to be in ${package}.
  • The module addressed in ${accessing-modules} must read ${module}.

For --add-exports examples, flip back to sections 7.1.3 and 7.1.4, where you used it to gain access to internal APIs of platform modules at compile and run time. Like other command-line options, requiring --add-exports to be present for more than experiments is a maintainability problem; see section 9.1 for details.

Summary

  • Implied readability:
  • With a requires transitive directive, a module makes its client read the thus-required module even though the module doesn’t explicitly depend on it. This allows the module to use types from dependencies in its API without putting the burden to manually require those dependencies on the client modules. As a consequence, the module becomes instantly usable.
  • A module should only rely on a transitive dependency being implicitly readable if it only uses it on the boundary to the respective direct dependency. As soon as the module starts using the transitive dependency to implement its own functionality, it should make it a direct dependency. This ensures that the module declaration reflects the true set of dependencies and makes the module more robust for refactorings that may remove the transitive dependency.
  • Implied readability can be used when moving code between modules by having the modules that used to contain the code imply readability on the ones that do now. This lets clients access the code they depend on without requiring them to change their module descriptors, because they still end up reading the module that contains the code. Keeping compatibility like this is particularly interesting for libraries and frameworks.
  • Optional dependencies:
  • With a requires static directive, a module marks a dependency that the module system will ensure is present at compile time but can be absent at run time. This allows coding against modules without forcing clients to always have those modules in their application.
  • At launch time, modules required only by requires static directives aren’t added to the module graph even if they’re observable. Instead, you have to add them manually with --add-modules.
  • Coding against optional dependencies should involve making sure no execution path can fail due to the dependency missing, because this would severely undermine the module’s usability.
  • Qualified exports:
  • With an exports to directive, a module makes a package accessible only to the named modules. This is a third and more targeted option between encapsulating a package and making it accessible for everybody.
  • Exporting to specific modules allows sharing code within a set of privileged modules without making it a public API. This reduces the API surface of a library or framework, thus improving maintainability.
  • With the --add-exports command-line option, you can export packages at compile and run time that the module’s developers intended as internal APIs. On the one hand, this keeps code running that depends on those internals; on the other hand, it introduces its own maintainability problems.
..................Content has been hidden....................

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