IBM Z Day on Nov. 21: Discover the ideal environment for modern, mission-critical workloads. Learn more

Java 9+ modularity: How to design packages and create modules, Part 2

In Part 1…

In Part 1 of this tutorial, you discovered the Java Platform Module System (JPMS) that was introduced in Java 9 and its two main goals: reliable configuration and strong encapsulation.

We focused on the security capabilities that JPMS brought to programming and explored this new concept in detail. You mastered the module declaration directives (module-info.java) which broadened your understanding about how to design packages and modules, and also learned how to organize access to them and I showed you the effect modules have on existing JDK and developed APIs.

Now… export and run configurations and accessibility tricks

In this tutorial, you will continue to explore more advanced concepts, but this time, you’ll see them in action. You will learn:

  • How to export a package in the custom module for use in other modules
  • How to use a class from the exported module in a current application that resides in a different module
  • How to compile and run your application from an exploded module folder
  • How to package this custom module application into JAR files and run them from a modular JAR file

And finally, you get to explore Java 9+ strong encapsulation and accessibility tricks.

Let’s go.

Create and use custom modules

The best way to explain a theory is by providing a live example to detail the problem and how it is solved. To illustrate the powerful concepts of reliable configuration and strong encapsulation, let’s develop customized module packages that you can export and use in other customized modules. And while you’re doing that, I’ll explain more about advanced module descriptor directives.

Develop the custom modules application

To demonstrate a module that depends on another custom module as well as standard modules, I have developed a Java SE 9+ custom modules project called HelloWorld-Modules using IntelliJ IDEA 2019+. It consists of:

  • The main module eg.com.taman.client, which uses the eg.com.taman.widget module components to render the required passed message and returns the customized message of a specific type to print it
  • A message type that resides in the eg.org.taman.data module

The project structure is three modules and five packages, organized this way:

  • The main module (1) that contains the main application, eg.com.taman.client, contains one package: eg.com.taman.main
  • The support module (2), eg.com.taman.widget, contains two packages: eg.com.taman.renderer and eg.com.taman.support
  • The data module (3), eg.org.taman.data, contains two packages: eg.org.taman.data and eg.org.taman.data.type

Let’s develop the application by first starting Intellij IDEA IDE; you have two options:

  • Open the project HelloWorld-Modules, which is located under the Java-SE-Code-Examples/12/modularity folder you cloned previously from my GitHub repository.
  • Or, if you prefer, to follow along with me to develop it from scratch as I do here using the Intellij IDEA IDE.

Also, it works just the same if you would like develop this project using command lines tools like we did for the project HelloWorld-Module example in Part 1 of this tutorial.

This is the plan on how we will explore module development and how modules and APIs interrelate:

  • Develop a module and export a package for use in other modules
  • Use a class from the export package in another developed module
  • Export a specific package for use in a specific module
  • Compile, JAR, and run the main module
  • Examine a module-dependency diagram

First steps to take to develop the modules

  1. Open your IntelliJ IDEA 2019+.
  2. Click on the new project.
  3. Choose Empty project and click Next.
  4. Give the project name (HelloWorld-Modules) and click Finish; the project opens in IntelliJ and the entire project structure automatically opens pointing to modules.
  5. From the left pane, click the project, then choose your main JDK (JDK 12) and project language level (JDK 12).
  6. Click Apply and then Finish.

Here’s how to set up the project structure dialog from within IntelliJ:

  1. From the left menu, choose Modules; in the middle pane, click the + sign to create three modules.
  2. Click the + sign, and then click New module, choose JDK 9, click Next, enter first module name (eg.com.taman.client), and click Finish.
  3. Click the + sign and follow the same wizard you did for the first module; enter the second module name (eg.com.taman.widget), and click Finish.
  4. Click the + sign and follow the wizard again, entering the third module name (eg.org.taman.data), and don’t forget to click Finish.

All modules should appear in the middle pane:

Modules list

Make sure that module JDKs are the same as the project JDK.

Develop the modules components

Under the project folder, you should see the following structure:

Modules structure

Now let’s develop each project module’s components.

Develop eg.org.taman.data

