πŸ“œ ⬆️ ⬇️

Getting Started with Java 9 and the Jigsaw Project - Part One

Good morning, Habr!

Since the time of the book " Java. A New Generation of Development " we have been following the development of the long-announced new features of this language, united under the common title " Project Jigsaw ". Today we are offering a translation of an article from November 24, which gives us sufficient confidence that the Java 9 Jigsaw version will take place.

Eight years have passed since the birth of the Jigsaw project, whose task is to modularize the Java platform and is reduced to the introduction of a common system of modules. It is assumed that Jigsaw will first appear in the Java 9 version . This release was previously planned for both Java 7 and Java 8. The scope of Jigsaw has also changed a number of times. Now there is every reason to believe that Jigsaw is almost ready, since he paid much attention to the Oracle plenary report at the JavaOne 2015 conference, as well as several presentations on this topic. What does this mean to 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 use numerous code examples to demonstrate the behavior of Jigsaw. In the first part, we discuss what constitutes a system of modules, how the JDK was modularized, and also consider the behavior of the compiler and the runtime environment in certain situations.
')
What is a module?

To describe the module is simple: it is a unit in the program, and each module immediately contains answers to three questions. These responses are recorded in the file module-info.java
module-info.java
which each module has.





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, de.codecentric.mymodule
de.codecentric.mymodule
, in order 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 a 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 exported by 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. Up to Java 8 inclusive, any public type in the path to your classes was available to any other type. With the advent of Jigsaw, Java's accessibility system changes from



on



Modularized JDK

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



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

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, if you do not read the modules java.desktop
java.desktop
or java.corba
java.corba
. Creating such a reduced environment will be described in the second part.
But pretty dry theory ...

Pokhimichim

All the code in this article is available here , including shell scripts for compiling, packaging, and running the example.

The practical case considered here is very simple. I have a module de.codecentric.zipvalidator
de.codecentric.zipvalidator
that performs a specific zip code validation. This module is read by the de.codecentric.addresschecker
module de.codecentric.addresschecker
de.codecentric.addresschecker
(which could check not only zip-codes, but here we do not do this, so as not to complicate things).
The zip validator is described in the following file module-info.java
module-info.java
:

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

So, this module exports the de.codecentric.zipvalidator.api
package de.codecentric.zipvalidator.api
de.codecentric.zipvalidator.api
and does not read any other modules (except java.base
java.base
). This module is read by addresschecker:

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


The general structure of the file system is:

 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 the directory of the same name of this module.
In the first example, everything looks great: we work strictly by the rules and in our class AddressCheckerImpl
AddressCheckerImpl
appeal only to ZipCodeValidator
ZipCodeValidator
and ZipCodeValidatorFactory
ZipCodeValidatorFactory
from exported package:

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


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

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


This looks familiar - as long as there is no talk about modules, since the zipvalidator does not depend on any user module. find
find
just helps us compile a list of .java
files .java
in the specified directory.
But as we report javac
javac
about the structure of our modules, when we get to the compilation? To do this, a switch is introduced in Jigsaw - modulepath
modulepath
or -mp
-mp
.

To compile addresschecker, we use the following command:

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

With the help of modulepath, we tell javac where to find the compiled modules (in this case, this.), It turns out something like a classpath switch.

However, compiling multiple modules separately seems like a mess - it is better to use another switch -modulesourcepath 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.
All compiled, we naturally want to try what happened:

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


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

Hooray, that'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. The concept of modular jar is introduced in jigsaw. A modular jar is very similar to a regular jar , but it also contains the compiled module-info.class.
module-info.class.
Provided that such files are compiled for the desired target version, these archives will be backward compatible. module-info.java
- not a valid type name, therefore compiled module-info.class
module-info.class
will be ignored by older JVMs.

To build a jar for zipvalidator, we write:

 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 is not specified separately during execution) and the module that should be packaged.
Since addresschecker also has a main class, we can also 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 specified in module-info.java
module-info.java
, as one might expect (initially the Jigsaw team planned to do so), and is usually written in the manifest.

If you run this example with

 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 have written our jars. We don’t have to specify the main class, since this information is already in the addresschecker.jar manifest. Just tell the module name with the -m
switch. -m
.

Until now, everything was easy and pleasant. Next, let's tinker with the modules a bit and see how Jigsaw behaves during compilation and execution if you start hooligans.

Using Unexported Types

In this example, let's see what happens if we access this type from another module that we should not use.

Because we are tired of this factory thing in AddressCheckerImpl
AddressCheckerImpl
, we change implementation to

 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 that fail during compilation.
But we are smart guys, so let's 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 a 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 run time! And very clearly tells us what we did wrong.

Cyclic dependencies

In the next case, we suddenly realized that the addresschecker module API contains a class that could be used by the zipvalidator. Since we are lazy, instead of refactoring a class into 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 forbidden by definition, the compiler gets in our way (for the common good):

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


So you can not do, and we are warned in advance about this, even during the compilation.

Implied Readability

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

 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 


ZipCodeValidationResult
class ZipCodeValidationResult
- a simple listing with instances of the form β€œtoo short”, β€œtoo long”, etc.
Class module-info.java
module-info.java
inherited this way:

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


Now our ZipCodeValidator implementation looks like this:

 @Override public <strong>ZipCodeValidationResult</strong> 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 accept and return as a return type, so that 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 addresschecker - zipvalidator uses exported types from the zipvalidator model in its public API. Since addresschecker does not read this module, it cannot access this type.

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

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


Keyword public
public
informs all modules reading zipvalidator that they must also read its model. It was different to work with the classpath: well, you could not rely on Maven POM, if you wanted to ensure that all your dependencies were also available 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 why should your customers care? And if you use them outside the class, you must also directly report this.

Summary

That came to an end the first part. 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 Java application consisting of two modules. Then, using a working example, they studied how the module system responds to violation of the established rules. Next, expanding the functionality, we studied the third module and talked about the concept of implied readability.

The following part will address the following questions:

Source: https://habr.com/ru/post/271941/


All Articles