Itt jársz most: Kezdőlap > Alkalmazásfejlesztés > Spring Boot alkalmazás migrálása Java 11-re

Szűrő megjelenítése

Spring Boot alkalmazás migrálása Java 11-re

Bár a Leaflet nem tekinthető egy túl nagy alkalmazásnak, a Java 11 körüli általános migrálási problémákba azért sikerült szinte kötelezően belefutnom. Számtalan blogot és fórumot (beleértve a StackOverFlow-t is) kellett feltúrnom ahhoz, hogy minden felmerülő problémára megtaláljam a megoldást - azonban így is akadt pár, amire jelenleg egyszerűen nincs, vagy legalábbis nem érdemes időt fordítani rá. Előbb vegyük sorba azokat, amik sikerültek.

Az alapok

Az első és legfontosabb az új JDK beszerzése. A Oracle JDK 11-es verziója immár a korábbiaktól eltérő licenszelés alatt kerül terjesztésre, mely enterprise környezetben már nem teszi lehetővé az alkalmazásaink futtatását - kivéve, ha súlyos licenszdíjat fizetünk érte, melyet az Oracle évenként és processzor-magonként számláz. Bár a fejlesztés (és tesztelés) során továbbra is használható az Oracle JDK implementációja, illetve az OpenJDK ezentúl az Oracle JDK támogatás-mentes tükörmásolataként él majd tovább, személy szerint azt tudnám javasolni, hogy a fejlesztés és az üzemeltetés lehetőleg azonos JDK-n történjen. Így persze eldől, mit használhatunk: sok felesleges pénz birtokában Oracle JDK, egyébként meg OpenJDK.

A JDK telepítése az Oracle implementáció esetében nem különbözik, OpenJDK esetén azonban búcsút kell intenünk az automatikus telepítőnek, a JAVA_HOME környezeti változó PATH-ra helyezésével tudjuk megoldani a “telepítést”. Érdekesség, hogy bár a tooling tekintetében sem különösebben változik a helyzet, az OpenJDK csomagból valahogy (egyelőre) hiányzik a JVisualVM - szerencsére VisualVM néven, külső projektként továbbra is elérhető, pluginjeivel együtt.

A következő lépés a Maven és a projekt Maven pluginjainak frissítése. Mivel a Java 9+ támogatás még viszonylag friss, jobb ha mindenből a legfrissebb verziót keressük. Ezek nagyjából a következőket jelentik:

  • Maven: 3.5.x fölött rendben leszünk, bár akik IntelliJ IDEA-t használnak külső Maven-nel, legyenek résen a 3.6.0 verzióval: az IDEA ezzel a verzióval nem képes frissíteni a dependency-ket, s bár az alkalmazás mindössze annyit mond, hogy sikertelen a frissítés, a háttérben az IDEA beépített HTTP kliense dob kivételt
  • Maven Compiler plugin: 3.8.0-s verzióra lesz szükségünk. Ez egyébként már tartalmazza a megfelelő verziójú ASM-et is (6.2+), így annak felülbírálására külön már nem lesz szükség.
  • Maven Surefire és Failsafe: 2.22.1-es verzióval rendben leszünk, bár a komponens modularizálása után problémákba fogunk ütközni - erről később.
  • Jacoco: 0.8.2 már rendelkezik a megfelelő támogatással, de kéz-a-kézben a Surefire-rel, modularizált Java 11 komponens esetén készüljünk fel egy adag nyugtatóval.

Természetesen nem szabad elfelejteni átállítani a Maven Compiler source és target verzióit sem, ami ez esetben már nem 1.11, hanem csak röviden 11 lesz.

Most, hogy megvagyunk az alapokkal, kezdhetjük az alkalmazás migrálását.

Spring Boot és Java 11 két jó... barát?

A Java licenszelésének megváltozása bár nem volt új dolog, a támogatás gyors léptékben történő megszüntetése nem adott túl sok lehetőséget arra, hogy a tooling és a framework-ök felnőjenek a feladathoz - erre a gondolatra még később visszatérünk a problémák tárgyalásánál. Szerencsére a Pivotal hamar lépett, a Java 11 hivatalos megjelenése után nem sokkal a támogatással felvértezett Spring Framework 5.1 és Spring Boot 2.1 milestone verziói már elérhetőek voltak, majd december elején megjelentek a végleges verziók is. Való igaz, ezek a verziók már tisztán támogatják a Java 11-et, megszűntek a kisebb-nagyobb problémák, amik a korábbi verziókban még Java 11 runtime-mal előfordultak, de mint utólag kiderült, a “teljes támogatás” meglehetősen túlzónak bizonyult, mivel a Java 9-ben debütált Jigsaw modul rendszer nos… nem teljesen kompatibilis. Később erre is visszatértünk.