For any module, you should first create the module descriptor (module-info.java).

  1. Right-click the module source folder, then New module-info.java; an empty module descriptor is created with a default module name as shown here:

    Data module

  2. To create a new package, right-click the src folder, click New, and then click Package.

  3. Enter the package name (eg.org.taman.data) and click OK.
  4. Right-click the new package, click New, click Class, and enter the class name (Message.java).
  5. Add the following code to the Message.java file:

     package eg.org.taman.data;
     import eg.org.taman.data.type.Type;
     import java.util.Objects;
    
     public class Message{
     privateStringmessage;
     privateTypetype;
     public String getMessage(){
     return this.message;
     }
    
     public void setMessage(String message){
     if(Objects.isNull(message)|| message.isEmpty())
     throw new IllegalArgumentException("Invalid content;message should not be Null,or empty!");
    
     this.message = message;
     }
    
     public Type getType(){
     return type;
     }
    
     public void setType(Type type){
     this.type = type;
     }
     }
    
  6. Create a second package (eg.org.taman.data.type), and inside the package, create a new enum named Type.java; the enum code will look like this:

     package eg.org.taman.data.type;
    
     public enum Type {
         XML,
         JSON,
         STRING
     }
    
  7. Update the module descriptor like so:

module eg.org.taman.data {
  exports eg.org.taman.data;
  exports eg.org.taman.data.type to eg.com.taman.widget;
}
Develop eg.com.taman.widget
  1. Right-click the module source folder, then New module-info.java; an empty module descriptor is created with a default module name as shown here:

    Widget module

  2. To create a new package, right-click the src folder, click New, click Package.

  3. Enter the package name (eg.com.taman.support) and click OK.
  4. Create another package name (eg.com.taman.renderer) and click OK.
  5. In the renderer package folder, create a new class named SimpleRenderer.java and add the following code to the SimpleRenderer.java class:

     package eg.com.taman.renderer;
     import eg.org.taman.data.Message;
    
     public class SimpleRenderer {
         public void renderAsString(Message message) {
             System.out.println(processMessage(message));
         }
         private String processMessage(Message msg){
             return String.format("%n {Message= %s', Type= %s}",
                     msg.getMessage(),
                     msg.getType().toString());
         }
     }
    
  6. In the support package folder, create a new class named RendererSupport.java.

  7. Update the module descriptor with the following code:

     module eg.org.taman.widget{
        exports eg.com.taman.support;
        requires transitive eg.org.taman.data;
     }
    
  8. You will see a red bulb next to the Requires module name field; click it and add the dependency to your module.

  9. Add the following code to the RendererSupport.java class:

     package eg.com.taman.support;
     import eg.com.taman.renderer.SimpleRenderer;
     import eg.org.taman.data.Message;
     import static eg.org.taman.data.type.Type.*;
    
     public class RendererSupport {
         private Message message = new Message();
         public void render(String message) {
             this.message.setMessage(message);
             this.message.setType(STRING);
             new SimpleRenderer().renderAsString(this.message);
         }
         public Message getCurrentMessage(){
             return this.message;
         }
     }
    
Develop eg.com.taman.client
  1. Right-click the module source folder, then New module-info.java; an empty module descriptor is created with a default module name as shown here:

    Client module

  2. To create a new package, right-click the src folder, click New, and then click Package.

  3. Enter the package name (eg.com.taman.main) and click OK.
  4. In the main package folder, create a new class named Client.java.
  5. Update the module descriptor like so:

     module eg.com.taman.client {
         requires eg.com.taman.widget;
     }
    
  6. You will see a red bulb next to the Requires module name field; click it and add the dependency to your module.

  7. Add the following code to the Client.java class:

     package eg.com.taman.main;
     import eg.com.taman.support.RendererSupport;
    
     public class Client {
      public static void main(String[] args) {
             RendererSupport support = new RendererSupport();
     support.render("Welcome to the Java 12 Platform Module System");
             System.out.printf("%n %s Message: %s %n Type: %s", "-----------------",
             support.getCurrentMessage().getMessage(),
             support.getCurrentMessage().getType());
         }
     }
    

Compile and run the project

Right-click Client.java and click Run. It compiles the project and runs the main method. The output should be like this:

{Message= Welcome to the Java 12 Platform Module System', Type= STRING}

 -----------------
 Welcome to the Java 12 Platform Module System
 -----------------
 STRING

Some tricks with modules declarations

The following are some common questions you might ask at this point, along with the answers.

Why can’t I access the message and message type in eg.com.taman.client without requiring the module eg.org.taman.data?

This is because you declared implied readability in package eg.com.taman.widget using the command requires transitive referring to the eg.org.taman.data module. If you try to remove the transitive keyword and compile it, you will get an access error, therefore you must require the package.

Why is there a compiler error when I try to import the Type class?

Because there is a requires directive to the data module from the client module. It is a qualified exports to the widget module only using exports ... to, which implies strong encapsulation.

Why, when I try to access the SimpleRender class from widget renderer package, does it raise a compilation error?

This is because the widget module doesn’t export to the outside world even if it has public types; it is private and only accessible inside the widget module.

Package custom modules to a modular JAR

So far, in the previous section on developing custom modules using IntelliJ 2019, you’ve developed, compiled, and run application modules from the exploded modular folder. Now, let’s move to the next step and create, compile, and run custom modules from modular JAR files.

Open your project in the IDE.

Create module JARs

  1. Open the project structure and click artifacts from the left pane.
  2. In the middle pane, click the + sign and the Add menu opens; select JAR and then chose from modules with dependencies.
  3. A dialog will open like this:

    Create a JAR file

  4. Click OK.

  5. Repeat this for other modules, but don’t create the main class for them because they are just normal modules.
  6. Make sure the following artifacts — eg.com.taman.client:jar, eg.com.taman.widget:jar, eg.com.taman.data:jar — are in the middle pane for the next step:

    Module JARs

  7. Make sure that each module JAR only contains its output module folder:

    Client JAR contains its source only

    Widget JAR contains its source only

  8. Click OK to close the project structure dialog. The final step is to package the modules.

  9. From the main menu, choose Build, and under the Build folder, click Build artifacts.
  10. From the menu, select each artifact you want to build; you should see the “Compilation completed successfully in the 4s 954ms” message in the event log window.
  11. Navigate to the project output folder, and you’ll find three JARs:

    The final JARs

Run the modular JAR eg.com.taman.client.jar

  1. Open the terminal window in your IDE; it opens on the project root:

    Run the client JAR

  2. Type the following command:

     mohamed_taman:HelloWorld-Modules$ java -p mlib -m eg.com.taman.client/eg.com.taman.main.Client
    

    If you are using Java 9 through 11, but if you use Java 12, you have to run the following command with the --enable-preview option because Java 12 contains a preview language feature, the new switch expression:

     mohamed_taman:HelloWorld-Modules$java --enable-preview -p mlib -m eg.com.taman.client/eg.com.taman.main.Client
    

    This should run successfully and output the following:

     {Message= Welcome to the Java 12 Platform Module System', Type= JSON}
      -----------------
      Welcome to the Java 12 Platform Module System
      -----------------
      JSON
    

Custom modules and the dependency graph

Let’s look at our project’s dependency graph (from the HelloWorld-modules project). First, you need to generate the graph:

  1. From the project window of the IDE, right-click the eg.com.taman.client module.
  2. Choose Diagrams > Show diagrams from the menu.
  3. Choose the Java modules diagram; you should see something like this:

    Project dependency graph

Next, let’s have a look at Java 9 strong encapsulation and accessibility concepts and rules.

Strong encapsulation and accessibility

A good question to ask at this point is how Java 9 implements strong encapsulation and accessibility.

Before Java 9, you could use any public class that you imported into your code. Whether you could access a class’s members was determined by how they were declared — public, protected, package access, or private.

Due to Java 9’s strong encapsulation in modules, public types in a module are no longer accessible to your code by default, so public no longer means available to all according to the following rules:

  • If a module exports a package, the public types in that package are accessible by any module that reads the package’s module.
  • If a module exports a package to a specific module (via exports...to), the public types in that package are accessible only to the specific module and only if that module reads the package’s module.
  • If a module does not export a package, the public types in that package are accessible only within their enclosing module.

Once you have access to a type in another module, then the normal rules of the public, protected, package access, and private apply.

It is worth it to note that when a requires dependency is not fulfilled by an exports clause in another module, a compilation error occurs.

To finish this tutorial, let’s look a little deeper into dependency graphs and how they can help you avoid module dependency problems and build cleaner modular code.

Cleaner modular code with module-dependency graphs

To cover the topic of how a module-dependency graph can help you make better modular code, you should understand the following:

  • The dependency graph components of a module depending on the java.desktop module
  • The aggregator modules java.se and java.se.ee
  • The errors that occur if a module directly or indirectly requires itself (also known as a cycle)

All the source code for this section resides under the Java-SE-Code-Examples/12/modularity/DependencyGraph folder, from the repository you cloned previously at the beginning of this tutorial.

To understand the dependency graph, first check the simple module module.graph.test, which depends on java.desktop and its components. To show it, use the following module declaration:

modulemodule.graph.test {
requiresjava.desktop;
}

IntelliJ IDEA shows the module declaration graph for module.graph.test as a box and also shows the java.desktop module because it’s explicitly listed in a requires directive:

java.desktop dependency graph

The other modules with blue lines (java.xml and java.datatransfer) are included in the graph because java.desktop requires them transitivity while java.prefs (the direct gray line) indicates it is declared with the requires directive by java.desktop.

Let’s look at a more complex module-dependency diagram.

The aggregator module java.se

This is a more complex java.se module’s dependency graph, known as an aggregator module that specifies via requires transitive all the modules necessary to support Java SE 9 apps:

The java.se dependency graph

To produce this graph, browse the external JDK 12 library in IntelliJ, locate the java.se module and open it, and right-click the java.se library root. From the menu, choose Diagrams and then click Show Java module diagram.

If you are using Java 9 or 10, follow the same steps as you did previously for a java.se module.

Some modules have a dotted line through the names; these are deprecated and marked for removal in the future releases.

Problems to avoid in the module-dependency graph

To avoid putting your module graph into a cycle, a module is not allowed to directly or indirectly reference itself and cause a compilation error. Two scenarios to avoid are:

  • A module that incorrectly requires itself
  • Two modules that incorrectly require each other

A module that incorrectly requires itself

Consider the following module declaration in which the module requires itself:

Modulemymodule {
requires mymodule;
}

When you compile this declaration, the following error occurs, indicating a cycle in the module’s dependencies:

module-info.java:2: error: cyclic dependence involving mymodule
requires mymodule;
^
1 error

Two modules that incorrectly require each other

Similarly, consider a project named CircularDependency containing two modules, module1 and module2, with the structure mentioned in the code.

If the module declarations for these two modules indicate that each module requires the other:

modulemodule1 {
exportspackage1;
requiresmodule2;
}

and

modulemodule2 {
exportspackage2;
requiresmodule1;
}

Then, when you compile these modules:

javac --module-source-path src ^
--module-path mods -d mods ^
src/module1/module-info.java ^
src/module1/package1/Class1.java ^
src/module2/module-info.java ^
src/module2/package2/Class2.java

the compiler again issues an error indicating a cycle in the module dependencies:

src\module1\module-info.java:9: error: cyclic dependence involvingmodule2
requires module2;
^
1 error

Modules in a cycle are really just one module

Ultimately, all the modules in a cycle are really one module, not separate modules.

A friend of mine who works for a large organization told me that his team was working with multiple large pre-Java 9 JAR files in this project to migrate to the modular Java 9. Initially, the team thought they would make each JAR a separate module, but their JARs turned out to be so interdependent that they decided to combine them into a single module.

This interdependency leads to cycles in your design.

Ideally, when you modularize a previously monolithic system, you want to break that system into separate modules that are easier to maintain and secure. The reason developers often avoid doing this is that it can cost you in significant refactoring challenges with especially large code bases.

Summary

So far in this series, you’ve seen the creation of a custom module and used it from inside a modular application. You’ve compiled and run an application that uses a custom module and learned how to package a custom module and modular application into a modular JAR file and run the JAR successfully.

You got a chance to explore strong encapsulation and accessibility and took a more detailed look at what a module-dependency graph is and how you can use it to avoid module dependencies and help you write better modular code.

The final tutorial of this series covers the difficulties and pitfalls of migrating from Java 8 to Java 9+.

Mohamed Taman