Ein Leitfaden zur Java 9-Modularität

1. Übersicht

Java 9 führt eine neue Abstraktionsebene über Paketen ein, die formal als Java Platform Module System (JPMS) oder kurz „Module“ bezeichnet wird.

In diesem Tutorial werden wir das neue System durchgehen und seine verschiedenen Aspekte diskutieren.

Wir werden auch ein einfaches Projekt erstellen, um alle Konzepte zu demonstrieren, die wir in diesem Handbuch lernen werden.

2. Was ist ein Modul?

Zunächst müssen wir verstehen, was ein Modul ist, bevor wir verstehen können, wie man es verwendet.

Ein Modul ist eine Gruppe eng verwandter Pakete und Ressourcen zusammen mit einer neuen Moduldeskriptordatei.

Mit anderen Worten, es handelt sich um eine Abstraktion „Paket von Java-Paketen“, mit der wir unseren Code noch wiederverwendbarer machen können.

2.1. Pakete

Die Pakete in einem Modul sind identisch mit den Java-Paketen, die wir seit der Einführung von Java verwenden.

Wenn wir ein Modul erstellen, organisieren wir den Code intern in Paketen, wie wir es zuvor bei jedem anderen Projekt getan haben.

Neben der Organisation unseres Codes werden Pakete verwendet, um zu bestimmen, welcher Code außerhalb des Moduls öffentlich zugänglich ist. Wir werden später in diesem Artikel mehr Zeit damit verbringen, darüber zu sprechen.

2.2. Ressourcen

Jedes Modul ist für seine Ressourcen wie Medien oder Konfigurationsdateien verantwortlich.

Zuvor haben wir alle Ressourcen in die Stammebene unseres Projekts gestellt und manuell verwaltet, welche Ressourcen zu verschiedenen Teilen der Anwendung gehörten.

Mit Modulen können wir die erforderlichen Bilder und XML-Dateien mit dem Modul versenden, das sie benötigt, wodurch unsere Projekte viel einfacher zu verwalten sind.

2.3. Modulbeschreibung

Wenn wir ein Modul erstellen, fügen wir eine Deskriptordatei hinzu, die verschiedene Aspekte unseres neuen Moduls definiert:

  • Name - der Name unseres Moduls
  • Abhängigkeiten - Eine Liste anderer Module, von denen dieses Modul abhängt
  • Öffentliche Pakete - Eine Liste aller Pakete, auf die von außerhalb des Moduls zugegriffen werden soll
  • Angebotene Services - Wir können Service-Implementierungen bereitstellen, die von anderen Modulen verwendet werden können
  • Verbrauchte Dienste - Ermöglicht dem aktuellen Modul, Verbraucher eines Dienstes zu sein
  • Reflexionsberechtigungen - Ermöglicht anderen Klassen explizit die Verwendung von Reflexion, um auf die privaten Mitglieder eines Pakets zuzugreifen

Die Regeln für die Benennung von Modulen ähneln denen für die Benennung von Paketen (Punkte sind zulässig, Bindestriche nicht). Es ist sehr üblich , entweder Projekt-Stil (my.module) oder Reverse DNS (zu tun com.baeldung.mymodule ) Stilnamen. In diesem Handbuch wird der Projektstil verwendet.

Wir müssen alle Pakete auflisten, die öffentlich sein sollen, da standardmäßig alle Pakete modulprivat sind.

Gleiches gilt für die Reflexion. Standardmäßig können wir keine Reflexion für Klassen verwenden, die wir aus einem anderen Modul importieren.

Später in diesem Artikel werden Beispiele für die Verwendung der Moduldeskriptordatei vorgestellt.

2.4. Modultypen

Es gibt vier Arten von Modulen im neuen Modulsystem:

  • Systemmodule- Dies sind die Module, die aufgelistet werden, wenn wir den obigen Befehl list-modules ausführen . Dazu gehören die Java SE- und JDK-Module.
  • Anwendungsmodule - Diese Module möchten wir normalerweise erstellen, wenn wir uns für die Verwendung von Modulen entscheiden. Sie werden in der kompilierten Datei module-info.class benannt und definiert, die in der zusammengestellten JAR enthalten ist.
  • Automatische Module - Wir können inoffizielle Module einschließen, indem wir dem Modulpfad vorhandene JAR-Dateien hinzufügen. Der Name des Moduls wird vom Namen der JAR abgeleitet. Automatische Module haben vollen Lesezugriff auf alle anderen vom Pfad geladenen Module.
  • Unbenanntes Modul - Wenn eine Klasse oder JAR in den Klassenpfad geladen wird, jedoch nicht in den Modulpfad, wird sie automatisch dem unbenannten Modul hinzugefügt. Es ist ein Sammelmodul, um die Abwärtskompatibilität mit zuvor geschriebenem Java-Code aufrechtzuerhalten.