Szóval ugyebár a Java 9 fölött bizonyos csomagokat elvesztettünk a JDK-ból, úgy mint a teljes JAXB, az Activation framework, de búcsút intettünk az SQL API-nak is. Nos ezeket nem oldja meg helyettünk a Spring és a Spring Boot sem, így a következő függésekre biztosan szükségünk lesz a folytatáshoz (a cikk írásának pillanatában stabil verziókkal):

<dependency>
	<groupId>org.glassfish.jaxb</groupId>
	<artifactId>jaxb-runtime</artifactId>
	<version>2.3.1</version>
</dependency>
 
<dependency>
	<groupId>javax.xml.bind</groupId>
	<artifactId>jaxb-api</artifactId>
	<version>2.3.1</version>
</dependency>
 
<dependency>
	<groupId>javax.activation</groupId>
	<artifactId>activation</artifactId>
	<version>1.1.1</version>
</dependency>

Ugyan elég kicsi az esélye, de előfordulhat, hogy mégsincs szükségünk a fenti függésekre, így megpróbálhatjuk elindítani ezek nélkül is az alkalmazást. Hiányuk NoClassDefFoundError-okat fog okozni, a JAXB hiánya javax.xml.bind, még az Activation Framework hiánya javax.activation osztályokra hivatkozva. Az SQL API-ból származó osztályok jelen lesznek abban az esetben, ha JDBC vagy JPA implementációt is használ az alkalmazás, ellenkező esetben azonban előfordulhat, hogy Jigsaw modulként kell hozzáadnunk az alkalmazáshoz az --add-modules java.sql direktíva használatával.

Előfordulhat továbbá, hogy javax.annotation osztályokat fog hiányolni az alkalmazás (innen származik például a @PostConstruct annotáció), ez esetben szükség lesz még az alábbi függésre is:

<dependency>
	<groupId>javax.annotation</groupId>
	<artifactId>javax.annotation-api</artifactId>
	<version>1.3.2</version>
</dependency>

Belefuthatunk továbbá az “InjectionManagerFactory not found” kivételbe, mely a szintén eltávolított HK2 implementáció miatt jelentkezik (például akkor, ha Jersey kliens található az alkalmazásunkban). Ennek orvoslására használhatjuk az alábbi függést:

<dependency>
	<groupId>org.glassfish.jersey.inject</groupId>
	<artifactId>jersey-hk2</artifactId>
	<version>2.27</version>
</dependency>

Végre indul! … vagy mégsem

Ezen a ponton már nagyjából kezd működőképessé válni az alkalmazásunk, de még távol lehet a cél, alkalmazásunk komplexitásától függően. Néhány problémába még sikerült sajnos belefutnom, így most álljon itt néhány közülük.

Embedded servlet finomhangolás

Bizonyos esetekben szükség lehet az embedded servlet container működésének tetszőleges megváltoztatására - értem ez alatt a Spring Boot által biztosított konfigurációs paramétereken túlmutató változástatások szükségességét. Mindazok, akik ilyen módosításokat használnak és nem követik aktívan a release notes-okat (így sajnos jómagam is) meglepetésben részesülhettek a 2.x verzióra történő átállás során. A korábbi “EmbeddedServletContainerCustomizer” osztály és annak minden közvetlen “hozzátartozója” előzetes deprekálás nélkül került ki a keretrendszerből, ami így persze nem forduló alkalmazást eredményezett. Szerencsére az átállás nem kifejezetten vészes művelet, a fentebb említett osztályt a WebServerFactoryCustomizer váltotta fel, az implementáció pedig szinte ugyanaz, mint korábban. Az embedded servlet containertől függően ehhez tartozni fog egy AbstractServletWebServerFactory implementáció, mely Tomcat esetében a TomcatServletWebServerFactory lesz. Az implementáció az alábbiak szerint fog kinézni:

// előtte:
public EmbeddedServletContainerCustomizer ajpContainerCustomizer(int ajpPort) {
	return configurableEmbeddedServletContainer -> {
		TomcatEmbeddedServletContainerFactory tomcat = 
			(TomcatEmbeddedServletContainerFactory) configurableEmbeddedServletContainer;
		tomcat.setProtocol(AJP_PROTOCOL);
		tomcat.setPort(ajpPort);
	};
}
 
// utána:
public class EmbeddedWebServerAJPCustomization 
		implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {
 
	private static final String AJP_PROTOCOL = "AJP/1.3";
 
	private int ajpPort;
 
	public EmbeddedWebServerAJPCustomization(int ajpPort) {
		this.ajpPort = ajpPort;
	}
 
	@Override
	public void customize(TomcatServletWebServerFactory factory) {
		factory.setProtocol(AJP_PROTOCOL);
		factory.setPort(ajpPort);
	}
}

MySQL driver betöltése közben kivétel

Ezt többnyire a most már alapértelmezett Hikari connection pool és a driver közti verzióütközés okozza. Függetlenül attól, hogy még 5.x MySQL adatbázis motort használunk-e vagy már áttértünk a 8.x-re, frissítsük a connectort 8.0+ verzióra és megszűnik a probléma.

Ehhez kapcsolódóan felmerült még egy apró probléma, mégpedig az adatbázis szerver időzónáját illetően. Előfordulhat ugyanis, hogy az engine nem tudja feldolgozni az alkalmazástól kapott időzóna azonosítót, mivel az némiképp megváltozott az új Java-ban. Ilyen esetben a connection string végére illesztve az időzóna azonosítóját a probléma megszűnik: jdbc:mysql:// … &serverTimezone=Europe/Budapest

Az alkalmazás endpointjai nem elérhetőek a korábbi context path-on

Ez annak köszönhető, hogy a 2.x Spring Boot verziókban megváltoztattak bizonyos konfigurációs paramétereket - ez esetben a server.context-path helyett kell a server.servlet.context-path paramétert használni. Érdemes megtekinteni egyébként a változásokról szóló release notes-okat, illetve az éppen aktuális dokumentációt, mivel több ilyen változtatás is történt a frameworkben, sok esetben pont az ilyen, alapvetően sokkal inkább a servlet-et mint az alkalmazás többi részét érintő konfigurációs paraméterek esetében.

Hiányzó Unsafe osztály

Az Unsafe osztály a Java-ban mindig is amolyan “ne tedd be ide a lábad” terület volt, de az általa biztosított eszközkészlet nyilván csábító volt és mind a mai napig az a fejlesztők számára. Ritkán van rá szükség, de igazából olyankor nagyon. Java 9 fölött azonban kikerült a JDK alapmoduljából, így ha mégis szüksége volna rá az alkalmazásunknak, az --add-modules jdk.unsupported kapcsolóval újra használhatóvá válik

A Spring Boot Maven plugin nem tudja installálni az executable jar-t

Érdekes “hiba”, és nem is volt egyszerű rájönni, de a megoldása annál rövidebb. A plugin executions konfigurációjában az alábbi szekciót…

<executions>
	<execution>
		<goals>
			<goal>repackage</goal>
		</goals>
		<configuration>
			<classifier>exec</classifier>
		</configuration>
	</execution>
</executions>
 
… cseréljük ki erre:
<executions>
	<execution>
		<id>repackage</id>
		<configuration>
			<classifier>exec</classifier>
		</configuration>
	</execution>
</executions>

Ezután már megfelelően generálta és installálta a plugin az executable jar-t.

Bónusz: A Mockito “illegal reflective access” hibával eltöri az összes tesztet

A Mockito 1.x verziói nem támogatják a Java 11-et, annak 2.x verzióra frissítésével a probléma eltűnik.

Modularizálás

Miután az alkalmazásunk már képes fordulni, indulni és stabilan futni Java 11 alatt, hozzákezdhetünk a Java 9-ben debütált Jigsaw modulrendszer bevezetéséhez is. Tekintve, hogy a Spring Boot 2.1 már teljesen támogatja a Java 11-et, kézenfekvőnek tűnik az elhatározás, de aztán sajnos szembesülünk a szörnyű ténnyel: a támogatás nem teljesen terjed ki a modulrendszerre. Egyszerűbb alkalmazás esetén is komoly problémákba ütközhetünk, JPA használata mellett pedig már nem is tudtam szóra bírni a modularizált alkalmazást.

