Artikel uit JAVA magazine 3 2021
De volgende Long Term Support (LTS) versie, Java 17, wordt in september uitgebracht. Helaas draaien veel applicaties nog op een oudere Java versie. Dit artikel helpt je om je Java applicatie te upgraden naar Java 17. Daarbij kun je gebruik maken van de Git repository[1] die ik gemaakt heb en waarin verschillende upgrade uitdagingen en oplossingen beschreven worden.
Maar waarom zou je een applicatie eigenlijk upgraden naar een nieuwere Java versie? Dat kost toch alleen maar tijd? Nou, omdat upgraden best een aantal voordelen biedt, zoals een betere performance en de laatste beveiligingsupdates. Daarnaast kun je dan ook gebruik maken van allerlei interessante nieuwe features zoals records en pattern matching.
Hoeveel tijd de upgrade kost is lastig in te schatten. Het ligt er met name aan wat voor frameworks je allemaal gebruikt. Is dat slechts Spring Boot, dan is het bijwerken daarvan waarschijnlijk al genoeg. Ook de bouw omgeving en de overige omgevingen zoals productie moeten aangepast worden zodat ze Java 17 ondersteunen. Als de omgevingen Docker gebruiken, dan is dat relatief eenvoudig aan te passen in de Dockerfile(s). Is dat niet het geval, dan zullen de omgevingen aangepast moeten worden. Deze moeten dan de nieuwe en wellicht ook tijdelijk de oude Java versie ondersteunen.
Mijn ervaring is dat teams, omdat ze van tevoren geen goed beeld hebben, een Java upgrade erg ruim schatten. Bijvoorbeeld enkele weken tot maanden voor het aanpassen van één applicatie van Java 8 naar Java 11. Diezelfde applicatie kon ik in een aantal dagen aanpassen. Dat kwam deels door mijn ervaring, maar het is ook vaak een kwestie van beginnen en zien waar het schip strand. Een leuke klus bijvoorbeeld voor een vrijdagmiddag. Nadat je begonnen bent krijg je een betere indicatie van de hoeveelheid werk en misschien heb je het wel zo omgezet!
Maar wat kost er dan zoveel werk? De meeste applicaties bestaan uit eigen geschreven code en dependencies op frameworks. De applicatie wordt op de JDK gebouwd en uitgevoerd. Als er in een nieuwe versie van de JDK onderdelen verwijderd worden, dan moeten de dependencies, je eigen code, of beide aangepast worden.
De meeste onderdelen worden niet meteen verwijderd uit de JDK. Ze worden eerst ‘deprecated’ oftewel gemarkeerd voor verwijdering. Bijvoorbeeld JAXB was deprecated in Java 9 en 10 en werd verwijderd in Java 11. Als je dus continu Java bijwerkt, dan krijg je eerst te zien dat iets deprecated is. Vervolgens kun je het oplossen en tegen de tijd dat de functionaliteit verwijderd wordt, is het voor jou applicatie geen issue meer. Daarbij kan het wel eens helpen om even te wachten totdat de dependencies aangepast zijn en de laatste Java versie ondersteunen. Wat er precies veranderd op detailniveau kun je zien in de Java Almanac [2,3]. Daarnaast is het mogelijk om op hoog niveau de OpenJDK wijzigingen per versie te bekijken [4], dan wel om de gedetailleerde release notes [5] te bekijken.
{ Welke Java versie? }
Voorheen zat er een aantal jaar tussen de verschillende Java versies. Inmiddels komt er iedere 6 maanden een nieuwe Java versie uit en iedere 3 jaar een LTS versie. Het grootste verschil is dat de niet-LTS versies korter ondersteund worden. Minor updates worden uitgebracht totdat de volgende Java versie er is. LTS versies krijgen minor updates tot ruim na de release van de volgende LTS versie. De exacte ondersteuningstermijn verschilt per organisatie die de JDK builds oplevert.
Zijn de niet-LTS versies dan testversies die je niet zou moeten gebruiken op productie? Nee, het zijn volwaardige versies die je op productie kunt gebruiken. Het grote verschil tussen een LTS en de andere versies is dat je de LTS versie makkelijk kunt bijwerken. Door de minor versie te updaten krijg je de laatste beveiliging en andere verbeteringen. Als je steeds de major versie moet updaten, dan kan dat wat meer werk zijn. Van de andere kant, als je uiteindelijk van de ene LTS versie naar de volgende gaat, dan is dat ook meer werk. Mocht je de mogelijkheid hebben om regelmatig te updaten, dan raad ik aan om de laatste Java versies te gebruiken. Op die manier moet je iets vaker upgraden, maar de stap is wel kleiner en als er iets misgaat, dan is het ook makkelijker om te achterhalen waardoor het misging.
Waar je wel op moet letten is dat een JDK preview functionaliteit kan bevatten. Dat zijn nieuwe mogelijkheden die in een volgende versie weer aangepast kunnen zijn. Standaard staan die preview features uit, maar met het argument enable-preview worden ze aangezet. Binnen je team/organisatie kun je het beste afspreken of je preview functionaliteit wilt gebruiken. Het kan namelijk zijn dat je redelijk wat code moet aanpassen als de functionaliteit weer verandert.
Voor je begint is het handig om ervoor te zorgen dat je IDE, build tools en dependencies (met de Maven[6] of Gradle[7] Versions Plugin) up to date zijn. Als alles dan nog steeds werkt, dan is het tijd om Java te upgraden en ervoor te zorgen dat achtereenvolgens de code weer compileert, de testen weer werken en de applicatie weer draait. Deze aanpak gaat meestal goed, maar soms werken nieuwe dependencies niet op de oude Java versie of zijn er andere issues. Als dat het geval is voor jou applicatie, wijk dan gerust van de aanpak af, maar probeer wel gestructureerd te updaten in plaats van alles ineens.
{ Wijzigingen per Java versie }
Door de jaren heen zijn er een aantal grotere onderdelen uit de JDK gehaald. Als je deze gebruikt in je applicatie, dan zul je tijdens de upgrade aanpassingen moeten doen.
JavaFX is niet langer onderdeel van de Java 11 specificatie. Als je applicatie JavaFX gebruikt, dan kun je de OpenJFX build van Gluon gebruiken of de openjfx dependencies toevoegen aan je project. Dat JavaFX niet langer onderdeel van de specificatie is, betekent dat het niet langer in OpenJDK zit. Maar er zijn nog steeds organisaties die JDK’s uitbrengen met OpenJFX support zoals de Liberica JDK en ojdkbuild [8].
Vanaf Java 11 bevat de JDK standaard geen lettertypen meer. Dus het besturingssysteem zal er zelf voor moeten zorgen dat de lettertypen aanwezig zijn. Dat kan op Alpine Linux bijvoorbeeld met ‘apt install fontconfig’. Afhankelijk van het besturingssysteem en de lettertypen die je gebruikt, moet je wellicht nog een aantal extra’s installeren.
Java Mission Control (JMC) kan gebruikt worden voor het monitoren en profilen van je applicatie. Mocht je er nog niet naar gekeken hebben, dan kan ik dat zeker aanraden. Standaard wordt JMC niet meer gebundeld met de JDK. Onder de nieuwe naam JDK Mission Control is die nu los te downloaden van de AdoptOpenJDK en Oracle servers.
De verwijderde Java EE en Corba modules hebben de meeste impact op applicaties. Bijna iedere applicatie gebruikt wel iets als JAXB. Om dit op te lossen kunnen dependencies toegevoegd worden. Houd er rekening mee dat je niet de laatste versies van de artifacts moet gebruiken, maar dat je ook de nieuwe namen moet aannemen (zie Afbeelding 1). Zorg er dus voor dat imports van javax.xml.bind.* omgezet worden naar imports van jakarta.xml.bind.* en dat de juiste dependencies toegevoegd worden. JAX-B en JAX-WS hebben zelfs twee dependencies nodig, één voor de API en één voor de implementatie.
Afbeelding 1.
In Java 15 is de Nashorn JavaScript Engine verwijderd. Mocht je die gebruiken, dan kun je de dependency org.openjdk.nashorn:nashorn-core toevoegen.
De toegang tot een aantal interne JDK API’s is dichtgezet in Java 16. Dit had bijvoorbeeld impact op frameworks zoals Lombok. In dit geval helpt het om te wachten op nieuwe releases van de frameworks. Mochten die er niet (snel) zijn, dan is het mogelijk om de API’s weer open te zetten door de Java compiler een aantal argumenten mee te geven. Maar dat is natuurlijk verre van netjes. Eigenlijk zouden de dependencies en je eigen code herschreven moeten worden zodat ze de interne API’s niet meer nodig hebben.
Java 17 deprecate de Applet API en verwijdert de experimentele AOT en JIT Compiler die recent waren toegevoegd. Gelukkig zal dat geen impact hebben voor de meeste applicaties aangezien Applets al lang niet meer gebruikt worden. Mocht je gebruik willen maken van de experimentele compiler dan kun je GraalVM gebruiken.
Dit zijn de grootste wijzigingen in Java die de meeste impact zullen hebben op je applicatie. In mijn ervaring los je hiermee de meeste problemen van een upgrade al op en vaak draait de applicatie dan al. Soms, met name als je veel dependencies hebt, dan kan het wat meer werk zijn. Met name om een set aan dependencies en hun versies te vinden die goed samenwerken.
Kijk ook vooral in de GitHub repository[1] waar per Java versie beschreven staat wat er fout gaat inclusief een voorbeeld en een foutmelding. Waarna je de bijbehorende oplossing kan gebruiken.
{ Multi-Release JAR }
Soms kan het voorkomen dat je nog oude versies van Java moet ondersteunen. Bijvoorbeeld wanneer je applicatie bij een klant draait die de nieuwste Java versie nog niet ondersteunt. In dat geval kun je gebruik maken van multi release JAR’s. Daarmee kun je in één JAR file code voor meerdere Java versies opnemen. Als voorbeeld nemen we een Application class [Listing 1] en een Student class [Listing 2] die we in de folder src/main/java/com/example plaatsen.
public class Application {
public static void main(String[] args) {
Student student = new Student(“James “);
System.out.println(“Implementation ” + student.implementation());
System.out.println(“Student name James contains a blank: ” + student.isBlankName());
}
}
Listing 1.
public class Student {
final private String firstName;
public Student(String firstName) {
this.firstName = firstName;
}
boolean isBlankName() {
return firstName == null || firstName.trim().isEmpty();
}
static String implementation() { return “class”; }
}
Listing 2.
Vervolgens plaatsen we een Student record [Listing 3], welke gebruik maakt van de nieuwe String.isBlank() functie, in de folder src/main/java17/com/example.
public record Student(String firstName) {
boolean isBlankName() {
return firstName.isBlank();
}
static String implementation() { return “record”; }
}
Listing 3.
Als laatste moet ook de build tool geconfigureerd worden, zie de GitHub repository [1] voor de Maven configuratie. Op basis daarvan kan op JDK 17 een FAT jar gebouwd worden. Voeren we die FAT jar vervolgens uit op een Java versie lager dan 17, dan zal de class implementatie uit de java directory gebruikt worden. Als de applicatie draait op Java 17 of hoger, dan zal de record implementatie uit de java17 folder gebruikt worden.
Wanneer ga je jouw applicatie upgraden naar Java 17?
Referenties
[1] GitHub repo met voorbeelden en documentatie: https://github.com/johanjanssen/JavaUpgrades [2] De Java Version Almanac: https://javaalmanac.io/ [3] foojay Java Version Almanac: https://foojay.io/almanac/jdk-16/ [4] OpenJDK 17 wijzigingen (pas de url aan voor oudere Java versies): https://openjdk.java.net/projects/jdk/17/ [5] Release notes (pas de url aan voor oudere Java versies): https://www.oracle.com/java/technologies/javase/16-relnote-issues.html [6] Maven Versions Plugin: https://www.mojohaus.org/versions-maven-plugin/ [7] Gradle Versions Plugin: https://github.com/ben-manes/gradle-versions-plugin [8] ojdkbuild https://github.com/ojdkbuild/ojdkbuild
Johan Janssen is software architect bij Sanoma Learning en spreekt en schrijft regelmatig over onderwerpen die met Java of de JVM te maken hebben.