© Alexandru Jecan  2017

Alexandru Jecan, Java 9 Modularity Revealed, https://doi.org/10.1007/978-1-4842-2713-8_4

4. Defining and Using Modules

Alexandru Jecan

(1)Munich, Germany

In this chapter we’ll start to develop modular applications in Java 9 using some of the features offered by Project Jigsaw. We’ll begin by explaining the new concept of a Jigsaw module together with its module declaration, the module-info.java file. You’ll also learn about the five types of directives that can be used inside the module declaration: requires, exports, uses, provides, and opens. Then we’ll look at compiling and running modules using JDK 9, and for that we’ll introduce the new module path in detail.

The accessibility changes introduced in Java 9 are also covered in this chapter. They have a great impact on the platform because they differ almost entirely from the old accessibility rules that were in place before in Java. In Java 9 we can have more types of modules: normal modules, automatic modules, named modules, observable modules, open modules, and unnamed modules. Each is briefly covered in this chapter.

The Concept of Module

As you know by now, Java 9 introduces a new sort of first-class components called modules. A Jigsaw module, also a fundamental part of the Java 9 Platform, represents a container of packages. It contains packages, resource files, and native code. The packages can contain Java classes, enumerations, and interfaces.

Figure 4-1 displays the general structure of a module.

A431534_1_En_4_Fig1_HTML.gif
Figure 4-1. General structure of a module

A module consists of the source files together with the module declaration represented by the module-info.java file. Here’s a typical directory structure of a module named com.apress.moduleA:

src/
    com.apress.moduleA/
                            module-info.java
                            com/apress/moduleA/
                                             Main.java
                                             // other files

A directory having the same name with the name of the module is located right at the top. Inside it is the module-info.java file as well as a structure of directories representing the format of the package. In our case, the package has the same name as the module name. The .java files are located inside the directories of the package.

Note

It’s also possible to define a module that has no source files and no packages except the module descriptor module-info.java.

We mentioned the module declaration module-info.java. In the next subsection you’ll learn about it.

Module Declaration

Each module has a module declaration located into a special new file called module-info.java, located in the top level of the directory. To define a module in Java 9, we create the file module-info.java and put inside it the new keyword module followed by the module name and the module declaration in curly brackets.

Declaring a module is easy and straightforward. In this example, a module called com.apress.moduleA is declared inside the module-info.java file:

module com.apress.moduleA {}

In this case, the module declaration doesn’t contain anything besides the module heading. Module com.apress.moduleA doesn’t require any modules, nor does it export any packages, and it doesn’t provide or consume any services.

As already mentioned, each module must have a module declaration and thus must contain an own module-info.java file. This rule applies no matter whether it’s a platform module or a module created by developers. If the module-info.java file isn’t present, the Java compiler doesn’t treat the source code as a module. The module-info.java file gets compiled in exactly the same way as a Java file.

If we change the name of the module-info.java file to something else, the compiler will interpret the file as a normal file rather than as a module descriptor. In this case, the module system won’t be able to recognize the module anymore.

Note

In order to move the source code into a module, a module descriptor is mandatory. Otherwise, the source code won’t be part of a module.

Let’s describe some cases to see what we can put in a module-info.java file and what we can’t. Module-info.java can’t contain anything besides the module definition. The Java compiler can recognize syntax errors in the module declaration.

If the module declaration isn’t in the module-info.java file, the following error message will be displayed by the Java compiler, and the compilation will fail:

Error: module declarations should be in a file named module-info.java

A Java class can’t be put into the module-info.java file instead of a module. If you try to do so, the compilation will also fail:

error: cannot access module-info
  bad source file: srccom.apress.moduleAmodule-info.java
    file does not contain module declaration
    Please remove or make sure it appears in the correct subdirectory of the sourcepath.

Besides that, attempting to write two module declarations in a single module-info.java file will also result in a compilation error. As we already observed in the examples presented earlier, the Java compiler gives concrete indications about the cause and the location of the errors. In this way, we can go directly to the line of code that generates the issues and provide a fix for it.

The module-info.java module descriptor is compiled together with the source code. As a result, .class files, including a module-info.class file, are generated. All these compiled files can be packaged as a modular JAR file (we cover modular JAR files later in this chapter). The compiler treats module-info.java like any other Java file and translates it into a module-info.class file that we can put in a JAR file. The result is a modular JAR.

Note

The name of file module-info.java was chosen by the JCP team after the already existing name of the package-info.java file. The compiler can make use of the module-info.java file even if it contains an illegal Java identifier (the dash) in its name definition.

Platform modules consist of a module-info.java file by default. If we create our own module, in most cases we have to create and write the content of the module-info.java file on our own. But there are two cases when we don’t write a module-info.java file on our own:

  • When we put a JAR file on the module path, a module-info.java file will be automatically generated.

  • When we automatically generate a module-info.java file for a specific JAR file using the JDeps tool and the option --generate-module-info.

Don’t worry if notions like module path and JDeps are unfamiliar to you. You’ll find out later in this book what they are.

Module Name

Specifying a name for the module is mandatory. Two modules within the same code base can’t have the same name. It’s good practice to name our modules the same way we name packages: by using the domain names in reverse order. The name of the module could therefore be a prefix of the names of its exported packages, but we can name our modules how we want because we don’t have any constraints regarding the format of the module name. Nevertheless, the name of the module complies with the general rules of identifiers in Java. A module can have the same name as a Java class or an interface, because the names of the modules have their own namespace.

Note

There is an exception to the rule: when compiling multiple modules at the same time, it’s mandatory that the module name has the same name as the directory where the module descriptor module-info.java is located.

Inside a module declaration we can have a total of five types of clauses, discussed next.

Five Types of Clauses

A module declaration can consist of up to five types of clauses:

  • requires clauses specify the module that’s required by the current module.

  • exports clauses specify the packages that are exported by the current module

  • provides clauses specify the service implementations that the current module provides

  • uses clauses specify the services that the current module consumes

  • opens clauses specify the packages that the current module opens for deep reflection

Table 4-1 describes the syntax of these five clauses.

Table 4-1. The Five Clauses from a Module Descriptor

Directive Keyword

Description

requires <module_name>

Expresses which other modules the current module depends on.

exports <package_name>

(to <module_name>)

Expresses which packages from the current module are exported outside the module. The optional to clause lists the modules to which the packages are exported.

opens <package_name>

Makes the <package_name> available for deep reflection at runtime.

provides <service_name> with <service_name_implementation>

Specifies that the current module provide the implementation of <service_name> with <service_name_implementation>.

uses <service_type>

Specifies that the current module consumes instances of <service_type>.

This chapter covers only the first three clauses: requires, exports, and opens. The last two, provides and uses, are covered in Chapter 6 because they’re related to services.

Let’s continue by exploring in detail the most common clauses typically used inside a module declaration: the requires clause and the exports clause.

The requires Clause

The requires clause is used inside the module declaration (module-info.java) to express the module that the actual module needs upon in order to fulfill its dependencies. It’s used to express the module’s dependencies.

Figure 4-2 shows the syntax of the requires clause.

A431534_1_En_4_Fig2_HTML.gif
Figure 4-2. Syntax of the requires clause

The syntax is simple and concise. The requires directive specifies the name of the module it depends upon, followed by a semicolon. Inside the curly brackets of the module declaration we can put one or more requires clauses, each of them followed by the name of the module.

In the following example, the module com.apress.moduleA requires two modules, module com.apress.moduleB and module com.apress.moduleC:

module com.apress.moduleA {
        requires com.apress.moduleB;
        requires com.apress.moduleC;
}

In this example, two dependencies are expressed using the requires clauses. Module com.apress.moduleA has a dependency on module com.apress.moduleB and also has a dependency on module com.apress.moduleC. In this case, we say that module com.apress.moduleA requires (or reads) module com.apress.moduleB and requires (or reads) module com.apress.moduleC.

Figure 4-3 shows a module graph that illustrates these dependencies.

A431534_1_En_4_Fig3_HTML.gif
Figure 4-3. Module graph expressing dependencies between the three modules

In the module graph we have an arrow from module com.apress.moduleA to module com.apress.moduleB and also an arrow to module com.apress.moduleC. There is no arrow between module com.apress.moduleB and module com.apress.moduleC because those two modules don’t have dependencies between them. The direction of the arrow is straightforward: from com.apress.moduleA to com.apress.moduleB, because module com.apress.moduleA reads the module com.apress.moduleB and not inversely.

