First Steps with Java 9 and the Jigsaw Project - Part One

Original author: Florian Trossbach
  • Transfer
Good morning, Habr!

Ever since the book " Java. A New Generation of Development " we have been following the development of long-announced new features of this language, united under the general name " Project Jigsaw ". Today we offer a translation of the article of November 24, which instills enough confidence that Jigsaw will nevertheless take place in Java 9.

Eight years have passed since the inception of the Jigsaw project, whose task is to modularize the Java platform and boils down to the implementation of a common system of modules. Jigsaw is expected to first appear in Java 9. This release was previously planned for both Java 7 and Java 8. The scope of Jigsaw has also changed many times. Now there is every reason to believe that Jigsaw is almost ready, because it was given much attention in the Oracle plenary at the JavaOne 2015 conference, as well as several speeches on this topic at once . What does this mean for you? What is a Jigsaw project and how to work with it?

This is the first of two publications in which I want to make a brief introduction to the module system and demonstrate the behavior of Jigsaw using numerous code examples. In the first part we will discuss what a system of modules is, how the JDK was modularized, and also consider the behavior of the compiler and runtime in certain situations.

What is a module?

Describing a module is simple: it is a unit in the program, with each module immediately containing answers to three questions. These answers are recorded in the file that each module has.module-info.java


  • What is the name of the module?
  • What does he export?
  • What is required for this?




Simple module The

answer to the first question is simple. (Almost) every module has a name. It must comply with package naming conventions, for example , to avoid conflicts. To answer the second question, the module provides a list of all packages of this particular module, which are considered public APIs and, therefore, can be used by other modules. If the class is not an exported package, no one can access it from outside your module - even if it is public. The answer to the third question is a list of those modules on which this module depends. All public types exportedde.codecentric.mymodule




These modules are available to the dependent module. The Jigsaw team is trying to introduce the term “ read ” another module.

This is a major change in the status quo. Until Java 8, inclusive, any public type in the path to your classes was available to any other type. With the advent of Jigsaw, the Java type accessibility system changes from

  • public
  • private
  • default
  • protected


on the

  • public to anyone reading this module (exports)
  • public for some modules reading this (exports to, this will be discussed in the second part)
  • public for any other class within this module
  • private
  • default
  • protected


Modularized JDK

Module dependencies should form an acyclic graph, thus avoiding cyclic dependencies. To implement this principle, the Jigsaw team had to solve the following big problem: to break into modules the Java runtime environment, which, as reported, is full of cyclic and illogical dependencies. This graph turned out :



At the base of the graph is java.base. This is the only module that has only incoming edges. Each module you create reads regardless of whether you declare it or not - as in the case of an implied extension . exports packets such as the , , etc.java.base
java.lang.Object
java.base
java.lang
java.util
java.math


JDK modularization means that you can now specify which Java runtime modules you want to use. So, your application should not use an environment that supports Swing or Corba unless you read modules or . The creation of such a trimmed environment will be described in the second part. But a pretty dry theory ... We’ll chemicalize all the code in the article, available here , including shell scripts for compiling, packaging, and running the example. The practical case considered here is very simple. I have a module that performs a specific validation of a zip code. This module is read by the module (which could not only check zip codes, but here we do not do this in order not to complicate it). java.desktop
java.corba







de.codecentric.zipvalidator
de.codecentric.addresschecker

The zip validator is described in the following file : module de.codecentric.zipvalidator { exports de.codecentric.zipvalidator.api; } So, this module exports the package and does not read any other modules (except ). This module is read by addresschecker:module-info.java






de.codecentric.zipvalidator.api
java.base


module de.codecentric.addresschecker{
    exports de.codecentric.addresschecker.api;
    requires de.codecentric.zipvalidator;
}


The general structure of the file system is as follows:

two-modules-ok/
├── de.codecentric.addresschecker
│   ├── de
│   │   └── codecentric
│   │       └── addresschecker
│   │           ├── api
│   │           │   ├── AddressChecker.java
│   │           │   └── Run.java
│   │           └── internal
│   │               └── AddressCheckerImpl.java
│   └── module-info.java
├── de.codecentric.zipvalidator
│   ├── de
│   │   └── codecentric
│   │       └── zipvalidator
│   │           ├── api
│   │           │   ├── ZipCodeValidator.java
│   │           │   └── ZipCodeValidatorFactory.java
│   │           ├── internal
│   │           │   └── ZipCodeValidatorImpl.java
│   │           └── model
│   └── module-info.java


