We introduced the concepts of modularity in the last post. This week we’ll look at some of the more technical details around modularity, including the new keywords relating to modules, and how to create a module.
Quick Revision of Java Modularity
Modules are a new way of grouping and organising our Java code better. A module is simply a set of related packages with explicitly stated dependencies on other modules.
We can decompose a large application into a number of modules. Each module can be built as a separate smaller JAR
file and deployed as needed, rather than deploying a large JAR
file for the entire application. A module can be deployed by itself.
Module Declarations and Descriptors
Modules need to explicitly declare which modules they depend on, and which packages they export. These dependency declarations improve the integrity, security and maintainability of the code.
Modules describe these dependencies in a module declaration. This is a Java source code file called module-info.java
. Hyphens are not allowed in Java class names, but the module descriptor file uses the hyphen. The naming is similar to the package-info.java
file that contains package annotations and javadoc
comments to document a package.
After the module declaration has been compiled with a standard Java compiler, it becomes the module descriptor. A module must include a module descriptor. It is deployed in the root directory of the module’s file hierarchy.
The module descriptor contains metadata that specifies the module’s dependencies, the packages the module exposes to other modules, the packages it requires, etc. The JDK uses the module descriptors to verify the dependencies and interactions between modules both at compile-time and at runtime.
The module declaration contains the following:
- The name of the module. Module names are normally written in lowercase (similar to packages).
exports
– Specifies the packages within the module that will be available to other modules. All other packages in the module are implicitly hidden.requires
– Specifies the modules on which this module depends.uses
– Specifies the services that the current module uses (consumes).provides
– Specifies the services that the current module provides.open
– Specifies the classes in the module that can be accessed using the Reflection API.
Each module declaration starts with the keyword module
which is followed by a unique module name and a module body enclosed in braces. The exported packages and required modules will be specified inside the braces. This is very similar to the way we create a Java class:
module modulename { // module metadata goes here }
Module Directives
The body of the module declaration can be empty or it may contain various module directives. These directives include exports
, module
, open
, opens
, provides
, requires
, uses
, with
, to
and transitive
. These are contextual keywords. They are keywords only in the context of module declarations, and may be used as normal identifiers in other Java code.
Module Naming Rules
The module system relies on the uniqueness of a module’s name. We will have problems if we have conflicting module names, or names that change between application versions. It is very important to have stable and globally unique module names.
This means that when we design our system, we must put a lot of effort into good package design. We must decide which classes are placed in which package. We must decide on unique package names. The packages and their contents must be stable and conform to good object oriented design. That’s a blog post all on its own!
The best way for us to ensure that the module names are globally unique is to use the common reverse-domain naming scheme that we already use for packages:
module com.incusdata.office { // module metadata goes here }
Note: The fullstops are part of the module name. They are not interpreted as subdirectory path separators, as is done with packages.
The exports and requires Keywords
The two main keywords in a module declaration are requires
and exports
.
By default, a module doesn’t expose its API to other modules. This gives strong modular encapsulation. This makes our code more secure, but in order for anyone to be able to use it, we need to explicitly expose our API. We use the exports
directive to expose all public members of the named package.
There are two types of packages in a module: exported packages and concealed packages.
- Exported packages are intended to be used outside of the module. Any class in any other module can use these packages.
- Concealed packages are not intended to be used outside the module. They are internal to the module and can be used inside the module only. If a package is not explicitly exported, then it is implicitly hidden.
We also need to specify if this module depends on any other modules. We use the requires
directive to specify module dependencies.
Java Modularity: Example
For example, let’s say we are building an application that will track employees in an office environment. It would probably have a data access layer containing a DAO
class, a service layer which would contain an EmployeeProcessor
to process Employee
entity objects. These classes would be contained in similarly named packages. The DAO package might be a more general package not being part of the office application.
The following module declaration declares that the module com.incusdata.office
depends on the com.incusdata.dao
module, and exports the two packages com.incusdata.office.entity
and com.incusdata.office.service
:
module com.incusdata.office { requires com.incusdata.dao; exports com.incusdata.office.entity; exports com.incusdata.office.service; }
The requires
directive specifies that the module com.incusdata.office
has both a runtime and a compile-time dependency on com.incusdata.dao
.
The exports
directive specifies that the public members of com.incusdata.office.entity
and com.incusdata.office.service
packages will be accessible by any other dependent modules. Only the listed package itself is exported. No sub-packages of the exported package are exported. Private members are not accessible even if we try to use reflection. However, we can specifically permit certain forms of access depending on the command line options we use at runtime.
What’s Next?
In the next post, we’ll look at a more comprehensive example. We’ll also look at the other syntax used with the requires
and exports
keywords.
I’m always interested in your opinion, so please leave a comment. Your feedback helps me write tips that help you.