Chapter 15 Putting the Pieces Together
This chapter covers a bells and whistles version of Service Monitor, whether to use modules,
what an ideal module might look like, keeping module declarations clean, comparing the module
system to build tools, OSGI, and microservices.
Now that we've covered pretty much everything there is to know about the module system,
it's time to wrap things up.
In this final chapter, I want to connect the dots and give a few pieces of advice for creating
awesome modular applications.
The first step is to show you an example of how the various features discussed throughout
the book can come together by applying most of them to the Service Monitor application
in Section 15.1.
Then, I'll take a deep dive into a number of more general concerns that will help you
decide whether to even create modules, what to aim for when doing so, and how to carefully
evolve your module declarations so they stay squeaky clean in Section 15.2.
I'll close with a review of the technology landscape surrounding the module system in
Section 15.3 and my vision for Java's modular ecosystem in Section 15.4.
Section 15.1 Adding Bells and Whistles to Service Monitor
Chapter 2 showed the anatomy of the Service Monitor application.
In Section 2.2, you created simple modules that only used plain requires and exports
directives.
Since then, we've not only discussed those in detail, but also explored the module system's
more advanced features.
We've looked at each of them individually, but now I want to put them all together.
To enjoy the Service Monitor application in all its glory, check out the repository's
Features Combined branch.
The following listing contains the declarations for all the modules in Service Monitor.
Listing 15.1, Service Monitor, using advanced features presented throughout the book.
If you compare this listing to Listing 2.2 or look at Figure 15.1, you can see that the
fundamental structure of Service Monitor has stayed pretty much the same, but looking closer,
you can see a number of improvements.
Let's go over them one by one.
Figure 15.1a Figure 15.1b, Comparison of Module Graphs
for the Service Monitor application depending on feature use.
The first variant only uses plain exports and requires directives, a, whereas the second
makes full use of refined dependencies and exports as well as services, b, the basic
variant has been extended to include the same modules and packages as the advanced one.
Section 15.1.1, Diversified Dependencies.
One change that's easy to spot are the requires transitive and requires optional directives.
Although plain requires directives are the right choice in most cases, a significant
portion of dependencies are a little more complicated.
The most obvious case is optional dependencies, where a module uses types from another module
and hence needs to be compiled against it, but the dependency may still be absent at
runtime.
This is exactly the case for Monitor.Statistics and Stats.Fancy, so the dependency is established
with a requires static directive.
The module system will then enforce the presence of Stats.Fancy when compiling Monitor.Statistics,
which makes sense because otherwise compilation would fail, and will add a reads edge from
Monitor.Statistics to Stats.Fancy if the latter made it into the module graph, which also
makes sense because otherwise Monitor.Statistics couldn't access types from Stats.Fancy.
But Stats.Fancy may not make it into the module graph, in which case Monitor.Statistics
has to handle its absence.
Listing 15.2.
Checking whether the optional dependency Stats.Fancy is present.
Optional dependencies are discussed in detail in Section 11.2.
The other case is a little less obvious than optional dependencies, but no less common,
maybe even more so.
The module Monitor.REST, for example, has this method in its public API.
See this code?
But statistics comes from Monitor.Statistics, so any module using REST needs to read statistics
or it can't access statistics and thus can't create a monitor server.
In other words, REST is useless to modules that don't also read statistics.
In the Service Monitor application, this happens surprisingly often.
Every module that requires at least one other module and exports a package ends up being
in that situation.
That's considerably more frequent than out in the wild, and only happens that often because
the modules are so small that almost all of their code is public API.
It would be surprising if they didn't constantly expose their dependencies' types in their
own APIs.
So although this occurs more rarely in practice, you can still expect to see it on a daily
basis.
In the JDK, roughly 20% of the dependencies are exposed.
To not keep users guessing about which other modules they need to require explicitly, which
is cumbersome and bloats module declarations, the module system offers requires transitive.
Because REST requires transitive statistics, any module reading REST also reads statistics,
and thus users of REST are spared the guesswork.
Implied readability is discussed in detail in section 11.1.
In section 15.1.2, reduced visibility.
Another change from the application's original versions in section 2.2 is that its modules
work harder to reduce their API surface.
The updated modules use considerably fewer plain exports directives.
Thanks to services, the observers no longer have to export their implementations.
By using qualified exports, the package monitor.observer.utils in monitor.observer is only accessible to
a selected set of modules.
Monitor.persistence opens its entity package instead of exporting it, thus only making
it available at runtime.
These changes reduce the amount of code that's readily accessible for any random module,
which means developers can change more code inside a module without having to worry about
the effects on downstream consumers.
Reducing the API surface this way is a boon for the maintainability of frameworks and
libraries, but large applications with many modules can also benefit.
Section 11.3 introduces qualified exports, and section 12.2 explores open packages.
Section 15.1.3, decoupled with services.
The only structural change of the module graph, compared to section 2.2, is that monitor no
longer directly depends on the observer implementations.
Instead, it only depends on the module providing the API, monitor.observer, and it uses service
observer factory as a service.
All three implementing modules provide that service with their specific implementations,
and the module system connects the two sides.
This is much more than just an aesthetic improvement.
Thanks to services, it's possible to configure aspects of the application's behavior, which
kinds of services it can observe, at launch time.
New implementations can be added, and obsolete ones can be removed by adding or removing
modules that provide that service.
No changes of monitor are required, and hence the same artifacts can be used without having
to rebuild them.
To learn all about services, check out chapter 10.
Section 15.1.4 loads code at runtime with layers.
Although services allow us to define the application's behavior at launch time, we even went one
step further.
It isn't visible in the module declarations, but by enabling the monitor module to create
new layers, we made it possible for the application to start observing services at runtime, for
which it didn't even have the service observer implementation when it launched.
On demand, monitor will create a new module graph and, together with a new class loader,
load additional classes and update its list of observers.
Section 15.3.
Creating a new layer with the graph created for modules on those paths.
Such behavior is particularly interesting for applications that aren't frequently redeployed
and where restarts are inconvenient.
Complex desktop applications come to mind, but a web backend that runs on the customer's
premises and needs to be comprehensibly configurable could also qualify.
For a discussion of what layers are and how to create them, see section 12.4.
Section 15.1.5 handles dependencies on plain jars.
Another detail that isn't obvious from the module declarations is the modularization
status of Service Monitor's third-party dependency.
Neither the Hibernate version nor the Spark version it uses is modularized yet, and they
still ship as plain jars.
Because explicit modules require them, they need to be on the module path, though, where
the module system turns plain jars into automatic modules.
So although Service Monitor is fully modularized, it can nonetheless depend on non-modularized
jars.
Looking at this from the ecosystem-wide perspective, where the JDK modules sit at the bottom and
application modules are at the top, this is effectively a top-down modularization effort.
Automatic modules in particular are covered in section 8.3, but all of chapter 8 applies
here.
If you want to catch up on modularization strategies, check out section 9.2.