Co to jest JPMS? Wprowadzenie do systemu modułów platformy Java

Aż do wersji Java 9 głównym elementem organizacji kodu w Javie był pakiet. Począwszy od Java 9, która się zmieniła: nad pakietem jest teraz moduł. Moduł gromadzi powiązane pakiety razem.

Java Platform Module System (JPMS) jest strukturą na poziomie kodu, więc nie zmienia to faktu, że pakujemy Javę do plików JAR. Ostatecznie wszystko jest nadal spakowane w plikach JAR. System modułów dodaje nowy deskryptor wyższego poziomu, którego mogą używać module-info.javapliki JAR, poprzez włączenie pliku.

Aplikacje i organizacje na dużą skalę będą korzystać z modułów, aby lepiej organizować kod. Ale każdy będzie zużywał moduły, ponieważ JDK i jego klasy są teraz zmodularyzowane.

Dlaczego Java potrzebuje modułów

JPMS jest wynikiem projektu Jigsaw, który został podjęty w następujących określonych celach: 

  • Ułatw programistom organizowanie dużych aplikacji i bibliotek
  • Popraw strukturę i bezpieczeństwo platformy i samego JDK
  • Popraw wydajność aplikacji
  • Lepsza obsługa dekompozycji platformy dla mniejszych urządzeń

Warto zauważyć, że JPMS jest funkcją SE (Standard Edition) i dlatego wpływa na każdy aspekt Javy od podstaw. To powiedziawszy, zmiana została zaprojektowana tak, aby większość kodu działała bez modyfikacji podczas przechodzenia z Java 8 do Java 9. Istnieją pewne wyjątki od tej reguły, które omówimy w dalszej części tego przeglądu.

Główną ideą modułu jest umożliwienie gromadzenia powiązanych pakietów, które są widoczne dla modułu, jednocześnie ukrywając elementy przed zewnętrznymi odbiorcami modułu. Innymi słowy, moduł pozwala na inny poziom hermetyzacji.

Ścieżka klasy a ścieżka modułu

W Javie do tej pory ścieżka klas była podstawą tego, co jest dostępne dla uruchomionej aplikacji. Chociaż ścieżka klas służy do tego celu i jest dobrze rozumiana, ostatecznie jest dużym, niezróżnicowanym zasobnikiem, w którym umieszczane są wszystkie zależności.

Ścieżka modułu dodaje poziom powyżej ścieżki klasy. Służy jako kontener dla paczek i określa, jakie pakiety są dostępne dla aplikacji.

Moduły w JDK

Sam JDK składa się teraz z modułów. Zacznijmy od przyjrzenia się nakrętkom i śrubom JPMS.

Jeśli masz JDK w swoim systemie, masz również źródło. Jeśli nie znasz JDK i nie wiesz, jak go zdobyć, zajrzyj do tego artykułu.

W katalogu instalacyjnym JDK znajduje się /libkatalog. Wewnątrz tego katalogu znajduje się src.zipplik. Rozpakuj to do /srckatalogu.

Zajrzyj do /srckatalogu i przejdź do /java.basekatalogu. Tam znajdziesz module-info.javaplik. Otwórz to.

Po komentarzach Javadoc na początku znajduje się sekcja o nazwie,  module java.base po której następuje seria exportswierszy. Nie będziemy się tutaj rozwodzić nad formatem, ponieważ staje się on dość ezoteryczny. Szczegóły można znaleźć tutaj.

Możesz zobaczyć, że wiele znanych pakietów z języka Java, na przykład java.io, jest eksportowanych z java.basemodułu. To jest istota modułu gromadzącego razem pakiety.

Druga strona  exportsto requiresinstrukcja. Dzięki temu moduł może być wymagany przez definiowany moduł.

Podczas uruchamiania kompilatora Java na modułach należy określić ścieżkę modułu w podobny sposób, jak ścieżka klasy. Pozwala to rozwiązać zależności.

Tworzenie modułowego projektu Java

Przyjrzyjmy się, jak zbudowany jest zmodulowany projekt Java.

Zrobimy mały program, który będzie miał dwa moduły, jeden dostarczający zależność, a drugi, który używa tej zależności i eksportuje wykonywalną klasę główną.

Utwórz nowy katalog w dogodnym miejscu w systemie plików. Nazwij to /com.javaworld.mod1. Zgodnie z konwencją, moduły Java znajdują się w katalogu, który ma taką samą nazwę jak moduł.

Teraz w tym katalogu utwórz module-info.javaplik. Wewnątrz dodaj zawartość z listy 1.

Listing 1: com.javaworld.mod1 / module-info.java

module com.javaworld.mod1 { exports com.javaworld.package1; }

Zauważ, że moduł i eksportowany przez niego pakiet to różne nazwy. Definiujemy moduł eksportujący pakiet.

