Een gids voor Java 9-modulariteit

1. Overzicht

Java 9 introduceert een nieuw abstractieniveau boven pakketten, formeel bekend als het Java Platform Module System (JPMS), of kortweg “Modules”.

In deze tutorial zullen we het nieuwe systeem doorlopen en de verschillende aspecten ervan bespreken.

We zullen ook een eenvoudig project bouwen om alle concepten te demonstreren die we in deze gids zullen leren.

2. Wat is een module?

Allereerst moeten we begrijpen wat een module is voordat we kunnen begrijpen hoe we ze kunnen gebruiken.

Een module is een groep nauw verwante pakketten en bronnen, samen met een nieuw modulebeschrijvingsbestand.

Met andere woorden, het is een abstractie van een "pakket met Java-pakketten" waarmee we onze code nog meer herbruikbaar kunnen maken.

2.1. Pakketjes

De pakketten in een module zijn identiek aan de Java-pakketten die we hebben gebruikt sinds het begin van Java.

Wanneer we een module maken, we organiseren de code intern in pakketten, net zoals we eerder deden met elk ander project.

Naast het organiseren van onze code, worden pakketten gebruikt om te bepalen welke code openbaar toegankelijk is buiten de module. We zullen hier later in het artikel meer tijd over besteden.

2.2. Middelen

Elke module is verantwoordelijk voor zijn bronnen, zoals media of configuratiebestanden.

Voorheen plaatsten we alle bronnen op het rootniveau van ons project en beheerden we handmatig welke bronnen bij verschillende delen van de applicatie hoorden.

Met modules kunnen we vereiste afbeeldingen en XML-bestanden verzenden met de module die deze nodig heeft, waardoor onze projecten veel gemakkelijker te beheren zijn.

2.3. Modulebeschrijving

Wanneer we een module maken, voegen we een descriptorbestand toe dat verschillende aspecten van onze nieuwe module definieert:

  • Naam - de naam van onze module
  • Afhankelijkheden - een lijst met andere modules waarvan deze module afhankelijk is
  • Openbare pakketten - een lijst met alle pakketten die we van buiten de module toegankelijk willen maken
  • Diensten aangeboden - we kunnen service-implementaties bieden die kunnen worden gebruikt door andere modules
  • Verbruikte services - stelt de huidige module in staat een afnemer van een dienst te zijn
  • Toestemmingen voor reflectie - staat expliciet andere klassen toe om reflectie te gebruiken om toegang te krijgen tot de privéleden van een pakket

De naamgevingsregels voor modules zijn vergelijkbaar met hoe we pakketten een naam geven (punten zijn toegestaan, streepjes niet). Het is heel gebruikelijk om projectstijl (my.module) of Reverse-DNS (com.baeldung.mymodule) stijlnamen. In deze gids gebruiken we de projectstijl.

We moeten alle pakketten vermelden die we openbaar willen maken, omdat standaard alle pakketten module-privé zijn.

Hetzelfde geldt voor reflectie. Standaard kunnen we geen reflectie gebruiken over klassen die we importeren uit een andere module.

Later in het artikel zullen we voorbeelden bekijken van het gebruik van het modulebeschrijvingsbestand.

2.4. Moduletypes

Er zijn vier soorten modules in het nieuwe modulesysteem:

  • Systeemmodules- Dit zijn de modules die worden vermeld wanneer we het lijst-modules commando hierboven. Ze omvatten de Java SE- en JDK-modules.
  • Applicatiemodules - Deze modules zijn wat we normaal gesproken willen bouwen als we besluiten om modules te gebruiken. Ze worden genoemd en gedefinieerd in het gecompileerde module-info.class bestand in de geassembleerde JAR.
  • Automatische modules - We kunnen niet-officiële modules opnemen door bestaande JAR-bestanden toe te voegen aan het modulepad. De naam van de module wordt afgeleid van de naam van de JAR. Automatische modules hebben volledige leestoegang tot elke andere module die door het pad wordt geladen.
  • Naamloze module - Wanneer een klasse of JAR wordt geladen op het klassenpad, maar niet het modulepad, wordt deze automatisch toegevoegd aan de naamloze module. Het is een allesomvattende module om achterwaartse compatibiliteit met eerder geschreven Java-code te behouden.

2.5. Distributie

Modules kunnen op twee manieren worden gedistribueerd: als een JAR-bestand of als een "geëxplodeerd" gecompileerd project. Dit is natuurlijk hetzelfde als elk ander Java-project, dus het zou geen verrassing moeten zijn.

We kunnen projecten met meerdere modules maken die bestaan ​​uit een "hoofdtoepassing" en verschillende bibliotheekmodules.

We moeten echter voorzichtig zijn, want we kunnen maar één module per JAR-bestand hebben.

Wanneer we ons buildbestand opzetten, moeten we ervoor zorgen dat elke module in ons project als een aparte pot wordt gebundeld.

3. Standaardmodules

Als we Java 9 installeren, kunnen we zien dat de JDK nu een nieuwe structuur heeft.

Ze hebben alle originele pakketten meegenomen en naar het nieuwe modulesysteem verplaatst.

We kunnen zien wat deze modules zijn door in de opdrachtregel te typen:

java --list-modules

Deze modules zijn onderverdeeld in vier grote groepen: java, javafx, jdk, en Orakel.

Java modules zijn de implementatieklassen voor de SE-kernspecificatie.

javafx modules zijn de FX UI-bibliotheken.

Alles wat de JDK zelf nodig heeft, wordt bewaard in het jdk modules.

En tenslotte, alles dat Oracle-specifiek is, staat in de orakel modules.

4. Moduleverklaringen

Om een ​​module op te zetten, moeten we een speciaal bestand plaatsen in de root van onze pakketten met de naam module-info.java.

Dit bestand staat bekend als de module-descriptor en bevat alle gegevens die nodig zijn om onze nieuwe module te bouwen en te gebruiken.

We construeren de module met een declaratie waarvan de body leeg is of uit modulerichtlijnen bestaat:

module myModuleName {// alle richtlijnen zijn optioneel}

We beginnen de moduleverklaring met de module trefwoord, en we volgen dat met de naam van de module.

De module werkt met deze verklaring, maar we hebben meestal meer informatie nodig.

Dat is waar de modulerichtlijnen binnenkomen.

4.1. Vereist

Onze eerste richtlijn is vereist. Met deze module-instructie kunnen we module-afhankelijkheden declareren:

module my.module {vereist module.name; }

Nu, mijn.module heeft zowel een runtime- als een compilatietijdafhankelijkheid Aan module naam.

En alle openbare typen die vanuit een afhankelijkheid worden geëxporteerd, zijn toegankelijk via onze module wanneer we deze richtlijn gebruiken.

4.2. Vereist statisch

Soms schrijven we code die verwijst naar een andere module, maar die gebruikers van onze bibliotheek nooit zullen willen gebruiken.

We kunnen bijvoorbeeld een hulpprogramma-functie schrijven die onze interne toestand mooi afdrukt wanneer een andere logboekregistratiemodule aanwezig is. Maar niet elke gebruiker van onze bibliotheek zal deze functionaliteit willen, en ze willen geen extra logboekregistratiebibliotheek toevoegen.

In deze gevallen willen we een optionele afhankelijkheid gebruiken. Door de vereist statisch richtlijn creëren we een afhankelijkheid voor alleen compileren:

module my.module {vereist statische module.name; }

4.3. Vereist Transitive

We werken vaak samen met bibliotheken om ons leven gemakkelijker te maken.

Maar we moeten ervoor zorgen dat elke module die onze code binnenbrengt, ook deze extra ‘transitieve 'afhankelijkheden met zich meebrengt, anders werken ze niet.

Gelukkig kunnen we de vereist transitief richtlijn om alle stroomafwaartse consumenten te dwingen ook onze vereiste afhankelijkheden te lezen:

module my.module {vereist transitieve module.name; }

Nu, als een ontwikkelaar vereist my.module, hoeven ze ook niet te zeggen vereist module.name zodat onze module nog steeds werkt.

4.4. Uitvoer

Standaard stelt een module zijn API niet bloot aan andere modules. Dit sterke inkapseling was in de eerste plaats een van de belangrijkste drijfveren voor het creëren van het modulesysteem.

Onze code is aanzienlijk veiliger, maar nu moeten we onze API expliciet openstellen voor de wereld als we willen dat deze bruikbaar is.

Wij gebruiken de export richtlijn om alle openbare leden van het genoemde pakket vrij te geven:

module mijn.module {exporteert com.my.package.name; }

Nu, als iemand dat doet vereist my.module, hebben ze toegang tot de openbare typen in ons com.my.package.name pakket, maar geen ander pakket.

4.5. Exporteert ... naar

We kunnen gebruiken exporteert… naar om onze openbare lessen open te stellen voor de wereld.

Maar wat als we niet willen dat de hele wereld toegang heeft tot onze API?

We kunnen beperken welke modules toegang hebben tot onze API's met behulp van de exporteert… naar richtlijn.

Net als bij de export richtlijn, verklaren we een pakket als geëxporteerd. Maar we vermelden ook welke modules we dit pakket als een vereist. Laten we eens kijken hoe dit eruit ziet:

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

4.6. Toepassingen

EEN onderhoud is een implementatie van een specifieke interface of abstracte klasse die kan zijn verbruikt door andere klassen.

We duiden de services aan die onze module gebruikt met de toepassingen richtlijn.

Let daar op de klassenaam we gebruik is ofwel de interface ofwel de abstracte klasse van de service, niet de implementatieklasse:

module my.module {gebruikt class.name; }

We moeten hier opmerken dat er een verschil is tussen een vereist richtlijn en de toepassingen richtlijn.

We zouden kunnen vereisen een module die een service biedt die we willen gebruiken, maar die service implementeert een interface vanuit een van zijn transitieve afhankelijkheden.

In plaats van onze module te dwingen alle transitieve afhankelijkheden voor het geval dat we de toepassingen richtlijn om de vereiste interface toe te voegen aan het modulepad.

4.7. Biedt… Met

Een module kan ook een serviceprovider die andere modules kunnen verbruiken.

Het eerste deel van de richtlijn is de biedt trefwoord. Hier plaatsen we de interface of abstracte klassenaam.

Vervolgens hebben we de met richtlijn waarbij we de naam van de implementatieklasse opgeven die ofwel werktuigen de interface of strekt zich uit de abstracte klasse.

Hier is hoe het eruit ziet in elkaar gezet:

module my.module {voorziet MyInterface van MyInterfaceImpl; }

4.8. Open

We noemden eerder dat inkapseling een drijvende motivator was voor het ontwerp van dit modulesysteem.

Vóór Java 9 was het mogelijk om reflectie te gebruiken om elk type en lid in een pakket te onderzoeken, zelfs het privaat degenen. Niets was echt ingekapseld, wat allerlei problemen kan opleveren voor ontwikkelaars van de bibliotheken.

Omdat Java 9 afdwingt sterke inkapseling, we moeten nu expliciet toestemming geven voor andere modules om te reflecteren op onze lessen.

Als we volledige reflectie willen blijven toestaan ​​zoals oudere versies van Java deden, kunnen we dat eenvoudig doen Open de hele module omhoog:

open module my.module {}

4.9. Opent

Als we reflectie van privétypen moeten toestaan, maar we willen niet dat al onze code wordt getoond, we kunnen de opent richtlijn om specifieke pakketten bloot te leggen.

Maar vergeet niet dat dit het pakket voor de hele wereld opent, dus zorg ervoor dat u dat wilt:

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

4.10. Opent ... tot

Oké, dus reflectie is soms geweldig, maar we willen toch zoveel mogelijk zekerheid inkapseling. We kunnen onze pakketten selectief openen voor een vooraf goedgekeurde lijst met modules, in dit geval met behulp van de opent… voor richtlijn:

module my.module {opent com.my.package naar moduleOne, moduleTwo, enz .; }

5. Opdrachtregelopties

Inmiddels is ondersteuning voor Java 9-modules toegevoegd aan Maven en Gradle, dus u hoeft niet veel handmatig uw projecten te bouwen. Het is echter nog steeds waardevol om te weten hoe om het modulesysteem vanaf de opdrachtregel te gebruiken.

We zullen de opdrachtregel gebruiken voor ons volledige voorbeeld hieronder om te helpen verduidelijken hoe het hele systeem in onze gedachten werkt.

