Java 9 – co nowego w modularności

TL;DR: Java 9 wprowadza system modułów (Project Jigsaw/JPMS), który pozwala na lepszą enkapsulację kodu i zarządzanie zależnościami. Kluczowe elementy to plik module-info.java, dyrektywy exports/requires oraz nowe narzędzia jak jlink.

Dlaczego modularność w Java 9 to przełom?

System modułów w Java 9 rozwiązuje problemy, z którymi programiści borykają się od lat. Obecnie każdy jar jest dostępny dla całej aplikacji, co prowadzi do „JAR hell” – konfliktów wersji bibliotek. Modularność wprowadza kontrolowaną widoczność i wymuszone deklarowanie zależności, co znacznie poprawia bezpieczeństwo i maintenance kodu.

Modularność nie jest obowiązkowa – istniejący kod Java 8 będzie działał na Java 9 bez zmian. To backward compatible enhancement.

Co się nauczysz:

  • Jak tworzyć moduły Java za pomocą module-info.java
  • Używanie dyrektyw exports, requires i provides
  • Migracja istniejących projektów do modularności
  • Narzędzia jdeps i jlink w praktyce
  • Best practices dla modularnych aplikacji
Wymagania wstępne: Znajomość Java 8, podstawy Maven/Gradle, doświadczenie z większymi projektami gdzie zarządzanie zależnościami było problematyczne.

System modułów Java (JPMS) – podstawowe koncepcje

Java Platform Module System (JPMS) wprowadza nowy poziom abstrakcji powyżej pakietów. Moduł to pojemnik na powiązane pakiety i zasoby, który:

  • Enkapsuluje implementację – tylko wyeksportowane pakiety są dostępne
  • Deklaruje zależności – wymuszone określenie wymaganych modułów
  • Zapewnia silne enkapsulację – reflection nie omija granic modułów
Moduł – nazwa jednostka deploymentu zawierająca kod Java oraz metadane opisujące zależności i eksportowane API.

Struktura pliku module-info.java

Każdy moduł musi zawierać plik module-info.java w głównym katalogu źródeł:

// src/main/java/module-info.java
module com.mycompany.userservice {
    // Eksportujemy pakiety dostępne dla innych modułów
    exports com.mycompany.userservice.api;
    exports com.mycompany.userservice.dto;
    
    // Wymagamy innych modułów
    requires java.base;          // automatycznie dodawane
    requires java.sql;
    requires com.mycompany.commons;
    
    // Opcjonalne zależności
    requires static lombok;       // compile-time only
    requires transitive gson;     // propaguje zależność
}
Pro tip: Używaj requires transitive gdy twój moduł wymaga, żeby klienci mieli dostęp do danej zależności. To jak „re-export” w innych językach.

Tworzenie pierwszego modułu

Stwórzmy praktyczny przykład modularnej aplikacji z dwoma modułami:

Moduł commons (współdzielone utility)

// commons/src/main/java/module-info.java
module com.example.commons {
    exports com.example.commons.utils;
    exports com.example.commons.model;
}

// commons/src/main/java/com/example/commons/utils/StringUtils.java
package com.example.commons.utils;

public class StringUtils {
    public static boolean isNotEmpty(String str) {
        return str != null && !str.trim().isEmpty();
    }
    
    // Ta metoda NIE będzie dostępna z zewnątrz
    // bo pakiet internal nie jest exported
    static void internalMethod() {
        System.out.println("Internal utility");
    }
}

Moduł application (główna aplikacja)

// app/src/main/java/module-info.java
module com.example.application {
    requires com.example.commons;
    requires java.base;  // implicit, ale można dodać explicit
    
    // Ten moduł nie eksportuje niczego - to aplikacja końcowa
}

// app/src/main/java/com/example/application/Main.java
package com.example.application;

import com.example.commons.utils.StringUtils;

public class Main {
    public static void main(String[] args) {
        String input = "Hello Java 9 Modules!";
        
        if (StringUtils.isNotEmpty(input)) {
            System.out.println("Input is valid: " + input);
        }
        
        // To nie skompiluje się - metoda internal nie jest dostępna
        // StringUtils.internalMethod(); // BŁĄD KOMPILACJI
    }
}
Uwaga: Gdy używasz modułów, refleksja nie może dostać się do niepublicznych elementów z innych modułów. To znacząca zmiana dla niektórych frameworków.

Dyrektywy module-info.java – pełna lista

DyrektywaOpisPrzykład
exportsUdostępnia pakiet innym modułomexports com.example.api;
exports…toUdostępnia pakiet tylko określonym modułomexports com.example.internal to com.example.test;
requiresDeklaruje zależność od innego modułurequires java.sql;
requires transitivePropaguje zależność do klientówrequires transitive java.desktop;
requires staticOpcjonalna zależność (compile-time)requires static lombok;
provides…withImplementuje service providerprovides DatabaseDriver with MySQLDriver;
usesKonsumuje serviceuses DatabaseDriver;
opensPozwala na reflection dostępopens com.example.model;

Service Provider Interface (SPI) w modułach

Java 9 ułatwia implementację wzorca SPI:

// Moduł API
module database.api {
    exports com.example.database.api;
}

// Interface
public interface DatabaseDriver {
    Connection connect(String url);
}

// Moduł implementacji
module database.mysql {
    requires database.api;
    provides com.example.database.api.DatabaseDriver 
        with com.example.database.mysql.MySQLDriver;
}

// Moduł klienta
module my.application {
    requires database.api;
    uses com.example.database.api.DatabaseDriver;
}

// Użycie w kodzie
ServiceLoader loader = ServiceLoader.load(DatabaseDriver.class);
DatabaseDriver driver = loader.findFirst().orElseThrow();