There is an agreement according to which the module is placed in a directory of the same name with this module.
In the first example, everything looks fine: we work strictly according to the rules and in our class we only access and from the exported package:AddressCheckerImpl
ZipCodeValidator
ZipCodeValidatorFactory


public class AddressCheckerImpl implements AddressChecker {
    @Override
    public boolean checkZipCode(String zipCode) {
        return ZipCodeValidatorFactory.getInstance().zipCodeIsValid(zipCode);
    }
}


Now run and generate the bytecode. To compile (which we, of course, need to do first, since the addresschecker reads the zipvalidator), we dojavac
zipvalidator


javac -d de.codecentric.zipvalidator \
$(find de.codecentric.zipvalidator -name "*.java")


It looks familiar - there is no talk of modules yet, since the zipvalidator does not depend on any user module. The command simply helps us list the files in the specified directory. But how do we report the structure of our modules when we get to compilation? To do this, a switch is entered in Jigsaw - or . To compile addresschecker, we use the following command: javac -modulepath. -d de.codecentric.addresschecker \ $ (find de.codecentric.addresschecker -name "* .java") Using modulepath, we tell javac where to find the compiled modules (in this case, this.), we get something like a path switch to classes (classpath switch).find
.java

javac
modulepath
-mp









However, compiling multiple modules individually seems to be a bit of a mess - it is better to use the other -modulesourcepath switch to compile several modules at once:

javac -d . -modulesourcepath . $(find . -name "*.java")


This code searches among all subdirectories. directories of modules and compiles all java files contained in them.
Having compiled everything, we naturally want to try what happened:

java -mp . -m de.codecentric.addresschecker/de.codecentric.addresschecker.api.Run 76185


Again, we indicate the path to the modules, telling the JVM where the compiled modules are located. We also set the main class (and parameter).

Hooray, here’s the conclusion:

76185 is a valid zip code


Modular Jar

As you know, in the Java world we are used to receiving and sending our bytecode in jar files. Jigsaw introduces the concept of modular jar. The modular jar is very similar to the usual jar , but it also contains a compiled one. Provided that such files are compiled for the desired target version, these archives will be backward compatible. Is not a valid type name, so the compiled one will be ignored by older JVMs. To build the jar for the zipvalidator, write:module-info.class.
module-info.java
module-info.class




jar --create --file bin/zipvalidator.jar \
--module-version=1.0 -C de.codecentric.zipvalidator 
.

We specify the output file, the version (although the use of several versions of the module in Jigsaw at run time is not specified separately) and the module that should be packaged.
Since addresschecker also has a main class, we can specify it:

jar --create --file=bin/addresschecker.jar --module-version=1.0 \
--main-class=de.codecentric.addresschecker.api.Run \
-C de.codecentric.addresschecker .


The main class is not indicated in , as you might expect (initially the Jigsaw team had planned to do so), but is usually written in the manifest. If you run this example withmodule-info.java




java -mp bin -m de.codecentric.addresschecker 76185


we get the same answer as in the previous case. We again indicate the path to the modules, which in this case leads to the bin directory, where we wrote our jars. We do not have to specify the main class, as the addresschecker.jar manifest already has this information. Just give the module name to the switch . So far, everything has been easy and pleasant. Next, let's tinker with the modules a bit and see how the Jigsaw behaves during compilation and execution if you start to be a bully. Using Unexported Types In this example, we will see what happens if we access a type from another module that we should not use. Since we are tired of this factory thing in , we change the implementation to-m








AddressCheckerImpl


return new ZipCodeValidatorImpl().zipCodeIsValid(zipCode);


When trying to compile, we get the expected

error: ZipCodeValidatorImpl is not visible because 
package de.codecentric.zipvalidator.internal is not visible

So, it is the use of unexported types during compilation that does not work.
But we are smart guys, so we cheat a little and use reflection.

ClassLoader classLoader = AddressCheckerImpl.class.getClassLoader();
try {
    Class aClass = classLoader.loadClass("de.[..].internal.ZipCodeValidatorImpl");
    return ((ZipCodeValidator)aClass.newInstance()).zipCodeIsValid(zipCode);
} catch (Exception e) {
    throw new  RuntimeException(e);
}

Compiled perfectly, let's run. But no, it’s not so easy to fool Jigsaw:

java.lang.IllegalAccessException:
class de.codecentric.addresschecker.internal.AddressCheckerImpl 
(in module de.codecentric.addresschecker) cannot access class [..].internal.ZipCodeValidatorImpl 
(in module de.codecentric.zipvalidator) because module
de.codecentric.zipvalidator does not export package
de.codecentric.zipvalidator.internal to module
de.codecentric.addresschecker