  • module-padWij gebruiken de –Module-pad optie om het modulepad op te geven. Dit is een lijst met een of meer mappen die uw modules bevatten.
  • add-leest - In plaats van te vertrouwen op het module-aangiftebestand, kunnen we het commandoregel-equivalent van het vereist richtlijn; –Add-leest.
  • add-exportCommandoregelvervanging voor het export richtlijn.
  • add-opensVervang de Open clausule in het module-aangiftebestand.
  • add-modulesVoegt de lijst met modules toe aan de standaardset modules
  • lijst-modulesDrukt een lijst af van alle modules en hun versiereeksen
  • patch-module - Klassen toevoegen of overschrijven in modules
  • illegale-toegang = vergunning | waarschuwen | weigeren - Ontspan sterke inkapseling door een enkele algemene waarschuwing weer te geven, toont elke waarschuwing of mislukt met fouten. De standaardwaarde is toestaan.

6. Zichtbaarheid

We zouden wat tijd moeten besteden aan het praten over de zichtbaarheid van onze code.

Veel bibliotheken zijn afhankelijk van reflectie om hun magie te bewerken (Ik denk aan JUnit en Spring).

Standaard in Java 9 zullen we dat doen enkel en alleen hebben toegang tot openbare klassen, methoden en velden in onze geëxporteerde pakketten. Zelfs als we reflectie gebruiken om toegang te krijgen tot niet-openbare leden en bellen setAccessible (true), we hebben geen toegang tot deze leden.

We kunnen de Open, opent, en opent… voor opties om runtime-only toegang te verlenen voor reflectie. Opmerking, dit is alleen runtime!

We kunnen niet compileren op basis van privé-typen, en dat zou ook nooit nodig moeten zijn.

Als we toegang moeten hebben tot een module voor reflectie, en we zijn niet de eigenaar van die module (d.w.z. we kunnen de opent… voor richtlijn), dan is het mogelijk om de opdrachtregel te gebruiken –Add-opens optie om reflectie van eigen modules toe te staan ​​tot de vergrendelde module tijdens runtime.

Het enige voorbehoud hier is dat u toegang moet hebben tot de opdrachtregelargumenten die worden gebruikt om een ​​module uit te voeren om dit te laten werken.

7. Alles samenvoegen

Nu we weten wat een module is en hoe we ze moeten gebruiken, gaan we door met het bouwen van een eenvoudig project om alle concepten te demonstreren die we zojuist hebben geleerd.

Om het simpel te houden, zullen we Maven of Gradle niet gebruiken. In plaats daarvan vertrouwen we op de opdrachtregelprogramma's om onze modules te bouwen.

7.1. Ons project opzetten

Eerst moeten we onze projectstructuur opzetten. We zullen verschillende mappen maken om onze bestanden te ordenen.

Begin met het aanmaken van de projectmap:

mkdir module-project cd module-project

Dit is de basis van ons hele project, dus voeg hier bestanden toe zoals Maven- of Gradle-buildbestanden, andere bronmappen en bronnen.

We hebben ook een map geplaatst om al onze projectspecifieke modules te bevatten.

Vervolgens maken we een modulemap:

mkdir eenvoudige modules

Hier is hoe onze projectstructuur eruit zal zien:

module-project | - // src als we het standaardpakket gebruiken | - // build-bestanden gaan ook op dit niveau | - simple-modules | - hello.modules | - com | - baeldung | - modules | - hallo | - main .app | - com | - baeldung | - modules | - main

7.2. Onze eerste module

Nu we de basisstructuur hebben, gaan we onze eerste module toevoegen.

Onder de eenvoudige modules directory, maak een nieuwe directory aan met de naam hallo.modules.

We kunnen dit alles noemen wat we willen, maar volg de regels voor de naamgeving van pakketten (d.w.z. punten om woorden te scheiden, enz.). We kunnen zelfs de naam van ons hoofdpakket gebruiken als de modulenaam als we dat willen, maar meestal willen we vasthouden aan dezelfde naam die we zouden gebruiken om een ​​JAR van deze module te maken.

Onder onze nieuwe module kunnen we de pakketten maken die we willen. In ons geval gaan we één pakketstructuur maken:

com.baeldung.modules.hello

Maak vervolgens een nieuwe klasse met de naam HelloModules.java in dit pakket. We houden de code simpel:

pakket com.baeldung.modules.hello; openbare klasse HelloModules {openbare statische leegte doSomething () {System.out.println ("Hallo, Modules!"); }}

En tot slot, in de hallo.modules root directory, voeg onze module descriptor toe; module-info.java:

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

Om dit voorbeeld simpel te houden, exporteren we alleen alle openbare leden van het com.baeldung.modules.hello pakket.

7.3. Onze tweede module

Onze eerste module is geweldig, maar doet niets.

We kunnen een tweede module maken die er nu gebruik van maakt.

Onder onze eenvoudige modules directory, maak een andere modulemap aan met de naam main.app. We beginnen deze keer met de modulebeschrijving:

module main.app {vereist hello.modules; }

We hoeven niets aan de buitenwereld bloot te stellen. In plaats daarvan hoeven we alleen maar afhankelijk te zijn van onze eerste module, zodat we toegang hebben tot de openbare klassen die het exporteert.

Nu kunnen we een applicatie maken die er gebruik van maakt.

Maak een nieuwe pakketstructuur: com.baeldung.modules.main.

Maak nu een nieuw klassenbestand aan met de naam MainApp.java.

pakket com.baeldung.modules.main; importeer com.baeldung.modules.hello.HelloModules; openbare klasse MainApp {openbare statische leegte hoofd (String [] args) {HelloModules.doSomething (); }}

En dat is alles wat we nodig hebben om modules te demonstreren. Onze volgende stap is om deze code vanaf de opdrachtregel te bouwen en uit te voeren.

7.4. Onze modules bouwen

Om ons project te bouwen, kunnen we een eenvoudig bash-script maken en dit in de root van ons project plaatsen.

Maak een bestand met de naam compileer-simple-modules.sh:

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

Deze opdracht bestaat uit twee delen, de Javac en vind commando's.

De vind commando is gewoon een lijst van alle.Java bestanden in onze directory met eenvoudige modules. We kunnen die lijst dan rechtstreeks in de Java-compiler invoeren.

Het enige dat we anders moeten doen dan de oudere versies van Java, is een module-source-path parameter om de compiler te informeren dat het modules bouwt.

Zodra we deze opdracht hebben uitgevoerd, hebben we een outDir map met daarin twee gecompileerde modules.

7.5. Onze code uitvoeren

En nu kunnen we eindelijk onze code uitvoeren om te controleren of de modules correct werken.

Maak een ander bestand in de root van het project: run-simple-module-app.sh.

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

Om een ​​module uit te voeren, moeten we ten minste het module-pad en de hoofdklasse. Als alles werkt, zou u moeten zien:

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

7.6. Een service toevoegen

Nu we een basiskennis hebben van het bouwen van een module, laten we het wat gecompliceerder maken.

We gaan zien hoe we de voorziet… met en toepassingen richtlijnen.

Begin met het definiëren van een nieuw bestand in het hallo.modules module met de naam Hallointerface.Java:

openbare interface HelloInterface {void sayHello (); }

Om het u gemakkelijk te maken, gaan we deze interface implementeren met onze bestaande HelloModules.java klasse:

openbare klasse HelloModules implementeert HelloInterface {openbare statische leegte doSomething () {System.out.println ("Hallo, Modules!"); } public void sayHello () {System.out.println ("Hallo!"); }}

Dat is alles wat we hoeven te doen om een onderhoud.

Nu moeten we de wereld vertellen dat onze module deze service biedt.

Voeg het volgende toe aan onze module-info.java:

biedt com.baeldung.modules.hello.HelloInterface met com.baeldung.modules.hello.HelloModules;

Zoals we kunnen zien, verklaren we de interface en welke klasse deze implementeert.

Vervolgens moeten we dit consumeren onderhoud. In onze main.app module, laten we het volgende toevoegen aan onze module-info.java:

gebruikt com.baeldung.modules.hello.HelloInterface;

Ten slotte kunnen we in onze hoofdmethode deze service gebruiken via een ServiceLoader:

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

Compileren en uitvoeren:

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

We gebruiken deze richtlijnen om veel explicieter te zijn over hoe onze code moet worden gebruikt.

We zouden de implementatie in een privépakket kunnen plaatsen terwijl de interface in een openbaar pakket wordt weergegeven.

Dit maakt onze code veel veiliger met heel weinig extra overhead.

Ga je gang en probeer enkele van de andere richtlijnen uit om meer te weten te komen over modules en hoe ze werken.

8. Modules toevoegen aan de naamloze module

Het naamloze moduleconcept is vergelijkbaar met het standaardpakket. Daarom wordt het niet als een echte module beschouwd, maar kan het worden gezien als de standaardmodule.

Als een klas geen lid is van een benoemde module, wordt deze automatisch beschouwd als onderdeel van deze naamloze module.

Soms moeten we modules aan de standaard rootset toevoegen om ervoor te zorgen dat specifieke platform-, bibliotheek- of serviceprovidermodules in de modulegrafiek staan. Als we bijvoorbeeld proberen om Java 8-programma's te draaien zoals ze zijn met de Java 9-compiler, moeten we mogelijk modules toevoegen.

Over het algemeen, de optie om de genoemde modules toe te voegen aan de standaardset rootmodules is –Add-modules (,)* waar is een modulenaam.

Bijvoorbeeld om iedereen toegang te geven java.xml.bind modules zou de syntaxis zijn:

--add-modules java.xml.bind

Om dit in Maven te gebruiken, kunnen we hetzelfde insluiten in het maven-compiler-plugin:

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

9. Conclusie

In deze uitgebreide gids hebben we ons geconcentreerd op de basisprincipes van het nieuwe Java 9 Module-systeem.

We zijn begonnen met te praten over wat een module is.

Vervolgens hebben we besproken hoe we kunnen ontdekken welke modules in de JDK zijn opgenomen.

We hebben ook het moduleaangiftedossier in detail behandeld.

We hebben de theorie afgerond door te praten over de verschillende opdrachtregelargumenten die we nodig hebben om onze modules te bouwen.

Ten slotte hebben we al onze voorkennis in de praktijk gebracht en een eenvoudige applicatie gemaakt die bovenop het modulesysteem is gebouwd.

Als u deze code en meer wilt zien, moet u deze op Github bekijken.