What does it mean that module com.apress.moduleA has a dependency on module com.apress.moduleB and on module com.apress.moduleC? It means that module com.apress.moduleA uses types that are part of module com.apress.moduleB and of module com.apress.moduleC. Because module com.apress.moduleA uses types from those modules, it has dependencies on them that must be explicit declared in the module descriptor module-info.java. In this way, the Java compiler knows at compile-time what the dependencies of a module are and doesn’t allow the compilation if a single dependency is not fulfilled.

Note

Compared to the class path, the situation when a module dependency isn’t fulfilled is detected right at compile-time using the Java 9 Module System. It would have not been allowed for module com.apress.moduleB to also read module com.apress.moduleA at the same time at compile-time. We would then have had a circular dependency, which is in JDK 9 forbidden by the Java compiler.

If we try to run module com.apress.moduleA, a resolution is first performed. A resolution represents a process that searches and discovers the modules required by a module. All the modules found on the host system are being searched, and the modules found are searched again for dependencies. This process continues and runs until every required module has been covered and until every dependency of every required module has been solved. In our case, the resolution is simple, because module com.apress.moduleA requires only two modules: module com.apress.moduleB and module com.apress.moduleC. We suppose that these last two modules don’t have any dependencies upon other modules. In this case, the resolution process is successfully finished after all three modules are added to the module graph. For instance, if module com.apress.moduleB has had other dependencies, these would have been resolved and also added to the module graph. The result of the resolution process contains the entire data required for compiling and running the root module, com.apress.moduleA.

Every module implicitly requires java.base, as we already know. Mentioning requires java.base in the module descriptor is unnecessary because module java.base is by default required by every module. The module java.base will always be located right at the bottom of the module graph because every module depends upon it. Figure 4-4 shows the previous module graph with module java.base included at the bottom.

A431534_1_En_4_Fig4_HTML.gif
Figure 4-4. Module graph expressing dependencies between modules, including module java.base

If a module descriptor doesn’t contain any requires clauses, the module doesn’t have any dependency on any module except for module java.base. Module java.base doesn’t have any requires directives because it doesn’t depend upon any other module.

Until now, we’ve looked at only the positive cases. Let’s also explore some cases when something goes wrong and the compilation fails. The compilation will fail if the module used in the requires clause isn’t found. The following module declaration states that it requires module com.apress.moduleB, but if this module hasn’t been defined, compiling com.apress.moduleA will result in an error, because its dependencies can’t be fulfilled:

module com.apress.moduleA {
        requires com.apress.moduleB;
}

We state that a module isn’t defined if it doesn’t have a module-info.java file or if its module-info.java file doesn’t contain the right name of the module. In the previous example, the output of the compilation of module com.apress.moduleA results in an error:

error: module not found: com.apress.moduleB

We get the same results if we have a qualified export to a module that isn’t found.

Loops in module declarations aren’t allowed, as in the following example:

module com.apress.moduleA {
        requires com.apress.moduleA;
}

This module declaration has a cyclic dependence and results in a compilation error:

error: cyclic dependence involving com.apress.moduleA requires com.apress.moduleA
Note

Cyclic dependencies aren’t allowed by the module system at compile-time.

Listing 4-1 shows an example of a simple circular dependency between three modules.

Listing 4-1. Defining Three Module Descriptors for Three Distinct Modules
// module-info.java (module com.apress.moduleA)
module com.apress.moduleA {
        requires com.apress.moduleB;
}


// module-info.java (module com.apress.moduleB)
module com.apress.moduleB {
        requires com.apress.moduleC;
}


// module-info.java (module com.apress.moduleC)
module com.apress.moduleC {
        requires com.apress.moduleA;
}

A circular dependency is present because of the following conditions:

  • module com.apress.moduleA depends on module com.apress.moduleB.

  • module com.apress.moduleB depends on module com.apress.moduleC.

  • module com.apress.moduleC depends on module com.apress.moduleA.

The compilation of these three modules fails because it results in the same cyclic dependence error as in the previous example.

Every requires statement must contain only one module name. It can’t enumerate two module names using a comma in a single requires statement. In this case, we’ll get an error during compilation. It’s also forbidden to duplicate two requires statements in the same module declaration. The compilation error message would then be as follows:

error: duplicate requires: <module_name>

What happens when a module depends on other module but doesn’t declare this dependency inside its module descriptor? In this next example, the descriptor for module com.apress.moduleA reflects that it doesn’t require any other module. Listing 4-2 shows the module descriptor of this module:

Listing 4-2. The Module Descriptor of module com.apress.moduleA
// module-info.java
module com.apress.moduleA {


}

In Listing 4-3, module com.apress.moduleA contains a class called Main that imports and makes use of types from module com.apress.moduleB.

Listing 4-3. The Main Class of module com.apress.moduleA
// Main.java (module com.apress.moduleA)
package com.apress.moduleA;
import com.apress.moduleB.*;


public class Main {
        public static void main(String[] args) {
                Employee employee = new Employee("John", "Albert");
                System.out.println("First name is : " + employee.getFirstName());
                System.out.println("Last name is : " + employee.getLastName());
        }
}

Listing 4-4 defines the module com.apress.moduleB, which has an empty module declaration.

Listing 4-4. The Module Descriptor of module com.apress.moduleB
// module-info.java
module com.apress.moduleB {


}

Listing 4-5 defines a POJO class as part of the com.apress.moduleB module.

Listing 4-5. Class Employee from module com.apress.moduleB
// Employee.java (module com.apress.moduleB)
package com.apress.moduleB;
public class Employee {


        private String firstName;
        private String lastName;


        public Employee() {
        }


        public Employee(String firstName, String lastName) {
                this.firstName = firstName;
                this.lastName = lastName;
        }


        public String getFirstName() {
                return firstName;
        }


        public String getLastName() {
                return lastName;
        }
}

We compile all the .java files from both modules at the same time using the following command:

javac -d output --module-source-path src $(find  . -name "*.java")
Note

This compilation is done using Cygwin in Windows. Cygwin is a Unix-like command-line interface that runs in Windows. Throughout this book all the operations are performed using Cygwin.

For compilation we use the --module-source-path command-line option in order to indicate to javac the location of the source code of the modules. In our example, the --module-source-path src option defines that the subdirectories of the src directory comprise the code for various modules.

The compilation fails, and we’re informed that the package com.apress.moduleB doesn’t exist:

.srccom.apress.moduleAcomapressmoduleAMain.java:3: error: package com.apress.moduleB does not exist
import com.apress.moduleB.*;
^
.srccom.apress.moduleAcomapressmoduleAMain.java:8: error: cannot find symbol
                Employee employee = new Employee("John", "Albert");
                ^
  symbol:   class Employee
  location: class Main
.srccom.apress.moduleAcomapressmoduleAMain.java:8: error: cannot find symbol
                Employee employee = new Employee("John", "Albert");
                                        ^
  symbol:   class Employee
  location: class Main
3 errors

Module com.apress.moduleA has an empty module declaration. It doesn’t require any other module, so it can’t access types from other modules according to the strong encapsulation mechanism introduced in Jigsaw. This is why attempting to access types from the module com.apress.moduleB inside com.apress.moduleA results in a compilation error.

Note

You can find the source code for this example in the directory /ch04/requiresClause.

Let’s edit module-info.java of the module com.apress.moduleA and add the dependency to module com.apress.moduleB. Listing 4-6 shows its new module descriptor.

Listing 4-6. The Module Descriptor of module com.apress.moduleA
// module-info.java
module com.apress.moduleA {
        requires com.apress.moduleB;
}

Now we try to compile the source code again using the same options. Unfortunately, exactly the same compilation error as the one that we previously had. That’s because in Java 9 it’s not enough to specify that a module requires another module in order to access types from that module.

Additionally, the second module must export some of its types in order to make them accessible to the modules that depend upon it. In our case, for the compilation to successfully work, we must modify module-info.java of module com.apress.moduleB and specify that it exports all the types from package com.apress.moduleB. Listing 4-7 shows the new definition of its module-info.java file.

Listing 4-7. The Module Descriptor of Module com.apress.moduleB
// module-info.java
module com.apress.moduleB {
        exports com.apress.moduleB;
}

To recap, we’ve learned up to now how to define dependencies upon other modules using the requires clause. The property of a module to specify the modules that it requires represents the base of reliable configuration. Until now we’ve used only the simple form of the requires clause. Hence, the requires clause can also include the static keyword as well as the transitive keyword. We explain the requires transitive clause later in the “Accessibility” section.

The requires static myModule clause indicates that the module myModule should be present only at compile-time. At runtime, its presence isn’t mandatory. In this way, we must have a compile-time dependency, but no runtime dependency.

Let’s find out how to make a module express that it makes its packages available for other modules that depend upon it. The next section explains how the exports clause can be used inside the module declaration.