Narzędzia do pracy z modułami

jdeps – analiza zależności

Narzędzie jdeps pomaga w analizie istniejącego kodu i przygotowaniu do modularyzacji:

# Sprawdź zależności JAR-a
jdeps myapp.jar

# Sugerowane moduły dla istniejącego kodu
jdeps --generate-module-info . myapp.jar

# Sprawdź czy kod korzysta z internal JDK APIs
jdeps --jdk-internals myapp.jar

jlink – tworzenie runtime image

Nowe narzędzie jlink pozwala tworzyć minimalne runtime obrazy:

# Tworzenie custom runtime z tylko potrzebnymi modułami
jlink --module-path $JAVA_HOME/jmods:lib \
      --add-modules com.example.application \
      --output myapp-runtime \
      --launcher myapp=com.example.application/com.example.application.Main

# Rezultat - folder z kompletną, minimalną JVM + aplikacja
./myapp-runtime/bin/myapp
Pro tip: Runtime image utworzony przez jlink może być 5-10 razy mniejszy od standardowej JVM, co znacznie przyspiesza deployment w kontenerach.

Migracja istniejących projektów

Strategia postupowa migracji

Nie musisz modularyzować całej aplikacji od razu. Java 9 wspiera trzy rodzaje „modułów”:

  • Named modules – mają module-info.java
  • Automatic modules – JAR-y na module path bez module-info
  • Unnamed module – kod na classpath (legacy)

Krok po kroku: modularyzacja Maven projektu



    9
    9



    
        
            org.apache.maven.plugins
            maven-compiler-plugin
            3.6.1
            
                9
                9
            
        
    

Typowe problemy podczas migracji

Pułapka: Split packages – gdy ten sam pakiet istnieje w dwóch modułach. Java 9 tego nie toleruje i trzeba refactorować kod.
// PROBLEM: ten sam pakiet w dwóch modułach
// modul1: com.example.utils.StringHelper
// modul2: com.example.utils.NumberHelper

// ROZWIĄZANIE: przenieś do osobnych pakietów
// modul1: com.example.string.StringHelper  
// modul2: com.example.number.NumberHelper

Best practices dla modularności

Pro tip: Nazywaj moduły używając reverse domain notation jak pakiety Java: com.company.product.feature

Zasady projektowania modułów

  1. Pojedyncza odpowiedzialność – jeden moduł = jedna funkcjonalność biznesowa
  2. Minimalne API – eksportuj tylko to co naprawdę potrzebne
  3. Unikaj cyklicznych zależności – projektuj hierarchię modułów
  4. Stabilne interfaces – zmiany w eksportowanych API wpływają na wszystkich klientów

Wzorce architektoniczne

// Wzorzec: API + Implementation
module myapp.api {
    exports com.example.myapp.api;
}

module myapp.impl {
    requires myapp.api;
    provides com.example.myapp.api.MyService 
        with com.example.myapp.impl.MyServiceImpl;
}

module myapp.client {
    requires myapp.api;
    uses com.example.myapp.api.MyService;
}

Wpływ na frameworki i biblioteki

Modularność Java 9 wpłynie na cały ekosystem:

Spring Framework

Spring planuje stopniowe wprowadzenie wsparcia dla modułów. Obecnie Spring Boot 1.x będzie działał jako automatic module.

Hibernate i JPA

ORM libraries będą musiały dostosować się do ograniczeń reflection w modularnym środowisku.

Uwaga: Niektóre biblioteki używające reflection do prywatnych pól mogą przestać działać. Sprawdź kompatybilność przed migracją.
Czy muszę modularyzować istniejący kod Java 8?

Nie, modularność jest opcjonalna. Istniejący kod Java 8 będzie działał na Java 9 bez zmian jako „unnamed module”. Modularyzacja to długoterminowa inwestycja w architekturę.

Jak module-info.java wpływa na wydajność?

Modularność może poprawić wydajność przez eliminację niepotrzebnych zależności podczas kompilacji i runtime. JVM może też lepiej optymalizować modularny kod.

Co to są automatic modules?

JAR-y bez module-info.java umieszczone na module path stają się automatic modules. Ich nazwa modułu jest wygenerowana automatycznie z nazwy JAR-a. Eksportują wszystkie pakiety i wymagają wszystkich innych modułów.

Czy mogę mieszać moduły z classpath?

Tak, Java 9 wspiera stopniową migrację. Kod na classpath działa jako unnamed module i może używać modularnego kodu, ale nie na odwrót.

Jak debugging modularnych aplikacji?

Używaj flag JVM: --show-module-resolution do debugowania ładowania modułów, --illegal-access=debug do wykrywania problemów z enkapsulacją.

Jakie są rozmiary runtime image z jlink?

Minimalna aplikacja z base module zajmuje ~40MB vs ~200MB standardowego JRE. Aplikacje Spring Boot mogą zredukować rozmiar z 150MB do 80MB.

Czy IDE wspierają moduły Java 9?

IntelliJ IDEA 2017.1+ i Eclipse Oxygen będą wspierać moduły. Aktualnie (grudzień 2016) wsparcie jest w fazie beta/early access.

🚀 Zadanie dla Ciebie

Stwórz modularną aplikację składającą się z trzech modułów:

  1. common – zawiera klasę Logger z metodą log(String message)
  2. calculator – używa common, eksportuje klasę Calculator z metodami add/subtract
  3. app – używa calculator i common, zawiera main method

Dodaj odpowiednie pliki module-info.java i przetestuj kompilację z javac oraz uruchomienie z java.

Przydatne zasoby:

Jakie są twoje doświadczenia z modularyzacją projektów? Które biblioteki sprawiają najwięcej problemów podczas migracji?

Zostaw komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Przewijanie do góry