So, Jigsaw includes checking not only at compile time, but also at runtime! And very clearly tells us what we did wrong.

Cyclic dependencies

In the following case, we suddenly realized that the addresschecker API contained a class that the zipvalidator could use. Since we are lazy, instead of refactoring the class to another module, we declare a dependency for addresschecker:

module de.codecentric.zipvalidator{
        requires de.codecentric.addresschecker;
        exports de.codecentric.zipvalidator.api;
}


Since cyclic dependencies are prohibited by definition, the compiler gets in our way (for the sake of the common good):

./de.codecentric.zipvalidator/module-info.java:2: 
error: cyclic dependence involving de.codecentric.addresschecker


This cannot be done, and we are warned about this in advance, even during compilation.

Implied readability

To expand the functionality, we decide to inherit the zipvalidator by introducing a new module containing a certain model of the validation result, and not just a banal Boolean. The new file structure is shown here:de.codecentric.zipvalidator.model


three-modules-ok/
├── de.codecentric.addresschecker
│   ├── de
│   │   └── codecentric
│   │       └── addresschecker
│   │           ├── api
│   │           │   ├── AddressChecker.java
│   │           │   └── Run.java
│   │           └── internal
│   │               └── AddressCheckerImpl.java
│   └── module-info.java
├── de.codecentric.zipvalidator
│   ├── de
│   │   └── codecentric
│   │       └── zipvalidator
│   │           ├── api
│   │           │   ├── ZipCodeValidator.java
│   │           │   └── ZipCodeValidatorFactory.java
│   │           └── internal
│   │               └── ZipCodeValidatorImpl.java
│   └── module-info.java
├── de.codecentric.zipvalidator.model
│   ├── de
│   │   └── codecentric
│   │       └── zipvalidator
│   │           └── model
│   │               └── api
│   │                   └── ZipCodeValidationResult.java
│   └── module-info.java


A class is a simple enumeration having instances of the form “too short”, “too long”, etc. The class is inherited this way:ZipCodeValidationResult

module-info.java


module de.codecentric.zipvalidator{
       exports de.codecentric.zipvalidator.api;
       requires de.codecentric.zipvalidator.model;
}


Now our implementation of ZipCodeValidator looks like this:

@Override
public ZipCodeValidationResult zipCodeIsValid(String zipCode) {
   if (zipCode == null) {
       return ZipCodeValidationResult.ZIP_CODE_NULL;
[snip]
   } else {
       return ZipCodeValidationResult.OK;
   }
}


The addresschecker module is now adapted so that it can take an enumeration as the return type, so you can proceed, right? Not! Compilation gives:

./de.codecentric.addresschecker/de/[..]/internal/AddressCheckerImpl.java:5: 
error: ZipCodeValidationResult is not visible because package
de.codecentric.zipvalidator.model.api is not visible

An error occurred while compiling the addresschecker - the zipvalidator uses the exported types from the zipvalidator model in its public API. Since the addresschecker does not read this module, it cannot access this type.

There are two solutions to this problem. Obvious: add a read edge from the addresschecker to the zipvalidator model. However, this is a slippery slope: why do we need to declare this dependency if it is needed only for working with zipvalidator? Shouldn't the zipvalidator guarantee that we can access all the necessary modules? Should and may - here we come to implied readability. By adding the keyword public to the required definition, we tell all client modules that they must also read another module. As an example, consider an updated classmodule-info.java
zipvalidator:

module de.codecentric.zipvalidator{
       exports de.codecentric.zipvalidator.api;
       requires public de.codecentric.zipvalidator.model;
}


The keyword tells all modules reading the zipvalidator that they should also read its model. It was necessary to work with the class path differently: for example, you could not rely on Maven POM if you wanted to guarantee that all your dependencies were also accessible to any client; to achieve this, you had to explicitly specify them if they were part of your public API. This is a very beautiful model: if you use dependencies only inside the class, then what matters to your clients? And if you use them outside the class, you must also report it directly. Summarypublic




So the first part came to an end. We discussed three questions that need to be answered for each module, as well as the modularization of the Java runtime. Next, we looked at an example where we compiled, launched, and packaged a simple two-module Java application. Then, using a working example, we studied how the module system responds to a violation of established rules. Further, expanding the functionality, we studied the third module and talked about the concept of implied readability.

The following sections will address the following issues:

  • How does Jigsaw work if the module path contains several modules of the same name?
  • What happens if there are unlike modules in the path to the modules, which, however, export the same packages?
  • What to do with inherited dependencies that are not modularized?
  • How to create your own truncated version of the runtime?

Also popular now: