Modular Development and Its Impact
In software engineering, modularity is an important concept. From the point of view of performance as well as maintainability, it is important to create autonomous units called modules. These modules can be tied together to make a complete system. The modules provides encapsulation where the implementation is hidden from other modules. Each module can expose distinct APIs that can act as connectors so that other modules can communicate with it. This type of design is useful as it promotes loose coupling, helps focus on singular functionality to make it cohesive, and enables testing it in isolation. It also reduces system complexity and optimizes application development process. Improving performance of each module helps improving overall application performance. Hence, modular development is a very important concept.
I know you may be thinking, wait a minute, isn't Java already modular? Isn't the object-oriented nature of Java already providing modular operation? Well, object-oriented certainly imposes uniqueness along with data encapsulation. It only recommends loose coupling but does not strictly enforce it. In addition, it fails to provide identity at the object level and also does not have any versioning provision for the interfaces. Now you may be asking, what about JAR files? Aren't they modular? Well, although JARs provide modularization to some extent, they don't have the uniqueness that is required for modularization. They do have a provision to specify the version number, but it is rarely used and also hidden in the JAR's manifest file.
So we need a different design from what we already have. In simple terms, we need a modular system in which each module can contain more than one package and offers robust encapsulation compared to the standard JAR files.
This is what Java 9's modular system offers. In addition to this, it also replaces the fallible classpath mechanism by declaring dependencies explicitly. These enhancements improve the overall application performance as developers can now optimize the individual self-contained unit without affecting the overall system.
This also makes the application more scalable and provides high integrity.
Let's look at some of the basics of the module system and how it is tied together. To start off with, you can run the following commands to see how the module system is structured:
$java --list-modules
If you are interested in a particular module, you can simply add the module name at the end of the command, as shown in the following command:
$java --list-modules java.base
The earlier command will show all the exports in packages from the base module. Java base is the core of the system.
This will show all the graphical user interface packages. This will also show requires
which are the dependencies:
$java --list-modules java.desktop
So far so good, right? Now you may be wondering, I got my modules developed but how to integrate them together? Let's look into that. Java 9's modular system comes with a tool called JLink. I know you can guess what I am going to say now. You are right, it links a set of modules and creates a runtime image. Now imagine the possibilities it can offer. You can create your own executable system with your own custom modules. Life is going to be a lot more fun for you, I hope! Oh, and on the other hand, you will be able to control the execution and remove unnecessary dependencies.
Let's see how to link modules together. Well, it's very simple. Just run the following command:
$jlink --module-path $JAVA_HOME/jmods:mlib --add-modules java.desktop --output myawesomeimage
This linker command will link all the modules for you and create a runtime image. You need to provide a module path and then add the module that you want to generate a figure and give a name. Isn't it simple?
Now, let's check whether the previous command worked properly or not. Let's verify the modules from the figure:
$myawesomeimage/bin/java --list-modules
The output looks like this:
With this, you will now be able to distribute a quick runtime with your application. It is awesome, isn't it? Now you can see how we moved from a somewhat monolithic design to a self-contained cohesive one. Each module contains its own exports and dependencies and JLink allows you to create your own runtime. With this, we got our modular platform.
Note that the aim of this section is to just introduce you to the modular system. There is a lot more to explore but that is beyond the scope of this book. In this book, we will focus on the performance enhancement areas.
Quick Introduction to Modules
I am sure that after reading about the modular platform, you must be excited to dive deep into the module architecture and see how to develop one. Hold your excitement please, I will soon take you on a journey to the exciting world of modules.
As you must have guessed, every module has a property name
and is organized by packages. Each module acts as a self-contained unit and may have native code, configurations, commands, resources, and so on. A module's details are stored in a file named module-info.java
, which resides in the root directory of the module source code. In that file, a module can be defined as follows:
module <name>{ }
In order to understand it better, let's go through an example. Let's say, our module name is PerformanceMonitor
. The purpose of this module is to monitor the application performance. The input connectors will accept method names and the required parameters for that method. This method will be called from our module to monitor the module's performance. The output connectors will provide performance feedback for the given module. Let's create a module-info.java
file in the root directory of our performance application and insert the following section:
module com.java9highperformance.PerformanceMonitor{ }
Awesome! You got your first module declaration. But wait a minute, it does not do anything yet. Don't worry, we have just created a skeleton for this. Let's put some flesh on the skeleton. Let's assume that our module needs to communicate with our other (magnificent) modules, which we have already created and named--PerformanceBase
, StringMonitor
, PrimitiveMonitor
, GenericsMonitor
, and so on. In other words, our module has an external dependency. You may be wondering, how would we define this relationship in our module declaration? Ok, be patient, this is what we will see now:
module com.java9highperformance.PerformanceMonitor{ exports com.java9highperformance.StringMonitor; exports com.java9highperformance.PrimitiveMonitor; exports com.java9highperformance.GenericsMonitor; requires com.java9highperformance.PerformanceBase; requires com.java9highperformance.PerformanceStat; requires com.java9highperformance.PerformanceIO; }
Yes, I know you have spotted two clauses, that is, exports
and requires
. And I am sure you are curious to know what they mean and why we have them there. We'll first talk about these clauses and what they mean when used in the module declaration:
exports
: This clause is used when your module has a dependency on another module. It denotes that this module exposes only public types to other modules and none of the internal packages are visible. In our case, the modulecom.java9highperformance.PerformanceMonitor
has a dependency oncom.java9highperformance.StringMonitor
,com.java9highperformance.PrimitiveMonitor
, andcom.java9highperformance.GenericsMonitor
. These modules export their API packagescom.java9highperformance.StringMonitor
,com.java9highperformance.PrimitiveMonitor
, andcom.java9highperformance.GenericsMonitor
, respectively.requires
: This clause denotes that the module depends upon the declared module at both compile and runtime. In our case,com.java9highperformance.PerformanceBase
,com.java9highperformance.PerformanceStat
, andcom.java9highperformance.PerformanceIO
modules are required by ourcom.java9highperformance.PerformanceMonitor
module. The module system then locates all the observable modules to resolve all the dependencies recursively. This transitive closure gives us a module graph which shows a directed edge between two dependent modules.
Note
Note: Every module is dependent on java.base
even without explicitly declaring it. As you already know, everything in Java is an object.
Now you know about the modules and their dependencies. So, let's draw a module representation to understand it better. The following figure shows the various packages that are dependent on com.java9highperformance.PerformanceMonitor
.
Modules at the bottom are exports
modules and modules on the right are requires
modules.
Now let's explore a concept called readability relationship. Readability relationship is a relationship between two modules where one module is dependent on another module. This readability relationship is a basis for reliable configuration. So in our example, we can say com.java9highperformance.PerformanceMonitor
reads com.java9highperformance.PerformanceStat
.
Let's look at com.java9highperformance.PerformanceStat
module's description file module-info.java
:
module com.java9highperformance.PerformanceStat{ requires transitive java.lang; }
This module depends on the java.lang module
. Let's look at the PerformanceStat
module in detail:
package com.java9highperformance.PerformanceStat; import java.lang.*; public Class StringProcessor{ public String processString(){...} }
In this case, com.java9highperformance.PerformanceMonitor
only depends on com.java9highperformance.PerformanceStat
but com.java9highperformance.PerformanceStat
depends on java.lang
. The com.java9highperformance.PerformanceMonitor
module is not aware of the java.lang
dependency from the com.java9highperformance.PerformanceStat
module. This type of problem is taken care of by the module system. It has added a new modifier called transitive. If you look at com.java9highperformance.PerformanceStat
, you will find it requires transitive java.lang
. This means that any one depending on com.java9highperformance.PerformanceStat
reads on java.lang
.
See the following graph which shows the readability graph:
Now, in order to compile the com.java9highperformance.PerformanceMonitor
module, the system must be able to resolve all the dependencies. These dependencies can be found from the module path. That's obvious, isn't that? However, don't misunderstand the classpath with the module path. It is a completely different breed. It doesn't have the issues that the packages have.