2.5. Verteilung

Module können auf zwei Arten verteilt werden: als JAR-Datei oder als "explodiertes" kompiliertes Projekt. Dies ist natürlich das gleiche wie bei jedem anderen Java-Projekt, daher sollte es keine Überraschung sein.

Wir können Projekte mit mehreren Modulen erstellen, die aus einer „Hauptanwendung“ und mehreren Bibliotheksmodulen bestehen.

Wir müssen jedoch vorsichtig sein, da wir nur ein Modul pro JAR-Datei haben können.

Wenn wir unsere Build-Datei einrichten, müssen wir sicherstellen, dass jedes Modul in unserem Projekt als separate JAR-Datei gebündelt wird.

3. Standardmodule

Wenn wir Java 9 installieren, können wir sehen, dass das JDK jetzt eine neue Struktur hat.

Sie haben alle Originalpakete genommen und in das neue Modulsystem verschoben.

Wir können sehen, was diese Module sind, indem wir in die Befehlszeile eingeben:

java --list-modules

Diese Module sind in vier Hauptgruppen unterteilt: Java, Javafx, JDK und Oracle .

Java- Module sind die Implementierungsklassen für die Kern-SE-Sprachspezifikation.

Javafx- Module sind die FX-UI-Bibliotheken.

Alles, was das JDK selbst benötigt, wird in den JDK- Modulen gespeichert .

Und schließlich alles , was Oracle-spezifisch ist , ist in den Oracle - Modulen.

4. Moduldeklarationen

Um ein Modul einzurichten, müssen wir eine spezielle Datei im Stammverzeichnis unserer Pakete mit dem Namen module-info.java ablegen .

Diese Datei wird als Moduldeskriptor bezeichnet und enthält alle Daten, die zum Erstellen und Verwenden unseres neuen Moduls erforderlich sind.

Wir konstruieren das Modul mit einer Deklaration, deren Hauptteil entweder leer ist oder aus Modulanweisungen besteht:

module myModuleName { // all directives are optional }

We start the module declaration with the module keyword, and we follow that with the name of the module.

The module will work with this declaration, but we'll commonly need more information.

That is where the module directives come in.

4.1. Requires

Our first directive is requires. This module directive allows us to declare module dependencies:

module my.module { requires module.name; }

Now, my.module has both a runtime and a compile-time dependency on module.name.

And all public types exported from a dependency are accessible by our module when we use this directive.

4.2. Requires Static

Sometimes we write code that references another module, but that users of our library will never want to use.

For instance, we might write a utility function that pretty-prints our internal state when another logging module is present. But, not every consumer of our library will want this functionality, and they don't want to include an extra logging library.

In these cases, we want to use an optional dependency. By using the requires static directive, we create a compile-time-only dependency:

module my.module { requires static module.name; }

4.3. Requires Transitive

We commonly work with libraries to make our lives easier.

But, we need to make sure that any module that brings in our code will also bring in these extra ‘transitive' dependencies or they won't work.

Luckily, we can use the requires transitive directive to force any downstream consumers also to read our required dependencies:

module my.module { requires transitive module.name; }

Now, when a developer requires my.module, they won't also have also to say requires module.name for our module to still work.

4.4. Exports

By default, a module doesn't expose any of its API to other modules. This strong encapsulation was one of the key motivators for creating the module system in the first place.

Our code is significantly more secure, but now we need to explicitly open our API up to the world if we want it to be usable.

We use the exports directive to expose all public members of the named package:

module my.module { exports com.my.package.name; }

Now, when someone does requires my.module, they will have access to the public types in our com.my.package.name package, but not any other package.

4.5. Exports … To

We can use exports…to to open up our public classes to the world.

But, what if we don't want the entire world to access our API?

We can restrict which modules have access to our APIs using the exports…to directive.

Similar to the exports directive, we declare a package as exported. But, we also list which modules we are allowing to import this package as a requires. Let's see what this looks like:

module my.module { export com.my.package.name to com.specific.package; }

4.6. Uses

A service is an implementation of a specific interface or abstract class that can be consumed by other classes.

We designate the services our module consumes with the uses directive.

Note that the class name we use is either the interface or abstract class of the service, not the implementation class:

module my.module { uses class.name; }

We should note here that there's a difference between a requires directive and the uses directive.

We might require a module that provides a service we want to consume, but that service implements an interface from one of its transitive dependencies.

Instead of forcing our module to require all transitive dependencies just in case, we use the uses directive to add the required interface to the module path.

4.7. Provides … With

A module can also be a service provider that other modules can consume.

The first part of the directive is the provides keyword. Here is where we put the interface or abstract class name.

Next, we have the with directive where we provide the implementation class name that either implements the interface or extends the abstract class.

Here's what it looks like put together:

module my.module { provides MyInterface with MyInterfaceImpl; }

4.8. Open

We mentioned earlier that encapsulation was a driving motivator for the design of this module system.

Before Java 9, it was possible to use reflection to examine every type and member in a package, even the private ones. Nothing was truly encapsulated, which can open up all kinds of problems for developers of the libraries.

Because Java 9 enforces strong encapsulation, we now have to explicitly grant permission for other modules to reflect on our classes.

If we want to continue to allow full reflection as older versions of Java did, we can simply open the entire module up:

open module my.module { }

4.9. Opens

If we need to allow reflection of private types, but we don't want all of our code exposed, we can use the opens directive to expose specific packages.

But remember, this will open the package up to the entire world, so make sure that is what you want:

module my.module { opens com.my.package; }

4.10. Opens … To

Okay, so reflection is great sometimes, but we still want as much security as we can get from encapsulation. We can selectively open our packages to a pre-approved list of modules, in this case, using the opens…to directive:

module my.module { opens com.my.package to moduleOne, moduleTwo, etc.; }

5. Command Line Options

By now, support for Java 9 modules has been added to Maven and Gradle, so you won't need to do a lot of manual building of your projects. However, it's still valuable to know how to use the module system from the command line.

We'll be using the command line for our full example down below to help solidify how the entire system works in our minds.