Teraz należy utworzyć plik na tej ścieżce, wewnątrz katalogu, który zawiera module-info.javaplik: /com.javaworld.mod1/com/javaworld/package1. Nazwij plik  Name.java. Umieść w nim zawartość Listingu 2.

Listing 2: Name.java

 package com.javaworld.package1; public class Name { public String getIt() { return "Java World"; } } 

Listing 2 stanie się klasą, pakietem i modułem, na których będziemy polegać.

Now let’s create another directory parallel to /com.javaworld.mod1 and call it /com.javaworld.mod2. In this directory, let’s create a module-info.java module definition that imports the module we already created, as in Listing 3.

Listing 3: com.javaworld.mod2/module-info.java

 module com.javaworld.mod2 { requires com.javaworld.mod1; } 

Listing 3 is pretty self-explanatory. It defines the com.javaworld.mod2 module and requires com.javaworld.mod1.

Inside the /com.javaworld.mod2 directory, create a class path like so: /com.javaworld.mod2/com/javaworld/package2.

Now add a file inside called Hello.java, with the code provided in Listing 4.

Listing 4: Hello.java

 package com.javaworld.package2; import com.javaworld.package1.Name; public class Hello { public static void main(String[] args) { Name name = new Name(); System.out.println("Hello " + name.getIt()); } } 

In Listing 4, we begin by defining the package, then importing the com.javawolrd.package1.Name class. Take note that these elements function just as they always have. The modules have changed how the packages are made available at the file structure level, not the code level.

Similarly, the code itself should be familiar to you. It simply creates a class and calls a method on it to create a classic “hello world” example.

Running the modular Java example

The first step is to create directories to receive the output of the compiler. Create a directory called /target at the root of the project. Inside, create a directory for each module: /target/com.javaworld.mod1 and /target/com.javaworld.mod2.

Step 2 is to compile the dependency module, outputting it to the /target directory. At the root of the project, enter the command in Listing 5. (This assumes the JDK is installed.)

Listing 5: Building Module 1

 javac -d target/com.javaworld.mod1 com.javaworld.mod1/module-info.java com.javaworld.mod1/com/javaworld/package1/Name.java 

This will cause the source to be built along with its module information.

Step 3 is to generate the dependent module. Enter the command shown in Listing 6.

Listing 6: Building Module 2

 javac --module-path target -d target/com.javaworld.mod2 com.javaworld.mod2/module-info.java com.javaworld.mod2/com/javaworld/package2/Hello.java 

Let’s take a look at Listing 6 in detail. It introduces the module-path argument to javac. This allows us to define the module path in similar fashion to the --class-path switch. In this example, we are passing in the target directory, because that is where Listing 5 outputs Module 1.

Next, Listing 6 defines (via the -d switch) the output directory for Module 2. Finally, the actual subjects of compilation are given, as the module-info.java file and class contained in Module 2.

To run, use the command shown in Listing 7.

Listing 7: Executing the module main class

 java --module-path target -m com.javaworld.mod2/com.javaworld.package2.Hello 

The --module-path switch tells Java to use /target directory as the module root, i.e., where to search for the modules. The -m switch is where we tell Java what our main class is. Notice that we preface the fully qualified class name with its module.

You will be greeted with the output Hello Java World.

Backward compatibility 

You may well be wondering how you can run Java programs written in pre-module versions in the post Java 9 world, given that the previous codebase knows nothing of the module path. The answer is that Java 9 is designed to be backwards compatible. However, the new module system is such a big change that you may run into issues, especially in large codebases.

When running a pre-9 codebase against Java 9, you may run into two kinds of errors: those that stem from your codebase, and those that stem from your dependencies.

For errors that stem from your codebase, the following command can be helpful: jdeps. This command when pointed at a class or directory will scan for what dependencies are there, and what modules those dependencies rely on.

For errors that stem from your dependencies, you can hope that the package you are depending on will have an updated Java 9 compatible build. If not you may have to search for alternatives.

One common error is this one:

How to resolve java.lang.NoClassDefFoundError: javax/xml/bind/JAXBException

This is Java complaining that a class is not found, because it has migrated to a module without visibility to the consuming code. There are a couple of solutions of varying complexity and permanency, described here.

Ponownie, jeśli odkryjesz takie błędy z zależnością, sprawdź w projekcie. Mogą mieć kompilację Java 9 do użytku.

JPMS to dość gruntowna zmiana, której wdrożenie zajmie trochę czasu. Na szczęście nie ma pilnego pośpiechu, ponieważ Java 8 jest wydaniem długoterminowym.

Biorąc to pod uwagę, na dłuższą metę starsze projekty będą musiały migrować, a nowe będą musiały inteligentnie używać modułów, miejmy nadzieję, że wykorzystają niektóre z obiecanych korzyści.

Artykuł „Co to jest JPMS? Wprowadzenie do systemu modułów platformy Java” został pierwotnie opublikowany przez JavaWorld.