The exports Clause

The exports clause has the role of exporting a package at compile-time as well as at runtime. It allows a module to specify which packages it exports. Only an exported package can be available to other modules, provided that the other conditions regarding reliable configuration are met. The reverse is also true. A package that isn’t exported isn’t available for any other modules.

Note

A module doesn’t export any package by default. This means that by default, no package from the current module is available to other modules for access.

The exports clause is specified in the module descriptor by using the keyword exports followed by the package name. It’s forbidden to separate packages or modules using a comma. For each package, a separate exports clause must exist.

Figure 4-5 shows the syntax of the exports clause.

A431534_1_En_4_Fig5_HTML.gif
Figure 4-5. Syntax of the exports clause

Similar to the requires clause, the exports clause has some constraints. For example, duplicating the exports statements inside the module declaration isn’t allowed and results in a compilation error:

error: duplicate export: <module_name>

We suppose that module com.apress.moduleB wants to make two of its packages, com.apress.moduleB.packageB1 and com.apress.moduleB.packageB2, available to other modules that require it. In this case we say that module com.apress.moduleB exports those packages. Listing 4-8 illustrates this in its module declaration.

Listing 4-8. The module-info.java of module com.apress.moduleB
module com.apress.moduleB {
        exports com.apress.moduleB.packageB1;
        exports com.apress.moduleB.packageB2;
}

Listing 4-9 states that module com.apress.moduleC also exports a package called com.apress.moduleC.packageC1.

Listing 4-9. The Module Descriptor of module com.apress.moduleC
module com.apress.moduleC {
        exports com.apress.moduleC.packageC1;
}

In the following module graph, module com.apress.moduleB has two packages that are both exported. Module com.apress.moduleC also consists of two packages, but only one is exported according to its module definition. Package com.apress.moduleC.packageC2 isn’t exported and therefore will never be accessible outside the module com.apress.moduleC. Any other module attempting to make use of package com.apress.moduleC.packageC2 will not only fail, it will also not be compiled successfully.

Figure 4-6 shows a new enhanced type of the module graph where we have also inserted the packages that the module contains. The packages com.apress.moduleB.packageB1, com.apress.moduleB.packageB2 and com.apress.moduleC.packageC1 are exported, and the com.apress.moduleC.packageC2 package is not exported.

A431534_1_En_4_Fig6_HTML.gif
Figure 4-6. Module graph showing which packages are exported and which aren’t

Module com.apress.moduleA can successfully access types in both packages packageB1 and packagesB2 for the following reasons:

  • It reads module com.apress.moduleB.

  • The packages packageB1 and packageB2 are being exported by module com.apress.moduleB.

However, if a new package were added into module com.apress.moduleB, it would not be accessible to module com.apress.moduleA unless it were declared as exported in the module description of module com.apress.moduleB. Module com.apress.moduleA can access types from com.apress.moduleC, but only from package packageC1 because this is the only package that is being exported by module com.apress.moduleC. Package packageC2 is not being exported and therefore can’t be accessed by module com.apress.moduleA.

We’ve learned so far how to set up a module declaration file and how to use the requires and exports clauses. Listing 4-10 presents a simple module with both requires and exports clauses.

Listing 4-10. The Module Descriptor of module com.apress.moduleA Using Both requires and exports Clauses
module com.apress.moduleA {
    requires com.apress.moduleB;
    exports com.apress.moduleA.packageP1;
}

In this example, we defined a module called com.apress.moduleA that depends upon another module called com.apress.moduleB and also exports the package called com.apress.moduleA.packageP1.

The opens Clause

Until now we’ve seen how we can achieve strong encapsulation using the exports directive. But what happens if we need to access some types using reflection?

Note

The exports clause just shown doesn’t allow its non-public types to become accessible by using deep reflection.

There are two different situations for using reflection in Java 9:

  • Code in the unnamed module (the class path) can access code in any named modules using reflection. This is possible due to a flag called --illegal-access that’s set by default and that was added by the JCP team in order to ease migration. Many frameworks such as Hibernate and JPA need reflective access to code in named modules. These frameworks typically reside on the class path. Chapter 8 discusses the --illegal-access flag in more detail.

  • Code in a named module can’t access code in any named modules using reflection.

This is where the new opens clause and the --add-opens command-line option come in play.

In order to solve this problem, a new directive called opens was introduced. Its role is to provide reflective access to the types passed as parameter.

Note

Using opens to export a package that has some internal implementation isn’t recommended. Doing so will expose the internal implementation and break the strong encapsulation rules.

Figure 4-7 illustrates the syntax of the opens clause.

A431534_1_En_4_Fig7_HTML.gif
Figure 4-7. The syntax of the opens clause

The opens clause is another clause that can exist in a module descriptor, besides the requires and exports clauses discussed earlier in this chapter. The opens clause is used inside the module declaration to define the packages that are available for deep reflection at runtime for all modules. Therefore, both the public and the private types of the package are accessible using deep reflection by code in other modules.

Note

Packages inside a module are by default available for deep reflection only by code in any unnamed module.

The opens directive can also be qualified by specifying a list of target named modules, as illustrated in Figure 4-8.

A431534_1_En_4_Fig8_HTML.gif
Figure 4-8. The syntax of the qualified opens clause

There are some characteristics of the opens clauses you should know. First, it’s important to know that it is possible to use both exports and opens directives for the same package. In this case, the package is exported for access at compile-time and runtime and also available for deep reflection at runtime. As a result, its public types can be accessed at compile-time and at runtime, and both its public and private types can be accessed at runtime using reflection. Second, the opens directive can’t be used inside an open module. Open modules are covered later in this chapter.

Note

The opens directive can’t use wildcards and cannot contain more than one package.

Other Clauses

The other two directives that can be used inside a module descriptor are the uses and provides clauses. These clauses are used to define (provides) and consume (uses) services and will be described in Chapter 6 – Services.

Note

None of the five directives already mentioned (requires, exports, opens, uses, and provides) are mandatory inside a module declaration. There is no restriction in selecting which directive to use and which not to use. We can create our own module and use any combination of the five directives.

We’ve now seen the basics of the module declaration. It’s time to learn how to compile and run modular applications in JDK 9.

Compiling and Running Modules

The compilation of a modular Java 9 application is different than in Java 8 or 7. All the examples of compiling and running modules are expressed in this book using the command-line running a Linux-like environment. However, this is an unusual practice in day-to-day work. Build tools like Maven or Gradle are far more productive, suitable, and easy to use for compling and running Java applications. Chapter 12 covers the integration of Java 9 with Maven and shows how to use it to build, package, compile, and run Java 9 modular applications.

Compile a Single Module

Here’s a very simple example of compiling a single module using JDK 9. Suppose we have a module com.apress.moduleA that has no dependencies. It contains a Main.java file inside its package com.apress.moduleA. The structure of the folder is as follows:

src/com.apress.moduleA/
                 module-info.java
                 com/
                     apress/
                           moduleA/
                                  Main.java

Listing 4-11 shows the content of class Main. It prints a message on the console.

Listing 4-11. The Main class from the module com.apress.moduleA
// Main.java
package com.apress.moduleA;
public class Main {
        public static void main(String[] args) {
                System.out.println(“Here is Java 9!”);
        }
}

Listing 4-12 shows the module-info.java file of module com.apress.moduleA. It doesn’t define any clause.

Listing 4-12. The Module Descriptor for moduleA
// module-info.java
module com.apress.moduleA {


}

First we create the destination directory where the compiler outputs to using the mkdir command:

mkdir –p outputDir

Then we use the Java compiler to compile the Main.java file and the module-info.java file. The compilation will create a .class file for each .java file:

$ javac –d outputDir/com.apress.moduleA src/com.apress.moduleA/module-info.java src/com.apress.moduleA/com/apress/moduleA/Main.java

The javac command gets the files that is has to compile. The -d option specifies the directory where the compiler outputs to. In this case, it will output to the directory outputDir/com.apress.moduleA. The last two parameters represent the path to the files that we want to compile, module-info.java and Main.java.

The corresponding class files are generated during the compilation in the directory passed as parameter to the option -d. The compilation of a single module is done very much the same as in Java 7 or 8 without necessarily needing to use other compiler flags than the ones used in the older versions of Java. Compared to Java 8, the single distinction here is that module-info.java was also compiled.

Note

The module declaration from the module-info.java file is compiled along with the rest of the source code, and a module-info.class file is generated.

Run an Application Containing a Single Module

To run the previously compiled classes, we use the Java launcher with the following command:

$ java --module-path outputDir --module com.apress.moduleA/com.apress.moduleA.Main