  • module-pathWe use the –module-path option to specify the module path. This is a list of one or more directories that contain your modules.
  • add-reads – Instead of relying on the module declaration file, we can use the command line equivalent of the requires directive; –add-reads.
  • add-exportsCommand line replacement for the exports directive.
  • add-opensReplace the open clause in the module declaration file.
  • add-modulesAdds the list of modules into the default set of modules
  • list-modulesPrints a list of all modules and their version strings
  • patch-module – Add or override classes in a modules
  • illegal-access=permit|warn|deny – Either relax strong encapsulation by showing a single global warning, shows every warning, or fails with errors. The default is permit.

6. Visibility

We should spend a little time talking about the visibility of our code.

A lot of libraries depend on reflection to work their magic (JUnit and Spring come to mind).

By default in Java 9, we will only have access to public classes, methods, and fields in our exported packages. Even if we use reflection to get access to non-public members and call setAccessible(true), we won't be able to access these members.

We can use the open, opens, and opens…to options to grant runtime-only access for reflection. Note, this is runtime-only!

We won't be able to compile against private types, and we should never need to anyway.

If we must have access to a module for reflection, and we're not the owner of that module (i.e., we can't use the opens…to directive), then it's possible to use the command line –add-opens option to allow own modules reflection access to the locked down module at runtime.

The only caveat here's that you need to have access to the command line arguments that are used to run a module for this to work.

7. Putting It All Together

Now that we know what a module is and how to use them let's go ahead and build a simple project to demonstrate all the concepts we just learned.

To keep things simple, we won't be using Maven or Gradle. Instead, we'll rely on the command line tools to build our modules.

7.1. Setting Up Our Project

First, we need to set up our project structure. We'll create several directories to organize our files.

Start by creating the project folder:

mkdir module-project cd module-project

This is the base of our whole project, so add files in here such as Maven or Gradle build files, other source directories, and resources.

We also put a directory to hold all our project specific modules.

Next, we create a module directory:

mkdir simple-modules

Here's what our project structure will look like:

module-project |- // src if we use the default package |- // build files also go at this level |- simple-modules |- hello.modules |- com |- baeldung |- modules |- hello |- main.app |- com |- baeldung |- modules |- main

7.2. Our First Module

Now that we have the basic structure in place, let's add our first module.

Under the simple-modules directory, create a new directory called hello.modules.

We can name this anything we want but follow package naming rules (i.e., periods to separate words, etc.). We can even use the name of our main package as the module name if we want, but usually, we want to stick to the same name we would use to create a JAR of this module.

Under our new module, we can create the packages we want. In our case, we are going to create one package structure:

com.baeldung.modules.hello

Next, create a new class called HelloModules.java in this package. We will keep the code simple:

package com.baeldung.modules.hello; public class HelloModules { public static void doSomething() { System.out.println("Hello, Modules!"); } }

And finally, in the hello.modules root directory, add in our module descriptor; module-info.java:

module hello.modules { exports com.baeldung.modules.hello; }

To keep this example simple, all we are doing is exporting all public members of the com.baeldung.modules.hello package.

7.3. Our Second Module

Our first module is great, but it doesn't do anything.

We can create a second module that uses it now.

