Od czasu, kiedy rozpoczęliśmy Projekt Bilet, minęło już trochę czasu — warto więc zaktualizować wszystkie zależności. Tym zajmiemy się w dzisiejszym (mini) wpisie.
Aktualizacje bibliotek nie są niestety zbyt częstą praktyką — w końcu taka zmiana może wymagać stosunkowo sporej ilości niedużych zmian, wymaga dokładniejszego przetestowania, a jednocześnie nie zapewnia żadnej dodatkowej funkcjonalności dla naszych użytkowników. Jeśli chodzi o wydajność, często tego typu aktualizacja nie wprowadza zmian lub są one minimalne.
Mimo wszystko warto raz na jakiś czas przeznaczyć czas na aktualizację bibliotek. Pierwszy, najbardziej namacalny powód, to kwestia zabezpieczeń. Im bardziej automatycznie coś działa, tym większe ryzyko, że oprogramowanie to zawiera błędy lub luki (można tutaj przywołać jako przykład jeden z największych wycieków danych w historii, któremu można by potencjalnie zapobiec dbając o aktualizacje bibliotek). Druga kwestia to wykorzystanie najnowszych technologii i możliwości języka — być może dzięki aktualizacji jest okazja uprościć jakąś część aplikacji lub pozbyć się kodu, który teraz jest ‚w pakiecie’. To także dobra okazja do małych porządków w kodzie — dopisania testów, podzielenia dużych klas na mniejsze itp. Przede wszystkim jednak, częsta aktualizacja wersji pozwala uniknąć ‚skokowych’ zmian (np. wyobraź sobie aktualizacje aplikacji napisanej dla Spring 2 od razu do Spring 5, gdzie zmienia się praktycznie wszystko). Dzięki temu nasza aplikacja nie stanie się ‚legacy’, którym kolejne pokolenia programistów będą się straszyć ;)
Semver
Zanim zabierzemy się za aktualizację, na początek trochę teorii. W tym wypadku — o wersjach. Jak zapewne zauważyłaś, wiele bibliotek ma wersje składającą się z dwóch lub trzech części oddzielonych kropkami. To standardowy sposób zapisu wersji, a każdy z tych członów ma specjalne znaczenie. Elementy składowe mają nazwy MAJOR.MINOR.PATCH, czyli np. wersja 1.7.19 można zapisać jako:
- 1 — Major
- 7 — Minor
- 19 — PATCH
Z założenia, pierwsza część jest liczbą powinna zostać zwiększona za każdym razem, kiedy wprowadzane są zmiany niezgodne wstecz. Tzn aplikacje, które korzystają z Twojego API/biblioteki, przestaną działać (zakładamy, że w kodzie nie zostaną wprowadzone żadne zmiany).
Druga część — także liczbowa — powinna zostać zwiększona w przypadku zmian funkcjonalności, które nie powodują problemów ze zgodnością wstecz (np. dodanie nowego API/interfejsu lub nowego, opcjonalnego pola do istniejących obiektów).
Trzecia część najczęściej zawiera liczbę oraz kwalifikator (np. 2‑beta, 1‑SNAPSHOT, 13-rc1 itp). Zwiększamy ją za każdym razem, kiedy wprowadzamy zmiany nie powodujące zmian w funkcjonalności/API (np. poprawiamy jakieś błędy, refaktorujemy kod itp).
Taki system nazywania wersji znacząco upraszcza automatyzację i porównywanie wersji (np. jest jasne, że wersja 2.5 jest nowsza od 2.4, natomiast 2.0 jest nowsza od 1.9). Standard ten, nazywany Semantic Versioning (lub Semver w skrócie), jest jednak bardziej odzwierciedleniem popularnej praktyki niż standardem do którego wszyscy się stosują — np. sufiksy ‑SNAPSHOT czy ‑RELEASE znane z Mavena są teoretycznie niezgodne ze standardem. Podobnie dwuczłonowe numer wersji (np. 1.1) — niemniej z punktu widzenia używanych narzędzi standard ten opisuje idealne założenia jeśli chodzi o wersjonowanie naszych aplikacji/bibliotek — warto się z nim przynajmniej zapoznać.
Więcej informacji znajdziesz na głównej stronie poświęconej tej konwencji — semver.org
Sprawdzanie wersji
Skoro ustaliliśmy już, jak czytać wersje, pora z tej wiedzy skorzystać. A ponieważ jesteśmy programistami, lenistwo jest naszą największą zaletą — pozwolimy więc komputerowi zająć się wszystkim za nas.
W tym celu skorzystamy z pluginu ‘versions’, a dokładniej jego celu display-dependency-updates . W katalogu projektu wywołujemy komendę
mvn org.codehaus.mojo:versions-maven-plugin:display-dependency-updates
W efekcie Maven sprawdzi dostępne wersje a następnie pokaże raport oraz informacje o możliwych aktualizacjach.
Plugin ten ma także cel dependency-updates-report , którego działanie jest właściwie identyczne, z tą różnicą że generuje wspomniany raport do pliku (znajdziesz go w katalogu {katalog_projektu}/target/site/dependency-updates-report.html) — może to być przydatne, jeśli budujesz własną infrastrukturę do budowania projektów lub ta, której używasz, jest w stanie takie raporty automatycznie analizować. Raport ten zawiera też więcej informacji (np. o wersjach w przypadku aktualizacji wersji tylko minor).
Maven 2 i ‚najnowsze’ wersje lub przedziały wersji
Zastanawiasz się pewnie, dlaczego nie można po prostu określić wersji zależności jako ‚najnowsza’ i zapomnieć o problemie, skoro Maven ‚rozumie’ kolejność wersji?
Otóż historycznie było to możliwe — w Maven 2 funkcjonowały ‚metawersje’ — zamiast podawania konkretnej wersji można było określić wersję jako ‚RELEASE’ lub ‚LATEST’ co podczas budowania było podmieniane na najnowszą wersję (uwzględniając wydania snapshot, wersje beta itp) lub najnowszą wersję z sufiksem ‑RELEASE. Ta możliwość została usunięta w wersji 3 Mavena.
Głównym powodem usunięcia była powtarzalność procesu budowania. Dość częstą praktyką, w szczególności w większych firmach i narzędziach typu CI, jest lokalny cache artefaktów. Pozwala to zaoszczędzić sporo łącza i transferu (podczas budowania biblioteki nie są pobierane z internetu tylko z lokalnego serwera) oraz czasu (pobranie z serwera w sieci lokalnej jest dużo szybsze od pobierania ze zdalnego serwera, co ma znaczący wpływ na czas trwania procesu budowania w przypadku większych projektów). Ta optymalizacja powoduje jednak że dla komputerów w różnych sieciach ‚najnowsza wersja’ może oznaczać coś innego. Przez to aplikacja (lub biblioteka) będzie działała inaczej w zależności od tego na którym serwerze została zbudowana. Powodowało to także sporo problemów z testami, które mogły zacząć zgłaszać problemy w ‚losowych’ momentach. Wszystko to powodowało, że proces wytwarzania oprogramowania przestawał być deterministyczny, co jest podstawową zaletą używania komputerów ;) Z tego powodu koncept porzucono na rzecz ścisłego określania używanych wersji.
Więcej informacji na ten temat znajdziesz w oficjalnej notce na stronie Apache Foundation.
Teoria a praktyka
W tym miejscu warto zwrócić uwagę na jeszcze jedną rzecz — prawidłowe wersjonowanie bardzo często ma miejsce w przypadku bibliotek i aplikacji dostępnych publicznie (można powiedzieć wręcz że jest to wymóg, aby aplikacja/biblioteka była traktowana w środowisku poważnie i aby inni developerzy mieli zaufanie do korzystania z niej).
Rzecz najczęściej ma się zupełnie inaczej w firmach i wewnętrznych projektach. W takim wypadku bardzo często aplikacje rozwijane są równolegle (co powoduje częste zmiany, które teoretycznie powinny generować kolejne wersje oprogramowania) oraz stale (przez co problem z niekompatybilnością zmian jest zauważany i rozwiązywany w przeciągu godzin a nie dni czy tygodni). W takim środowisku poprawne używanie numerów wersji wiązało by się z dużym nakładem pracy i czasu aby za każdym razem aktualizować wersje, przez co przeważnie (to uogólnienie — są oczywiście firmy, które nawet w wewnętrznych projektach trzymają się pewnych zasad wersjonowania) przez całe swoje życie aplikacja jest w wersji ‚1.0‑SNAPSHOT’ .
Jest to jedna z sytuacji w których obiektywnie sensowna praktyka jest po prostu nieefektywna i kosztowna, a więc także pomijana. Ma to oczywiście pewne konsekwencje, ale w przeważającej większości przypadków oszczędność czasu i wysiłku jest zdecydowanie większa niż ewentualne problemy.
Od każdej zasady występują także wyjątki — niestety są biblioteki, które w kilku przypadkach złamały schemat numeracji wersji (celowo lub przez pomyłkę). Przykładem może być biblioteka antlr — w listopadzie 2005 roku wrzucono wersję ‘20030911’. I choć od tego czasu było już kilka aktualizacji (w momencie pisania tego artykułu najnowszą dostępną wersją jest 3.0ea8), to plugin podpowiada wersję ‘20030911’ jako najnowszą (w myśl semver, wersja ta ma major ‘2030911’, która jest wyższa niż ‘3’). Z tego powodu warto zwracać uwagę, jakie faktycznie zmiany są wprowadzane (szczególnie jeśli korzystamy z automatycznego pluginu)
Zanim zaczniemy — przygotowanie do pracy z Javą 9+
Wraz ze zmianą sposobu wydawania nowych wersji języka Java, wiele organizacji szybciej wdraża najnowsze wersje. W dużej mierze nie powinno nam to robić różnicy, ponieważ Java z założenia zakłada zgodność wstecz. Wyjątkiem jest Java 9, a dokładniej sposób organizacji ‘bazowych’ komponentów. To temat na osobny wpis, ale w dużym uproszczeniu zmiana polega na tym, aby jak najbardziej ‘odchudzić’ standardowe biblioteki i nie ładowanie niepotrzebnych elementów (które stały się częścią JDK/JRE, a jednocześnie nie są powszechnie potrzebne w projektach. Jednym z przykładów mogą być biblioteki do parsowania plików XML — niewiele współczesnych projektów z nich korzysta, a jednocześnie były one częścią standardu Java EE.
Problem pojawia się jeśli nasz projekt korzysta z tych bibliotek (często nie bezpośrednio, tylko np. framework lub jakaś biblioteka) — aktualizując Javę, projekt przestanie się kompilować. W naszym przypadku problematyczna jest właśnie biblioteka do XMLi, z której korzysta testowa baza danych. Są dwie możliwości — możemy albo dodać specjalne argumenty do kompilatora Javy, albo pobrać te klasy jako zależności (nieco szerszy opis i porównanie można znaleźć w świetnej odpowiedzi na Stack Overflow ; lista zależności tutaj). Ponieważ biblioteki te zostaną usunięte w przyszłych wersjach Javy całkowicie, najbezpieczniejszą opcją jest dodanie ich jako zależności:
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.0</version>
</dependency>
a także:
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1</version>
</dependency>
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>2.3.2</version>
</dependency>
Natkniemy się też potencjalnie na kilka innych problemów, np. używana przez nas wersja Findbugs nie wspiera Javy 11 (wystarczy aktualizacja do wersji 3.1.7: https://github.com/spotbugs/spotbugs/blob/release‑3.1/CHANGELOG.md#317—2018–09-12). Warto je rozwiązać zanim przejdziemy dalej (dzięki temu problemy nie nawarstwią się i będzie nam łatwiej radzić sobie z nimi po kolei).
Krok pierwszy — aktualizowanie dodatkowych zależności
Oczywiście możemy po prostu zaktualizować wszystkie zależności do najnowszych wersji a następnie poprawić wszystkie znalezione błędy. To może być szybsza metoda w małych projektach, które nie zdążyły się za bardzo ‘zestarzeć’, ale nie sprawdzi się za dobrze w większych i bardziej złożonych projektach.
Dodatkowe zależności to wszystkie biblioteki, które nie są zarządzane przez nasz parent pom (w tym wypadku takie, które nie są zarządzanie przez Spring Boot). Do tej grupy często zaliczają się mniejsze biblioteki, które nie są na tyle popularne aby dołączać je do np. Spring Framework. W ich wypadku najczęściej projekt po aktualizacji buduje się, ale mogą pojawić się błędy logiczne. Dlatego ważne jest, aby uruchomić wszystkie możliwe testy, a także odpalić aplikację i ‘przeklikać’ wszystkie możliwe do sprawdzenia scenariusze.
Tutaj posłużymy się wygenerowanym wcześniej podsumowaniem — będzie on bardzo długi, ponieważ zawiera także wszystkie zależności zarządzane przez Spring Boot (te znajdziemy pod nagłówkiem ‘The following dependencies in Dependency Management have newer versions’; w przypadku raportu generowanego do pliku jest to sekcja ‘Dependency Management’). Interesujące nas zależności w sekcji ‘The following dependencies in Dependencies have newer versions’ (w przypadku raportu w pliku jest to ‘Dependencies’), gdzie znajdziemy informacje o możliwej aktualizacji naszej testowej bazy danych. Aktualizację numeru wersji możemy wykonać ręcznie, albo automatycznie za pomocą komendy
mvn org.codehaus.mojo:versions-maven-plugin:use-latest-releases
Ten goal ma wiele użytecznych opcji, np. taką, która pozwala na aktualizację tylko wersji minor (jeśli dostępna). Więcej informacji znajdziesz w dokumentacji.
Warto także sprawdzić czy są dostępne nowsze wersje używanych przez nas pluginów i w razie potrzeby je zaktualizować:
mvn org.codehaus.mojo:versions-maven-plugin:display-plugin-updates
Krok drugi — aktualizacja parent POMa (minor releases)
Kolejnym krokiem jest aktualizacja parent POMa — w naszym przypadku wersji Spring Boot. Ten proces rozbijemy jednak na dwa etapy — aktualizację wersji minor oraz aktualizację wersji major.
W tym celu niestety nie możemy skorzystać z tego samego pluginu, i jego celu ‘update-parent’ — ten cel nie pozwala na ograniczenie zmian wersji tylko ‘minor’, przez co tę zmianę musimy wykonać ręcznie. Dostępne wersje możemy sprawdzić w centralnym repozytorium lub jednej z bliźniaczych stron. W naszym przypadku najnowszą dostępną wersją 1.x jest 1.5.19.RELEASE — i z tej wersji skorzystamy.
Ten krok przynajmniej w teorii nie powinien spowodować żadnych zmian funkcjonalnych. Frameworki często bardzo dbają o to, aby numeracja wersji była zgodna z założeniami semver, więc standardowy zestaw testów jednostkowych i integracyjnych powinien wystarczyć, aby upewnić się, że aplikacja działa tak jak oczekujemy (to nie znaczy, że ni warto przetestować aplikacji dokładniej! Te kilka godzin pracy może oszczędzić Ci masę potencjalnych problemów, gdyby coś jednak nie działało).
To także dobry moment na wrzucenie zmian ‘pośrednich’ na repozytorium oraz potencjalnie na środowisko produkcyjne — kolejny krok jest znacznie bardziej ryzykowny, ale też potencjalnie czasochłonny (zarówno z uwagi na dostosowanie kodu jak i rozszerzone testowanie).
Krok trzeci — aktualizacja parent POMa (major releases)
Ostatni krok to ‘grande finale’ naszych aktualizacji — aktualizacja major release. Oczywiście nie zawsze ten krok jest konieczny — czasem aktualizując wersję minor, osiągniemy najnowszą dostępną. W naszym przypadku tak nie jest — najnowszą wersją Spring Boot jest na chwilę obecną 2.1.3.RELEASE, a więc zakasujemy rękawy, parzymy kawę i zabieramy się do pracy.
Pierwszą rzeczą, którą chcemy sprawdzić, to jakie zmiany trzeba wprowadzić. Jest (mała) szansa na to, że aktualizacja nie zmienia rzeczy, z których korzystał nasz projekt (tak było np. w przypadku wielu ‘standardowych’ projektów aktualizowanych ze Springa 3 do 4). Niestety, w naszym przypadku nie mamy takiego szczęścia i projekt nawet się nie skompiluje. (Ważne! To, że projekt po aktualizacji się skompiluje, nie znaczy, że działa! Koniecznie uruchom go lokalnie, ‘przeklikaj’ wszystkie ścieżki i spróbuj wywołać błędy np wysyłając w zapytaniu nieprawidłowe dane — testowanie zawsze powinno objąć także ścieżki negatywne!). Zaczynamy od najważniejszego (kawy), po czym zaczynamy szukać w sieci. Kilka informacji szczególnie przydatnych podczas migracji:
- Oficjalny tutorial na temat migracji Spring Boot do wersji 2
- Oficjalny tutorial na temat migracji Spring Framework do wersji 5
Każda aplikacja jest inna, i kroki opisane poniżej mogą nie wystarczyć do migracji innej aplikacji. Są one jednak dobrym punktem startu, a powyższe linki powinny pomóc z innymi problemami.
Pierwsza zmiana to usunięcie stałej SecurityProperties.ACCESS_OVERRIDE_ORDER, którą używaliśmy w naszym SecurityConfig’u. Ponieważ nie była to krytyczna linia, możemy się jej w całości pozbyć.
Kolejna zmiana to usunięcie spring-security-oauth2 z zarządzanych zależności. Wystarczy dodać konkretny numer wersji, w naszym przypadku zamieniając:
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
</dependency>
na:
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.3.5.RELEASE</version>
</dependency>
co powinno rozwiązać wszystkie problemy z budowaniem projektu. Niemniej Spring Security 5 wprowadził jeszcze jedną ważną zmianę, a mianowicie wprowadzając nowy sposób przechowywania haseł (zmiana domyślnej implementacji PasswordEncoder) — więcej na ten temat znajdziesz w release note. Z naszego punktu widzenia oznacza to trzy rzeczy:
- musimy zmienić implementację wg opisu w release note w naszym SecurityConfig
- zmienić sposób zapisu wszystkich haseł i sekretów wg nowego sposobu, tzn:
- w bazie danych musimy zmienić wszystkie hashe haseł dodając na początku ‘{bcrypt}’ (bez apostrofów)
- a także w klasie Oauth2AuthServerConfig podać frontendClientSecret z prefiksem ‘{noop}’ — w przeciwnym razie nasze zapytania o tokeny OAuth będą generowały dziwne błędy (możemy to zrobić zarówno zmieniając wartość w konfiguracji, jak i dodając prefiks w samym kodzie — konfiguracja oczywiście pozwala na łatwiejsze modyfikacje w przyszłości).
Zmianie uległy także domyślne ścieżki w Spring Actuator — zamiast /health mamy teraz /actuator/health. Stosowną zmianę musimy wprowadzić zarówno w naszych testach integracyjnych (WebappApplicationIT) jak i w konfiguracji zabezpieczeń (SecurityConfig). W tej wersji Actuator sprawdza także działanie ElasticSearch (który mamy dodany w zależnościach, ale do tej pory go nie skonfigurowaliśmy) co spowoduje problem z testami integracyjnymi. Na ten moment usuniemy tą zależność (i dodamy ją ponownie, kiedy będziemy z tej funkcjonalności korzystać).
Ostatnia zmiana w naszej aplikacji związana jest ze zmianą nazw kluczy konfiguracji dla liquibase — a dokładniej dodaniu do nich przedrostka ‘spring.’
Podsumowanie
Aktualizacja wersji jest niezwykle ważna z kilku powodów — przede wszystkim pozwala uniknąć problemów i znanych luk bezpieczeństwa (co może doprowadzić do tak spektakularnych problemów jak wyciek danych z firmy Equifax w 2017 roku). Regularne aktualizacje pozwalają też na ‘sprzątanie’ i poprawki kodu małymi krokami wraz z każdą nową wersją zamiast wielu poprawek koniecznych przy aktualizacji o kilka wersji. Cały proces nie jest trudny — co najwyżej żmudny i często wymagający czytania Release notes oraz szukania rozwiązań po różnych forach.
Kod źródłowy
Kody źródłowe są dostępne w serwisie GitHub — użyj przycisków po prawej aby pobrać lub przejrzeć kod do tego modułu. Jeśli masz wątpliwości, jak posługiwać się Git’em, instrukcje i linki znajdziesz w naszym wpisie na temat Git’a.
Jeśli uważasz powyższą lekcję za przydatną, mamy małą prośbę: polub nasz fanpage. Dzięki temu będziesz zawsze na bieżąco z nowymi treściami na blogu ( i oczywiście, z nowymi częściami kursu Javy). Dzięki!