As a result, the string “Here is Java9!” is printed at the console. We recognize in the previous listing that the java command uses new flags for handling modules. The --module-path option, introduced in Java 9, gets as parameter a directory or a list of directories containing the location of the already compiled files. In our case, these are in the outputDir directory. The module path is used in order for the compiler to be able to locate the modules at runtime.

The differences between the module path and the class path are listed later in this chapter, in the section “The Module Path.”

Note

We exploded the files on the file system as .class files. Another possibility would have been to package them as modular JARs. Later in this chapter we show how.

The second option used by the Java launcher is the command-line option --module. It’s used to specify the main class and the main module by taking a parameter in form of <module_name> / <main_class>. The location of the main class is mandatory information that the java command needs to be aware of.

The Java launcher will load the root module com.apress.moduleA, resolve all its dependencies and transitive dependencies by running the resolution process, and finally run its Main class, which was passed to the option --module. At the end, the message is printed in the console.

Congratulations! You just learned how to successfully compile and run your first module in Java 9.

If we had tried to run the java command without the --module-path option, like this

$ java –m com.apress.moduleA/com.apress.moduleA.Main

the following error message would have been displayed:

Error occurred during initialization of VM
java.lang.module.ResolutionException: Module com.apress.moduleA not found
        at java.lang.module.Resolver.fail(java.base@9-ea/Resolver.java:796)
        at java.lang.module.Resolver.resolveRequires(java.base@9-ea/Configuration.java:370)
        at java.lang.module.Configuration.resolveRequiresAndUses(java.base@9-ea/ModuleDescriptor.java:2081(
        at jdk.internal.module.ModuleBootstrap.boot(java.base@9-ea/ModuleBootstrap.java:263)
        at java.lang.System.initPhase2(java.base@9-ea/System.java:1925)

What does this error mean? Module com.apress.moduleA isn’t found because we didn’t inform the Java launcher about the location of the compiled modules. The directory where the compiled modules are located has to be specified using the --module-path option. The Java launcher can’t find the modules unless we explicitly specify their location.

Compile Multiple Modules

Until now, we’ve compiled and executed a single module. But for the compilation of two or more modules, some extra compiler flags have been introduced in Java 9. In the following example you’ll learn how to compile multiple modules at the same time.

Suppose we have a total of three modules: module com.apress.moduleA, module com.apress.moduleB, and module com.apress.moduleC. The structure of the folders for the three modules is like this:

src/com.apress.moduleA/
                             module-info.java
                             com/
                                   apress/
                                          moduleA/
                                                      Main.java
    com.apress.moduleB/
                             module-info.java
                             com/        
                                   apress/
                                          moduleB/
                                                      ClassB1.java
                                                      ClassB2.java
    com.apress.moduleC/
                             module-info.java
                             com/
                                   apress/
                                          moduleC/
                                                      ClassC1.java
                                                      ClassC2.java

Further, suppose that for each module the module-info.java doesn’t contain any clause. Listing 4-13 shows the javac command used to compile only the module com.apress.moduleA.

Listing 4-13. Compile module com.apress.moduleA Using the --module-source-path Flag
$ javac –d outputDir --module-source-path src src/com.apress.moduleA/module-info.java src/com.apress.moduleA/com/apress/moduleA/Main.java

The compilation of multiple modules at the same time is done using the new --module-source-path command-line option introduced in Java 9. It’s used to tell javac about the sources in the directory. In this case we point the --module-source-path to the src directory and output our compiled modules into the outputDir directory.

The compilation generates the following classes inside the outputDir directory:

outputDir/com.apress.moduleA/
                                    com/
                                        apress/
                                               moduleA/
                                                       Main.class
                                    module-info.class

The other two modules, com.apress.moduleB and com.apress.moduleC, haven’t been generated because we didn’t specify them in the javac command. We compiled only the module com.apress.moduleA.

Let’s specify that module com.apress.moduleA has a dependency on module com.apress.moduleB and on com.apress.moduleC. Listing 4-14 shows the module-info.java file of module com.apress.moduleA where we define the dependency using the requires clause.

Listing 4-14. The Module Descriptor of module com.apress.moduleA
// module-info.java
module com.apress.moduleA {
        requires com.apress.moduleB;
        requires com.apress.moduleC;
}

If we run the javac command from Listing 4-13, we get the following structure of the outputDir directory:

outputDir/com.apress.moduleA/
                            com/
                                        apress/
                                                   moduleA/
                                                        Main.class
                                    module-info.class
          com.apress.moduleB/
                                    module-info.class
          com.apress.moduleC/
                                    module-info.class

In the javac command, we specified to compile only the Main.java and the module-info.java files from module com.apress.moduleA. But due to the fact that module com.apress.moduleA requires both modules com.apress.moduleB and com.apress.moduleC, the module descriptors from com.apress.moduleB and com.apress.moduleC have also been compiled to class files.

We compile all the classes that end with .java using the following command:

$ javac -d outputDir --module-source-path src $(find . -name "*.java")

The --module-source-path option specifies the location of non-compiled files. We search for all the files that end with the extension .java, including the module descriptor, of course. All the files ending with .java from all the modules have been compiled to .class files. The structure of outputDir is shown in the following code. Both Java classes and the module-info.java files have been compiled to .class files:

outputDir/com.apress.moduleA/
                                    com/
                                          apress/
                                                 moduleA/
                                                             Main.class
                                    module-info.class
            com.apress.moduleB/
                                    com/
                                          apress/
                                                   moduleB/
                                                             ClassB1.class
                                                             ClassB2.class
                                    module-info.class
            com.apress.moduleC/
                                    com/
                                         apress/
                                                moduleC/
                                                                ClassC1.class
                                                                ClassC2.class
                                     module-info.class

Run an Application Containing Multiple Modules

To run the previously compiled classes, we use the Java launcher with the following command:

$ java --module-path outputDir --module com.apress.moduleA/com.apress.moduleA.Main

This is the same java command used in the previous example, when we ran an application consisting of only one module. But now we have three modules inside the application that we want to run, so why do we specify a single module?

The --module option needs to get only the name of the root module. It doesn’t need to get all the modules. By getting the root module, it starts a resolution process and finds the other modules according to the information present in the module descriptor.

Note

The --module command-line option gets as parameter only the name of the Main class from the root module.

The resolution process starts with module com.apress.moduleA and then finds the modules com.apress.moduleB and com.apress.moduleC. After it finishes, the Main class of the root module is executed.

In this section, we’ve learned how to compile and run multiple modules using Java 9. Now let’s see an example where compilation fails due to the broken accessibility rules. Here, we import ClassB1 and ClassB2 into Main.java. Listing 4-15 shows the Main class of com.apress.moduleA.

Listing 4-15. The Main Class
// Main.java
package com.apress.moduleA;
import com.apress.moduleB.ClassB1;
import com.apress.moduleC.ClassC1;
public class Main {
        public static void main(String[] args) {
                System.out.println("Here is Java 9!");
        }
}

We know from the previous example that module com.apress.moduleA requires modules com.apress.moduleB and com.apress.moduleC. By compiling all the Java files

$ javac -d outputDir --module-source-path src $(find . -name "*.java")

the following error is thrown:

Main.java:3: error: ClassB1 is not visible because package com.apress.moduleB is not visible
import com.apress.moduleB.ClassB1;


Main.java:4: error: ClassC1 is not visible because package com.apress.moduleC is not visible
import com.apress.moduleC.ClassC1;


2 errors

The compilation fails as a result of strong encapsulation. The classes ClassB1 and ClassC1 can’t be imported into the Main class. Even if module com.apress.moduleA requires the other two modules, it can’t access types from them because those types aren’t exported by these modules. The public access modifier used to define the classes ClassB1 and ClassC1 isn’t able to enforce accessibility any more in Java 9.

In order to make the classes ClassB1 and ClassC1 available to the Main class, we must explicitly specify the following two things:

  • In the module descriptor of module com.apress.moduleB, we specify that the package containing the ClassB1 is exported.

  • In the module descriptor of module com.apress.moduleC, we specify that the packages containing the ClassC1 is exported.

By doing this, all classes from the exported package from module com.apress.moduleB and from the exported package from module com.apress.moduleC will be accessible in this way to module com.apress.moduleA, and the compilation will succeed without any error.

Private vs. Public Methods

We extend our example to be able to call a method from a different module. A private method won’t be able to be accessed from other module even if the module is read and the corresponding types are exported.

Note

It’s important to remember that a private method is always package private.

Listing 4-16 shows the class ClassB1 from package com.apress.moduleB which has a new private static method.

Listing 4-16. Class ClassB1 Contains a Private Method
// ClassB1.java
package com.apress.moduleB;
public class ClassB1 {
        private static String getInfoForClassB1() {
                return "ClassB1 from ModuleB";
        }
}

Listing 4-17 shows how the Main class from module com.apress.moduleA calls the static method from ClassB1.

Listing 4-17. Class Main
// Main.java
package com.apress.moduleA;
import com.apress.moduleB.ClassB1;
import com.apress.moduleC.ClassC1;


public class Main {
        public static void main(String[] args) {
                System.out.println(“Here is Java 9!”);
                System.out.println(ClassB1.getInfoForClassB1());
        }
}

The compilation fails with the following error message:

Main.java:10: error: getInfoForClassB1() has private access in ClassB1

By setting the getInfoForClassB1() method to public in ClassB1.java, the compilation will succeed. Running the application

$ java --module-path outputDir –m com.apress.moduleA/com.apress.moduleA.Main

results in the following being printed:

Here is Java 9!
ClassB1 from ModuleB

In this example, we showed that having an access identifier as private makes the type inaccessible, as in the previous versions of Java.

We’ve learned how to compile and run modular applications in Java 9. In the next section we discover the new modular JAR files introduced in Jigsaw.

Modular JARs

The modular JARs were introduced in JDK 9. They represent module artifacts that contain compiled module definitions. A modular JAR resembles a regular JAR and contains .class files and also a module-info.class file. The difference between a modular JAR and a regular JAR consists only of the module-info.class that a modular JAR additionally has.

The module-info.class file lies in a modular JAR at the top level of its directory. Every modular JAR must contain such a file. If it doesn’t, it’s just a regular JAR file, not a modular one.

Note

The module-info.class is located at the root of the directory, inside the modular JAR file. It’s not located inside the packages.

A modular JAR can be used on the module path as well as on the class path. When used on the class path, the module-info.class file isn’t taken into consideration.

Note

A modular JAR file is compatible with older versions of the JDK. It works as a regular JAR file on the class path for all Java versions prior to Java 9.

Because the module descriptor is compiled in a module-info.class file in Java 9, a modular JAR can work as a module by being placed on the module path. If we place the modular JAR on the class path, the module-info.class file will be simply ignored. But all the other files of the modular JAR, except the module-info.class file, will be taken into consideration. In this way, the modular JAR will act as a normal JAR file.

It’s a great advantage to have a JAR file that can be used either on the class path or on the module path. We could compile a library and be able to use it on the class path for JDK 8 (or earlier) or compile it with JDK 9+ and use it on the module path.

Note

A modular JAR can incorporate only one module. It can’t be composed of more than one module.

The alternative to modular JARs would be to explode the compiled modules on the file system. Both solutions work, but it’s definitely better and more suitable to have a single modular JAR instead of a group of files.

Let’s imagine a situation where the module path contains two modular JAR files that are located in the same directory. If the module that’s part of the first modular JAR file has the same name as the module that’s part of the second modular JAR file, there will be an error at compile-time.

In order to create a modular JAR, we can use the jar tool, which was enhanced in Java 9. We’ll learn more about it later in this section. Next, we talk about the structure of a modular JAR file.

Structure of a Modular JAR

The structure of a modular JAR file is similar to the one of a normal JAR file, except a module-info.class file is present. Here’s an example of a modular JAR file:

META-INF/
META-INF/MANIFEST.MF
module-info.class
com/apress/moduleA/Main.class
com/apress/moduleB/ClassB1.class
com/apress/moduleB/ClassB2.class
...

There is also a MANIFEST.MF file located in the META-INF directory. The module-info.class file is located in the root directory. All our compiled .class files are present in the modular JAR.

Up to now, we’ve learned about modular JAR files and about their structure. But how can we create one? We’ll find the answer in the next section when we talk about packaging and present the jar tool used to create modular JAR files.

Packaging

Java 9 allows a module to be packaged, besides in the already known normal JAR file, in a modular JAR file, multi-release JAR file, JMOD file, or JIMAGE file. The JMOD files and multi-release JAR files are discussed in Chapter 10. It’s important to mention that the Java Platform Module System doesn’t force in any way a module to be packaged as a modular JAR.

Let’s start by discussing how we can package a module in a modular JAR. You can do this using the jar tool that can be found under the JDK_HOMEin directory.

Package as a Modular JAR Using the jar Tool

The jar tool has been enhanced to support the newly introduced concept of modules. Table 4-2 displays the new options that have been added in JDK 9 to the jar tool, according to the official JDK 9 API specification.

Table 4-2. New Options Added to the JAR tool in JDK 9

Short Format

Long Format

Description

-d

--describe-module

Prints the module descriptor

 

--module-version=VERSION

Specifies the module version when creating a modular JAR or when updating a non-modular JAR

 

--hash-modules=PATTERN

Computes and records the hashes of modules matched by the given pattern and that depend directly or indirectly upon a modular JAR being created or upon a non-modular JAR being updated

-p

--module-path

Specifies the location of module dependence for generating the hash

 

--release VERSION

Puts the files in a versioned directory of the JAR file

The options presented in Table 4-2 can be used when we create modular JARs or when we update a non-modular JAR. The jar --help command has also been enhanced in JDK 9. It contains more detailed descriptions about each command.

Using the jar tool, we create a new modular JAR file called moduleA.jar in the lib directory by packaging everything that exists in the modules directory:

$ jar --create --file lib/moduleA.jar --main-class com.apress.moduleA.Main –c modules
  • The --create option creates the modular JAR file.

  • The --file option indicates the name of the modular JAR that will be created. It also specifies the location where it will be created (in our case, the lib directory).

  • The --main-class option sets the Main class while the module is being packaged.

  • The -c option specifies the location where the compiled modules are (in our case, they’re in the modules directory, which contains the compiled class files for the module com.apress.moduleA).

Note

In order to be able to package a module as a modular JAR, the module must have been compiled before.

Adding a Module Version

When creating a modular JAR, a module version can also be added. The option --module-version can be used during packaging to add some metadata regarding the version of the module. The metadata isn’t added to the module declaration and won’t be processed at runtime. It’s important to make this distinction.

Printing the Module Descriptor

The module descriptor is printed with the jar tool’s command-line option --describe-module:

$ jar --describe-module

Or simply:

$ jar -d

By printing the module descriptor we can see what the corresponding JAR file contains. The following information is displayed:

  • The name of the module that’s contained in the JAR file

  • A list of modules that the module contained into the JAR file depends on

  • The name and package of the main class

An important change is the introduction of a long format for the specification of the options. In Java 8, we had only the short format composed of a letter. In Java 9 it’s possible to use both the short and long formats, as described in Table 4-3. However, there are situations when there’s no corresponding for the long format.

Note

The Java Archive Tool, also called the jar tool, is an archiving tool that creates a JAR file by archiving a couple of files. Java 9 improved the jar tool by adding support for modules.

The next section describes a fundamental feature in JDK 9: the introduction of the module path.

The Module Path

Project Jigsaw introduces a new concept for replacing the class path: the module path, which can represent one of the following:

  • A path to a sequence of directories that contain modules

  • A path to a modular JAR file

  • A path to a JMOD file

In contrast to the module path, the class path represents a sequence of JAR files. The module path is used by the compiler to find the modules in order to resolve them. The module path can be mixed together with the class path. In this case, the classes that are part of the modules are able to depend on anything that exists on the class path.

Every existing artifact from the module path must have a module declaration. On the module path, we can’t have artifacts that don’t have a module declaration.

Note

Even if the module path was introduced in Java 9 to replace the class path, the class path still exists and can be used standalone or in combination with the module path.

There are three types of module paths introduced in Jigsaw:

  • Application module path

  • Compilation module path

  • Upgrade module path

We already worked with the application module path and the compilation module path when we compiled and ran multiple modules. The next subsections go over these types of module paths.

Application Module Path

The application module path is used by the Java launcher to mark the directory that incorporates the application modules. It’s expressed using the new command-line option --module-path or its short-form, -p.

Figure 4-9 shows the command-line syntax of the module path flag:

A431534_1_En_4_Fig9_HTML.gif
Figure 4-9. Syntax of the --module-path command-line option

The module path flag gets a list of directories as parameters separated by a colon. There can be an unlimited number of directories listed, but for each of them there has to be a colon separating them.

Note

In the previous example, we used a colon (:) to separate directories, but a colon is used only for Linux environments. For Windows environments, we must use a semicolon (;) instead.

The modules contained in the directories that form the module path can be:

  • Packaged as modular JAR files.

  • Exploded as standalone class files.

The Java launcher can load exactly the module that it needs from the module path because it knows this information due to the configurations that exist in the module declaration.

Note

The module path allows specifying modules instead of JAR libraries as the class path.

At runtime, using the module path is possible to specify the different types of modules that we’ve built together with the project. A module can be only in one place. If it’s in more than one place, the first occurrence will be retained, and the other occurrences won’t be taken into consideration.

Remember

The module path can contain only modules.

Besides the application module path, there are two more types of module path: the compilation module path and the upgrade module path.

Compilation Module Path

The compilation module path, which contains definitions of modules in source form and is used together with javac, is specified using the new Java option --module-source-path on the command-line. It’s used during the compilation to inform the Java compiler of the location of the modules that must be searched.

Figure 4-10 illustrates the syntax of the command-line --module-source-path option:

A431534_1_En_4_Fig10_HTML.gif
Figure 4-10. Syntax of the --module-source-path command-line option

The --module-source-path flag specifies a list of Java source files to be compiled. They can be listed one after another separated by a blank space.

Note

In most situations, the list of Java files will be huge. Linux helps you in this respect. You can type $(find. –name '*.java') to get the entire list of directories having the extension .java.

Generally, we can think of the --module-source-path command-line option as the module correspondent of the --sourcepath option.

Upgrade Module Path

The upgrade module path is specified using the Java compiler option --upgrade-module-path on the command-line. According to OpenJDK, “it contains compiled definitions of modules intended to be used in place of upgradeable modules built-in to the environment.” The upgrade module path isn’t covered in this book.

In this section we’ll talk about the module resolution process.

Module Resolution

Module resolution is a process introduced in Java 9 that checks the correctness of the module path and also resolves the dependencies that exist throughout a module system. It takes place at both compile-time and runtime. The goal of the module resolution process is to end up with a minimum necessary set of resolved modules in order to be able to run the application.

Note

Modules are resolved during build and installation. They’re not resolvable during runtime.

The requires clauses from the module declaration located in the module-info.java file provide the module system with valuable information about the dependencies that have to be solved. These dependencies are nothing more than modules that our current module depends upon. In Java 9, they’re called observable modules, covered later in this chapter in the section “Types of Modules.”

After all the observable modules of our current module are found, the module system doesn’t stop. It searches further for the observable modules of the most recently found modules. This process continues until every dependence of every module is fulfilled and until the base module java.base is reached.

Note

At compile-time, during the module resolution, Jigsaw searches to see whether there are any cyclic dependencies. If it finds any, the application won’t compile.

Root Module

The root module is the module that the resolution process starts with. It’s specified in the java command using the --module option, as we’ve seen in the examples when we ran the modular application.

First, the root module is added to the group of resolved modules. Second, the module system scans the module descriptor of this module and adds all the dependencies (modules) to the group of resolved modules. The process continues, and the Java Platform Module System tries to find the dependencies on the other modules. When all the modules searched are found and the module java.base is reached, the process stops. After the resolving process is finished, we have all the necessary modules for running our software application.

In some cases, a module is on the module path but wasn’t found during the resolution process so that it can be added to the module graph. Here, we have to manually add the module to the module graph. This can be done using the --add-modules command-line option. We discuss this option in more detail in Chapter 8.

It’s important to know that the module resolution process detects any modules that could eventually be missing. If a mandatory module is missing, the module resolution process stops, and an exception is thrown.

Note

If we have an incomplete module path at compile-time, the compiler will give us a warning. Both platform modules and developer modules are searched for during the resolution process.

Another important topic in Jigsaw is accessibility, covered in the next section.

Accessibility

The accessibility rules changed fundamentally in Java 9. A type declared as public but not exported will be available only inside the module where it resides. This a major change compared to older versions of Java. Prior to Java 9, it was enough to mention that a type is public and therefore was accessible anywhere. In Java 9, declaring a type as public doesn’t imply that it will be accessible everywhere.

Simply reading a module doesn’t guarantee access to its packages. In addition, in order to be accessible, modules have to export some of their packages. Only the public types from the exported packages will be accessible from another module. By taking advantage of strong encapsulation and reliable configuration, we can explicitly define which of the module’s types are available for external access. In this way, we can very easily hide our implementation internals.

Hiding the implementation details becomes the standard in Java 9. It’s obtained by default—we don’t have to opt for it. It’s enough to omit to export a type in order to make that type strong encapsulated and invisible from outside of the module. A package can quickly benefit from the power of strong encapsulation by simply being placed inside a module. In Java 9, we’re able to decide which types should be accessible from outside by enumerating them in the module declaration. Very simple and concise.

To recap, in order for a module A to read package P from module B, two conditions have to be met simultaneously. The first is that module A should read (requires) module B. The second condition is that module B should export its package P.

Note

In Java 9, accessibility is imposed at both compile-time and at runtime. An error of type IllegalAccessError is thrown at runtime if the accessibility rules are breached. The accessibility checks are enforced in the Java Virtual Machine.

In Java 9, simply setting a public modifier to a type doesn’t mean that access is granted. In the Java versions before 9, setting an accessibility type of public to a type conferred him global accessibility, but in Java 9 three conditions have to be simultaneously met in order to make a type called T accessible outside the module:

  • The package in which type T resides has to be exported.

  • The module that needs to access type T has to read the module that contains the type T.

  • Type T must have a public identifier.

These conditions are illustrated in Table 4-3, which displays a comprehensive list of the cases when the accessibility is granted or not:

  • The “Module Is Read” column has the value Yes if the module that contains the type T is read by the module that wants to access the type T.

  • The “Package Is Exported” column has the value Yes if the module that contains the type T exports the package of type T.

  • The “Access Modifier of Type T” column represents the access modifiers for type T.

  • The “Accessible in the Other Module” column specifies Yes if the type T is accessible from the other module, and No otherwise.

Table 4-3 shows the new accessibility cases in Java 9 together with the results.

Table 4-3. Accessibility Cases in Java 9

Module Is Read

Package Is Exported

Access Modifier of Type T

Accessible in the Other Module

Yes

Yes

Public

Yes

Yes

Yes

Protected

No

Yes

Yes

(default)

No

Yes

Yes

Private

No

Yes

No

Public

No

Yes

No

Protected

No

Yes

No

(default)

No

Yes

No

Private

No

No

Yes

Public

No

No

Yes

Protected

No

No

Yes

(default)

No

No

Yes

Private

No

No

No

Public

No

No

No

Protected

No

No

No

(default)

No

No

No

Private

No

As a rule, a public component of an exported package can be accessed from outside of the module provided that the module that makes use of it reads the origin module. In contrast, a public element of a non-exported package isn’t accessible from outside of the module. It’s accessible by default to all the source code from the module where it resides, but it won’t be accessible from outside of the module.

The Java compiler throws exceptions when we attempt to access types that aren’t accessible. The most common is a ClassNotFoundException. At runtime the most common errors are IllegalAccessError or InaccessibleObjectException.

Readability vs. Implied Readability

Readability is the relation between two modules that refers to the fact that a module that reads another module can access the types from its exported packages. In this case, we say that a module reads another module.

We touched the subject of readability in previous examples when we described the requires and exports directives inside of a module declaration. To recap the concept of readability, you can go back to the “Module Declaration” section, subsection “The requires Clause.” What we haven’t covered yet is the new concept of implied readability.

Implied Readability

Implied readability refers to the situation when

  • The first module reads the second module.

  • The second module reads the third module.

  • The first module logically reads the third module as a result of the two conditions just met.

Suppose we have a module B that uses a type from a module C (B reads C). If another module A reads module B and uses types from module C, then without implied readability, module A should explicit specify that it also requires module C. By using implied readability, it’s not necessary to specify this in module A. It’s enough to specify in the module-info.java of module B that it requires transitive the types from module C. As a result, every module that reads types from module B will automatically be able to access the types from module C.

Figure 4-11 illustrates the corresponding module graph of the three modules and shows the readability relations between them.

A431534_1_En_4_Fig11_HTML.gif
Figure 4-11. Module graph showing implied readability

Listing 4-18 illustrates the module descriptor of module A, which requires module B.

Listing 4-18. Module Descriptor of Module A
// module-info.java
module A {
        requires B;
}

Listing 4-19 shows the module descriptor of module B, which requires transitive module C.

Listing 4-19. Module Descriptor of Module B
// module-info.java
module B {
        requires transitive C;
}

It’s not necessary for module A to require module C, because module B requires transitive module C. As a result, module A can automatically access types from module C.

Note

Implied readability is achieved by adding the statement requires transitive in the module declaration, followed by the name of the module that the current module depends upon.

Figure 4-12 illustrates the syntax of the requires transitive clause. It takes a module name as parameter.

A431534_1_En_4_Fig12_HTML.gif
Figure 4-12. Syntax of the requires transitive clause

Let’s look at an example of implied readability with platform modules in order to understand this better. Listing 4-20 shows the module declaration of the platform module java.desktop that defines requires transitive clauses in order to take advantage of implied readability.

Listing 4-20. The Module Descriptor of module java.desktop
// module-info.java
module java.desktop {
        requires transitive java.datatransfer;
        requires transitive java.xml;
        requires java.prefs;


        exports java.applet;
        ...
}

Listing 4-21 is an excerpt of the class DocumentHandler located in module java.desktop in the package com.sun.beans.decoder:

Listing 4-21. Class DocumentHandler from Module java.desktop
// jdk/src/java.desktop/share/classes/DocumentHandler.java

package com.sun.beans.decoder;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParserFactory;
...


public final class DocumentHandler extends DefaultHandler {

...

  public void parse(final InputSource input) {
        if ((this.acc == null) && (null != System.getSecurityManager())) {
            throw new SecurityException("AccessControlContext is not set");
        }
        AccessControlContext stack = AccessController.getContext();
        SharedSecrets.getJavaSecurityAccess().doIntersectionPrivilege(new PrivilegedAction<Void>() {
            public Void run() {
                try {
                    SAXParserFactory.newInstance().newSAXParser().parse(input, DocumentHandler.this);
                }
                catch (ParserConfigurationException exception) {
                    handleException(exception);
                }
                catch (SAXException wrapper) {
                    Exception exception = wrapper.getException();
                    if (exception == null) {
                        exception = wrapper;
                    }
                    handleException(exception);
                }
                catch (IOException exception) {
                    handleException(exception);
                }
                return null;
            }
        }, stack, this.acc);
    }
}

As you can see, the class DocumentHandler from module java.desktop uses SaxParserFactory from the java.xml module. This denotes that module java.desktop uses a type from module java.xml, so a readability relation occurs between module java.desktop and module java.xml.

If we add a dependency to the java.desktop module in our module descriptor and try to use the parse() method from the DocumentHandler, we’re attempting to access types not only from module java.desktop but also from module java.xml. In order to access the method from our own module, it’s mandatory that the module java.xml is required transitive by the module java.desktop. In this way our module can take advantage of implied readability and have access to the types from the module java.xml without explicitly requiring them. Any module that requires module java.desktop automatically requires the modules java.datatransfer and java.xml, because both are present in the module descriptor of module java.desktop. By requiring the module java.desktop, we get access to the exported packages from modules java.desktop, java.datatransfer, and java.xml.

If we omit the transitive keyword and use only the requires directive, we have the situation where our module is able to access types only from the module java.desktop, but not from java.xml.

Suppose we create a simple module called myModule that requires module java.desktop. Listing 4-22 shows the module descriptor of module myModule.

Listing 4-22. Module Descriptor of Module myModule
// module-info.java
module myModule {
        requires java.desktop;
}

Figure 4-13 shows the module graph of the module myModule and expresses the readability together with the implied readability relations between modules.

A431534_1_En_4_Fig13_HTML.gif
Figure 4-13. The module graph of myModule

We have the following situation:

  • myModule requires java.desktop (readability illustrated with a dashed line in the graph).

  • java.desktop module requires transitive module java.datatransfer and module java.xml (implied readability illustrated with a solid line).

Our module myModule gets readability to modules java.datatransfer and java.xml without needing to explicitly require them. As a result, it can use types from these two modules without having to worry about the need to specifically declare the dependencies to them.

Now that we’ve learned what implied readability is, let’s see what a qualified export means.

Qualified Exports

A module can export all its packages or a group of its packages to all the modules. The exports clause has been enhanced to specify that a module can export a group of its packages only to a set of named modules.

Listing 4-23 is an excerpt of the module descriptor from the module java.rmi.

Listing 4-23. Excerpt from Module Descriptor of module java.rmi
// module-info.java
module java.rmi {
    ...
    exports com.sun.rmi.rmid to java.base;
    exports sun.rmi.registry to
        java.management;
    exports sun.rmi.server to
        java.management,
        jdk.jconsole;
    exports sun.rmi.transport to
        java.management,
        jdk.jconsole;
}

Package com.sun.rmi.rmid is exported using a qualified export to module java.base. Therefore it’s accessible only inside the module java.base. The other modules can’t access it. Only the modules specified after the to clause will be able to access the package.

Note

A module can’t access an exported package from another module if it doesn’t read that module. This is true even if the package is exported using a qualified export.

The syntax used for defining qualified exports inside the module-info.java file is shown in Figure 4-14.

A431534_1_En_4_Fig14_HTML.gif
Figure 4-14. Qualified exports syntax
Note

A qualified export directive can define multiple modules separated by comma. In contrast, a simple export directive can define only one module.

Duplicating the names of the modules in a qualified export declaration is prohibited, as shown in Listing 4-24.

Listing 4-24. Qualified Export with a Duplicate Module’s Name
// module-info.java (com.apress.moduleA)
module com.apress.moduleA {


}

// module-info.java (com.apress.moduleB)
module com.apress.moduleB {
        exports com.apress.moduleB to com.apress.moduleA, com.apress.moduleA;
}

A compilation failure will occur in this case:

Error: duplicate export: com.apress.moduleA exports com.apress.moduleB to com.apress.moduleA, com.apress.moduleA

We’ve learned what the qualified exports directives are and how they’re different from standard exports directives. It’s a great advantage to be able to specify only the specific modules that are allowed to access the module’s data. Modules shouldn’t be obligated to expose their packages to all the existing modules.

Qualified exports are a great benefit of strong encapsulation. They’ve been extensively used during the process of modularizing the JDK and are present in a couple of module-info.java files inside the JDK.

We talked about accessibility and discussed the concepts of readability, implied readability, and qualified exports. Next let’s look at the different types of modules introduced in Jigsaw.

Types of Modules

Jigsaw defines two primary types of modules: named modules and the unnamed module. The named modules are divided into normal modules and automatic modules. The normal modules are also separated in basic modules and open modules. Figure 4-15 illustrates the classification of modules.

A431534_1_En_4_Fig15_HTML.gif
Figure 4-15. The classification of modules in the JPMS

First, we’ll talk about the named modules together with its children, and then the unnamed module.

Named Modules

The named modules comprise all the modules from the module system except the unnamed module. There are two important things that distinguish an unnamed module from a named one. First, the unnamed module lives on the class path, whereas the named modules live on the module path. Second, the unnamed module doesn’t have a name, whereas each named module has a name. A named module can be a normal module or an automatic module. Named modules are modules declared using a name in the module-info.java module descriptor file. Every module that has a declaration of form module <module_name> in its module-info.java is a named module. This is the single condition that has to be met for a module in order to be classified as a named module. Examples of named modules include all the platform modules, but our own modules can also be included in this category if they respect the unique condition just mentioned. Transforming a JAR file into a named module is possible by merely adding a module-info.class file to it.

Normal Modules

The notion of “normal” modules doesn’t officially exist. We use this term to define a named module that isn’t automatic. The main difference between a normal module and an automatic one is that a normal module has a module descriptor module-info.java, whereas an automatic module doesn’t. Additionally, a normal module is explicitly declared by developers, which declares the module’s dependencies in the module’s module descriptor. The module descriptor of an automatic module isn’t provided by developers. A normal module is declared using the keyword module followed by the name of the module. All the modules we’ve presented until now in this chapter were normal modules. A normal module doesn’t export any of its packages by default. Besides that, its exports clauses must be explicitly specified. The exports clauses export packages at compile-time as well as at runtime. A normal module comprises both basic modules and open modules.

Automatic Modules

An automatic module is a module created after placing a JAR file onto the module path. Comparing an automatic module to a normal one, two important distinctions emerge:

  • An automatic module requires by default all the existing modules from the system, which comprise all our own modules, plus all modules from the JDK image, plus all the other automatic modules.

  • An automatic module exports all its packages by default.

An automatic module can access types on the class path and is useful for third-party code especially. Automatic modules are used for migrating existing applications to Java 9. Chapter 8 talks about them in more detail.

Note

An automatic module isn’t explicit declared by us inside a module descriptor. It’s automatically created when placing a JAR file into the module path.

Basic Modules

We call every named module that isn’t an open module a basic module. However, the term “basic” module doesn’t officially exist in JDK 9. We use it to define a named module that’s neither automatic nor open. A basic module has the same set of characteristics as a normal module, except that it’s not opened for deep reflection.

Open Modules

Inside a module, packages aren’t accessible to code from another module at compile-time, even when using deep reflection. However, many third-party libraries and frameworks use reflection to access the internals of JDK at runtime. As a result, all these frameworks aren’t working in JDK 9 unless reflective access is granted. In JDK 9, reflective access is granted only by code in named modules to code from the class path. It’s not granted by default by code in named modules to code in other named modules. As a result, if the third-party libraries or frameworks lie on the class path, they have reflective access by default in the JDK. If they live on the module path, then they don’t have reflective access in the JDK. But to grant reflective access to all the packages in a module, the module should be declared as open.

An open module is defined by placing the identifier open in front of the keyword module, followed by the name of the module.

Open modules make all the packages inside of the module available for deep reflection. When we say “all the packages”, we mean both the public and private packages. We can also opt between opening an entire module for deep reflection or opening only specific packages. When choosing the latter, we don’t specify an entire module as open but only one or more packages inside the module. The keyword open can be placed near the module name or inside the module descriptor to open specific packages.

Note

The reason behind open modules is that they permit frameworks to reflect over the module’s internals, which isn’t possible using basic modules. Frameworks like Spring, JPA, and Hibernate need reflective access at runtime.

Listing 4-25 defines an open module called com.apress.myModule that requires two Spring modules, spring.tx and spring.context.

Listing 4-25. Defining an Open Module
open module com.apress.myModule {
        requires spring.tx;
        requires spring.context;
        exports com.apress.myModule.myPackage;
}

Two important facts must be stressed in regard to the previous example:

  • All types from all packages of the module com.apress.myModule are available for deep reflection at runtime.

  • At compile-time, only the public and protected types in package com.apress.myModule.myPackage are accessible.

As a result, the Spring framework can make use of the setAccessible() method to access the non-public elements of the com.apress.myModule.myPackage package.

Enabling Core Reflection Using Open Modules

With respect to the principles of strong encapsulation, there are some constraints introduced in Java 9 when calling the method setAccessible() method of the java.lang.reflect.AccessibleObject class. We can’t use the method setAccessible() to make private fields or methods from other modules accessible in our module. But there is a solution to make them accessible: by declaring the target module as an open module.

In the following example we have two modules. Class Employee from module target contains a private String field called employeeName. We want to set this field to be accessible from our second module called testReflection.

Listing 4-26 shows the declaration of module target, which exports its package.

Listing 4-26. The module-info.java of Module target
module target {
       exports target;
}

In Listing 4-27 the module testReflection reads the module target.

Listing 4-27. The module-info.java of Module testReflection
module testReflection {
       requires target;
}

Listing 4-28 shows the class Employee from module target that contains a private type called employeeName:

Listing 4-28. Definition of Class Employee
package target;

public class Employee {

    private String employeeName = null;

    public Employee(String employeeName) {
        this.employeeName = employeeName;
    }
}

The Main class creates an object of type Employee and calls its constructor, setting the value "John" to employeeName. After that, a Field object is returned that represents the field employeeName and the method setAccessible() with parameter true is called on this field in order to make it accessible throughout our testReflection module.

Listing 4-29 shows the Main class of the application.

Listing 4-29. Main Class from Package testReflection
package testReflection;

import java.lang.reflect.*;
import target.*;


public class Main {

    public static void main(String[] args) {

        Employee employee = new Employee("John");
        try {
            Field employeeField = Employee.class.getDeclaredField("employeeName");
            employeeField.setAccessible(true);
        }
        catch(NoSuchFieldException noSuchFieldException) {
        }
    }
}

We compile the code inside the two modules and specify the location of the compiled files to be in the out directory. Listing 4-30 shows the usage of the --module-source-path option to specify that all the files that have the extension .java are on the module source path.

Listing 4-30. Compile Files Using the flag --module-source-path
javac -d out --module-source-path src $(find . -name "*.java")

Listing 4-31 illustrates the java command used to run the Main class of our module. We pass the Main class to the --module option and the --module-path option points to the out directory where all the compiled class files exist.

Listing 4-31. Run the Main Class Using the --module option
$ java --module-path out --module testReflection/testReflection.Main

Unfortunately, an exception is thrown when attempting to run our application because the call of the method setAccessible() fails. Thus we can’t make the private field employeeName accessible.

Exception in thread "main" java.lang.reflect.InaccessibleObjectException: Unable to make field private java.lang.String target.Employee.employeeName accessible: module target does not "opens target" to module testReflection
        at java.base/jdk.internal.reflect.Reflection.throwInaccessibleObjectException(Reflection.java:424)
        at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:198)
        at java.base/java.lang.reflect.Field.checkCanSetAccessible(Field.java:171)
        at java.base/java.lang.reflect.Field.setAccessible(Field.java:165)
        at testReflection/testReflection.Main.main(Main.java:14)
Note

In this example, we have readability between the two modules target and testReflection. But due to the very powerful strong encapsulation mechanism introduced in Java9, calling the method setAccessible() on a private field of the other module throws an InaccessibleObjectException.

We can fix this issue very easily by defining the target module as an open module instead of a strong module. Calling the setAccessible() method on a private field of the target module will succeed. The private field employeeName is now accessible into the testReflection module.

Listing 4-32 defines the target module as an open module by specifying the keyword open.

Listing 4-32. Define the Target Module as an Open Module
open module target {
        exports target;
}
Note

You can find the source code for the first example in the folder /ch04/CoreReflectionFail and the source code for the second example in the folder /ch04/CoreReflectionSucceed.

Until now we’ve talked about the named module and its types. Next we’ll talk about the unnamed module.

The Unnamed Module

An unnamed module, as the term suggests, doesn’t have a name and isn’t declared. It comprises all the JAR files or modular JAR files from the class path. All these JAR files together form the unnamed module. The Java Platform Module System first searches for a specific type on the module path. The module path is searched before the class path. If the type isn’t found on the module path, the search is performed on the class path. If the type is found on the class path, it will be part of the so-called unnamed module. We use the singular term unnamed module instead of the plural unnamed modules because the unnamed module is unique for each class loader. There’s only one, single unnamed module for each class loader.

Note

An unnamed module is bound to a class loader. There’s a one-to-one relationship between a class loader and an unnamed module. The unnamed module reads all the named modules in the JDK image and on the module path. It also exports all of its packages.

By default, the unnamed module reads all the named modules from the system. In this way, according to the Java 9 accessibility rules, the unnamed module can access all packages from all the named modules that are being exported. The reverse isn’t true, meaning that the named modules can’t read the unnamed module. If we try to access code on the class path (in the unnamed module) from the module path, the compilation will fail. To succeed, we need to turn the code from the unnamed module into automatic modules. Therefore, we take the JAR file out of the class path and place it on the module path to become an automatic module.

Note

A named module can’t require an unnamed module.

All classes that aren’t contained into the named modules are implicitly contained in the unnamed module. All the packages contained in the unnamed module are open by default to all the modules from the module path, which makes reflective access from the module path on the class path possible.

Observable Modules

The observable modules are not a separate category of modules. This is why we didn’t include them in the classification of modules. The term observable modules is used to denominate all the modules from the system: platform modules, library modules, and our own modules. The modules from the module path are part of the observable modules too.

Summary

This chapter presented the new concept of module in Jigsaw. We learned how to define a module and described the structure of the new module-info.java file that represents the module descriptor.

Inside the module descriptor there are five types of directives that can be used: requires, exports, opens, uses, and provides. The first three were explained in detail throughout this chapter. We saw how we can define dependencies between modules using the requires directive and also how we can specify which packages a module exports using the exports directive. Further, we defined a couple of modules and illustrated the dependencies between them in a module graph.

We mentioned the differences between the exports and opens clauses. The exports clause allows compile-time and runtime access to the public types of a specific package. The use clause allows runtime access using reflection for both the public and private types of a specific package.

We compiled and ran a single module as well as multiple modules. To be able to do this, we used the new concept of module path together with the new command-line options --module-source-path and --module-path. Then we talked about the new modular JARs and described their internal structure. I described the enhancements added to the jar tool and showed how to package a modular JAR using the jar tool.

The chapter explained the three types of existing module paths: the application module path, the compilation module path, and the upgrade module path. It also talked about the module resolution process and about the new accessibility rules that were introduced in Java 9. We described topics like readability, implied readability, and qualified exports.

This chapter concluded by describing the different types of modules in Jigsaw: normal modules, open modules, named modules, unnamed modules, automatic modules, and observable modules. We stressed that as a consequence of the fact that a named module can’t read the unnamed module, code from the module path can’t access code inside the JARs placed on the class path.

In Chapter 5 you’ll learn about modular runtime images.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.16.69.199