Under our simple-modules directory, create another module directory called main.app. We are going to start with the module descriptor this time:

module main.app { requires hello.modules; }

We don't need to expose anything to the outside world. Instead, all we need to do is depend on our first module, so we have access to the public classes it exports.

Now we can create an application that uses it.

Create a new package structure: com.baeldung.modules.main.

Now, create a new class file called MainApp.java.

package com.baeldung.modules.main; import com.baeldung.modules.hello.HelloModules; public class MainApp { public static void main(String[] args) { HelloModules.doSomething(); } }

And that is all the code we need to demonstrate modules. Our next step is to build and run this code from the command line.

7.4. Building Our Modules

To build our project, we can create a simple bash script and place it at the root of our project.

Create a file called compile-simple-modules.sh:

#!/usr/bin/env bash javac -d outDir --module-source-path simple-modules $(find simple-modules -name "*.java")

There are two parts to this command, the javac and find commands.

The find command is simply outputting a list of all .java files under our simple-modules directory. We can then feed that list directly into the Java compiler.

The only thing we have to do differently than the older versions of Java is to provide a module-source-path parameter to inform the compiler that it's building modules.

Once we run this command, we will have an outDir folder with two compiled modules inside.

7.5. Running Our Code

And now we can finally run our code to verify modules are working correctly.

Create another file in the root of the project: run-simple-module-app.sh.

#!/usr/bin/env bash java --module-path outDir -m main.app/com.baeldung.modules.main.MainApp

To run a module, we must provide at least the module-path and the main class. If all works, you should see:

>$ ./run-simple-module-app.sh Hello, Modules!

7.6. Adding a Service

Now that we have a basic understanding of how to build a module, let's make it a little more complicated.

We're going to see how to use the provides…with and uses directives.

Start by defining a new file in the hello.modules module named HelloInterface.java:

public interface HelloInterface { void sayHello(); }

To make things easy, we're going to implement this interface with our existing HelloModules.java class:

public class HelloModules implements HelloInterface { public static void doSomething() { System.out.println("Hello, Modules!"); } public void sayHello() { System.out.println("Hello!"); } }

That is all we need to do to create a service.

Now, we need to tell the world that our module provides this service.

Add the following to our module-info.java:

provides com.baeldung.modules.hello.HelloInterface with com.baeldung.modules.hello.HelloModules;

As we can see, we declare the interface and which class implements it.

Next, we need to consume this service. In our main.app module, let's add the following to our module-info.java:

uses com.baeldung.modules.hello.HelloInterface;

Finally, in our main method we can use this service via a ServiceLoader:

Iterable services = ServiceLoader.load(HelloInterface.class); HelloInterface service = services.iterator().next(); service.sayHello();

Compile and run:

#> ./run-simple-module-app.sh Hello, Modules! Hello!

We use these directives to be much more explicit about how our code is to be used.

We could put the implementation into a private package while exposing the interface in a public package.

This makes our code much more secure with very little extra overhead.

Go ahead and try out some of the other directives to learn more about modules and how they work.

8. Adding Modules to the Unnamed Module

The unnamed module concept is similar to the default package. Therefore, it's not considered a real module, but can be viewed as the default module.

If a class is not a member of a named module, then it will be automatically considered as part of this unnamed module.

Sometimes, to ensure specific platform, library, or service-provider modules in the module graph, we need to add modules to the default root set. For example, when we try to run Java 8 programs as-is with Java 9 compiler we may need to add modules.

In general, the option to add the named modules to the default set of root modules is –add-modules (,)* where is a module name.

For example, to provide access to all java.xml.bind modules the syntax would be:

--add-modules java.xml.bind

To use this in Maven, we can embed the same to the maven-compiler-plugin:

 org.apache.maven.plugins maven-compiler-plugin 3.8.0  9 9  --add-modules java.xml.bind   

9. Conclusion

In diesem ausführlichen Handbuch haben wir uns auf die Grundlagen des neuen Java 9-Modulsystems konzentriert und diese behandelt.

Wir haben zunächst darüber gesprochen, was ein Modul ist.

Als Nächstes haben wir darüber gesprochen, wie Sie herausfinden können, welche Module im JDK enthalten sind.

Wir haben auch die Moduldeklarationsdatei ausführlich behandelt.

Wir haben die Theorie abgerundet, indem wir über die verschiedenen Befehlszeilenargumente gesprochen haben, die wir zum Erstellen unserer Module benötigen.

Schließlich haben wir unser gesamtes Vorwissen in die Praxis umgesetzt und eine einfache Anwendung erstellt, die auf dem Modulsystem aufbaut.

Um diesen Code und mehr zu sehen, lesen Sie ihn unbedingt auf Github.