Természetesen kísérletezni lehet, igazából teljesen jó dokumentációk érhetőek el a module descriptorok létrehozását illetően, azokat követve viszonylag könnyen megírhatjuk saját descriptorainkat, így belépve a Jigsaw rendszer nyújtotta “biztonságérzet” világába.

A helyzet azonban nem ennyire fényes. Mint korábban már említettem, sem a tooling, sem a framework-ök nem nőttek még fel a feladathoz, így számos problémába futhat bele az ember. A fentebb említett JPA problémát StackOverFlow-n is felvetettem (klikk ide), érdemi választ azonban nem kaptam rá - kivéve a Spring Boot egyik fejlesztőjétől kapott kommentet, aki nem értette, miért akarom modularizálni a Spring Boot alkalmazásom. Sajnos a meglátása egyébként jogos, mivel a Spring Boot Maven plugin által összeállított executable jar (sőt igazából bármilyen executable jar) nem fogja használni a module descriptorokat. Az alkalmazás ebben az esetben továbbra is classpath-tal indul, ami a legacy működésnek felel meg. A descriptorok így legfeljebb a fejlesztés alatt lehetnek hasznunkra, de jelen állapotában az is sokkal inkább csak nyűg, semmint segítség. Ha mindenképp ki szeretnénk használni a module descriptorokat, akkor a JLink nevű alkalmazás segítségével készíthetünk olyan self-containing zip csomagokat, melyek az alkalmazáson túl a JDK-t is tartalmazzák. Ilyen csomagolással van lehetőségünk module path-szal indítani az alkalmazást.

A legidegesítőbb probléma, ami előfordulhat, az a “split package” jelenség fennállása. Split package-ről beszélünk abban az esetben, ha a module descriptorok által hivatkozott modulok közül kettő vagy több ugyanolyan nevű package-eket tartalmaz. Ez esetben a modulok “átfedik egymást”, ami megnyithat a modulok között olyan láthatósági szintet, amit nem szeretnénk megengedni. Ennek megfelelően az ilyen eseteket el kell kerülni, bár a legtöbb esetben az okozza, hogy két függésből jönnek ugyanazok a package-ek és osztályok, szóval ez részben hasznos, mivel így elkerülhetőek a duplikált függések. Ha szándékos esetben csinál ilyet két különböző függés, mint például teszi azt a Mongo jelenlegi legfrissebb driver csomagja, akkor sajnos nem lesz lehetőségünk a descriptorok elhelyezésére.

Frameworkök és library-k terén tehát még súlyos hiányosságok tapasztalhatóak, de egyelőre a tooling sem sokkal jobb. Ékes példája ennek a QueryDSL fordításidejű annotáció-feldolgozója, mely egyszerűen nem látta a Mongo Document osztályait, így nem tudta generálni hozzá a megfelelő metaosztályokat. Hasonlóan rossz a helyzet a tesztek futtatására szolgáló Surefire és Failsafe pluginek terén, melyek modularizált alkalmazást csak 0-ra állított forkCount mellett hajlandóak tesztelni - egyéb esetben láthatósági problémák miatt a JVM váratlanul kilép és megáll a fordítási folyamat. A forkCount 0-ra csökkentése azonban eltöri a Jacoco-t, pontosabban a Surefire/Failsafe ez esetben nem kapja meg a megfelelő command line argumentet, így nem készül el a szükséges .exec file.

Ezek még súlyos hibák és hiányosságok, melyekre remélhetőleg hamar érkeznek majd a javítások. Addig is a modularizálást érdemes elhalasztani, mivel több fejfájást okozhatunk vele magunknak, mint amennyi előnyünk származik majd belőle.

Konklúzió

Mindenképp érdemes belevágni a Spring és Spring Boot alkalmazások Java 11-re való migrálásába, mivel mindamellett, hogy már csak ehhez a verzióhoz lesznek publikusan elérhető javítások (illetve OpenJDK 8-hoz még pár évig, de az nem pontos másolata az Oracle JDK-nak), a Java 9+ verziók új feature-eit is bátran elkezdhetjük használni. A modularizálás más kérdés, azt még érdemes hanyagolni, de előbb-utóbb remélhetőleg arra is lehetőségünk nyílik majd.

Kommentek
Hozzászólok

Még senki nem szólt hozzá ehhez a bejegyzéshez.