Embrace modules

Modules came about in JDK 9 as a means of modularizing Java itself, allowing for the first time to more explicitly prevent access to implementation code and to more explicitly state what is public API. For many years prior to the release of Java 9, the developers of the JDK warned developers to not make use of APIs in some packages (most notably com.sun.*), because these were not considered part of the Java API and were implementation APIs (as we discuss in more detail in the minimize API visibility best practice). However, because Java lacked any visibility controls, any developer could access these implementation APIs, regardless of whether it was intended for public use or not, and regardless of the requests of the JDK developers. This proved to be a massive millstone around the neck of JDK developers as it prevented evolution of implementation classes, because they effectively became API. When JDK 9 shipped it included the concept of modules, and with modules came the ability to 'export' packages explicitly, with any package not exported effectively being invisible to users of that library. This broke a lot of the Java ecosystem, because of their dependency on these implementation APIs, and it has taken many years to get things working again in some cases.

Overview

Despite the fact that Java modules were largely developed to modularize the Java platform itself, it does have some benefits for developers of libraries. Most notably is the ability to specify a module that explicitly exports some subset of the packages in the library, whilst keeping the others hidden and off the classpath. This is achieved by way of a module-info.java file in the root package of the packaged library, taking the following form (by way of example using an Azure SDK for Java library):

module com.azure.data.appconfiguration {
    requires transitive com.azure.core;

    exports com.azure.data.appconfiguration;
    exports com.azure.data.appconfiguration.models;
}

What you see here is that we have explicitly named this module com.azure.data.appconfiguration. Explicitly naming a module correctly is critical. Common practice is to use the same name as the base package for the library. In this case, all APIs are in the com.azure.data.appconfiguration base package, so it makes sense that this is also the module name.

On the next line you'll note that there is a transitive dependency on com.azure.core, which is the core library that all Azure SDK for Java libraries depend on. The transitive keyword tells the module system that we want the com.azure.data.appconfiguration library to depend on not just com.azure.core, but also everything that com.azure.core depends on.

The last two lines are the most important - we are exporting two packages. All classes in these two packages (assuming the right access modifiers) are part of the public scope. All other packages, including sub-packages, are not within public scope.

Important considerations

There are a few considerations to have if you are to introduce modules:

  1. The module-info.java file is only recognized by Java 9 and later. If this library is used on Java 8 and earlier, all packages remain in scope.
  2. Module names must be unique. Using the reverse domain name approach that Java has historically used for package naming works very well for module naming too.
  3. Packages cannot be split over multiple modules. For example, you cannot have a com.foo.bar package in both module A and module B. It is allowable to have common package structure though, for example, module A can have com.foo.bar and module B can have com.foo.bar.baz.