Skip to main content

Explain BOM Bill of Materials

Oczywiście! Wyjaśnię Ci koncepcję Maven BOM (Bill of Materials) w sposób bardzo szczegółowy i praktyczny. Po przeczytaniu tego będziesz dokładnie wiedział, czym jest, po co go używać i jak zaimplementować go w swoim projekcie Java.

Zacznijmy od metafory, która idealnie oddaje sens BOM.

Metafora: Lista części z IKEA​

Wyobraź sobie, że składasz skomplikowany mebel z IKEA, np. szafę PAX z drzwiami, szufladami i oświetleniem. IKEA dostarcza Ci jedną, główną instrukcję, a w niej listę wszystkich niezbędnych części (Bill of Materials). Ta lista gwarantuje, że:

  1. Masz wszystko, co potrzebne: Nie musisz się martwić, że zapomniałeś o jakiejś śrubce.
  2. Wszystko do siebie pasuje: Śrubka A ma idealną długość do półki B, a zawias C pasuje do drzwi D. IKEA przetestowała tę konfigurację i gwarantuje, że zadziała.

Teraz wyobraź sobie, że musisz samemu skompletować te części w sklepie budowlanym. Mógłbyś wziąć śrubki, które są za krótkie, zawiasy, które nie pasują do otworów, albo uchwyty w innym odcieniu. Efekt? Frustracja i niestabilny mebel.

Maven BOM jest dokładnie taką "listą części" dla Twojego projektu.


Co to jest Maven BOM (Bill of Materials)?​

Maven BOM to specjalny rodzaj pliku pom.xml. Jego jedynym celem jest deklarowanie spójnego i przetestowanego zestawu wersji dla grupy powiązanych ze sobą bibliotek.

Co ważne, sam plik BOM:

  • Nie zawiera żadnych zależności w sekcji <dependencies> (nie dodaje żadnych bibliotek do Twojego projektu).
  • Nie zawiera kodu źródłowego ani skompilowanych plików .jar.
  • Jego sercem jest sekcja <dependencyManagement>.

Problem, który rozwiązuje BOM: "Piekło Zależności" (Dependency Hell)​

Bez BOM, zarządzanie zależnościami w średnim lub dużym projekcie szybko staje się koszmarem.

Scenariusz 1: Ręczne zarządzanie wersjami Masz projekt Spring Boot i dodajesz zależności:

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.7.5</version> <!-- Wersja 1 -->
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>2.7.5</version> <!-- Wersja 2 -->
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.7.4</version> <!-- Ups! Pomyłka! -->
</dependency>
</dependencies>

Problemy:

  1. Powtarzalność: Musisz wpisywać wersję przy każdej zależności.
  2. Ryzyko pomyłki: Łatwo o literówkę lub wklejenie złej wersji, co prowadzi do niekompatybilności.
  3. Trudne aktualizacje: Chcesz zaktualizować Spring Boot do wersji 3.0? Musisz ręcznie zmienić wersję w kilkunastu miejscach, modląc się, żebyś o żadnym nie zapomniał.

Scenariusz 2: Konflikty wersji przechodnich (transitive dependencies) Biblioteka A wymaga biblioteki C w wersji 1.0. Ty dodajesz do projektu bibliotekę B, która wymaga biblioteki C w wersji 2.0. Maven musi wybrać jedną wersję (zazwyczaj "najbliższą" w drzewie zależności), co może spowodować, że biblioteka A przestanie działać.

BOM rozwiązuje oba te problemy.


Jak to działa w praktyce? Krok po kroku​

Kluczem do zrozumienia BOM są dwa elementy w pliku pom.xml: sekcja <dependencyManagement> i scope <scope>import</scope>.

1. Sekcja <dependencyManagement>​

Ta sekcja wygląda podobnie do <dependencies>, ale działa inaczej.

  • <dependencies>: "Mój projekt POTRZEBUJE tej biblioteki". Maven ją pobierze i doda do classpath.
  • <dependencyManagement>: "Jeśli mój projekt (lub jego moduły) zdecyduje się użyć tej biblioteki, TO MA UŻYĆ DOKŁADNIE TEJ WERSJI". Jest to tylko rekomendacja, zalecenie. Sama deklaracja w dependencyManagement niczego nie dodaje do projektu.

2. Importowanie BOM​

Zamiast ręcznie przepisywać całą sekcję <dependencyManagement> z BOM-a, po prostu go "importujemy". Robi się to w pom.xml Twojego projektu.

<project>
...
<dependencyManagement>
<dependencies>
<!-- Tutaj importujemy BOM od Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.7.5</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
...
</project>

Przeanalizujmy to:

  • groupId, artifactId, version: Standardowe koordynaty BOM-a. W tym przypadku jest to oficjalny BOM od Spring Boot w wersji 2.7.5.
  • <type>pom</type>: Kluczowe! Mówimy Mavenowi, że to, co importujemy, to nie jest plik .jar do uruchomienia, ale plik pom.xml do przeanalizowania.
  • <scope>import</scope>: To jest magia! Ten scope mówi Mavenowi: "Weź całą sekcję <dependencyManagement> z pliku spring-boot-dependencies-2.7.5.pom i wklej ją do sekcji <dependencyManagement> mojego projektu".

3. Korzystanie z zaimportowanych wersji​

Po zaimportowaniu BOM, Twój pom.xml staje się niezwykle czysty. Kiedy dodajesz zależności, nie musisz już podawać ich wersji:

<dependencies>
<!-- Nie ma potrzeby podawania wersji! -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- Tutaj też nie! -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<!-- I tutaj! -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

<!-- BOM zarządza też wersjami bibliotek trzecich, np. Jackson -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>

Co się dzieje, gdy Maven buduje projekt?

  1. Maven widzi zależność spring-boot-starter-web bez wersji.
  2. Sprawdza sekcję <dependencyManagement> w Twoim projekcie.
  3. Znajduje tam wpisy zaimportowane z spring-boot-dependencies.
  4. Odnajduje wpis dla spring-boot-starter-web i widzi, że rekomendowana wersja to 2.7.5.
  5. Używa tej wersji.

To samo stanie się dla jackson-databind i setek innych bibliotek, których wersje są zdefiniowane w BOM-ie Spring Boot.

Praktyczny przykład: Jak to zrobić w projekcie​

Załóżmy, że tworzysz nowy projekt Maven.

Krok 1: Znajdź odpowiedni BOM Praktycznie każdy duży framework (Spring, Quarkus, Jakarta EE, AWS SDK) dostarcza swój BOM. Zazwyczaj nazywa się on {nazwa-frameworka}-dependencies lub {nazwa-frameworka}-bom.

Krok 2: Dodaj BOM do dependencyManagement w swoim pom.xml

<project ...>
<modelVersion>4.0.0</modelVersion>
<groupId>com.mojafirma</groupId>
<artifactId>moj-super-projekt</artifactId>
<version>1.0.0-SNAPSHOT</version>

<!-- To jest dobry zwyczaj: definiowanie wersji jako property -->
<properties>
<java.version>17</java.version>
<spring-boot.version>2.7.5</spring-boot.version>
</properties>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<!-- Teraz dodajesz zależności bez wersji -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<!-- Wersja pluginu też może być zarządzana przez BOM! -->
<!-- Ale często jest dziedziczona z parent POM, np. spring-boot-starter-parent -->
</plugin>
</plugins>
</build>
</project>

Krok 3: Aktualizacja projektu Gdy wyjdzie Spring Boot 2.7.6, jedyne co musisz zrobić, to zmienić jedną linijkę:

<properties>
...
<spring-boot.version>2.7.6</spring-boot.version>
</properties>

Po tej zmianie i przebudowaniu projektu (mvn clean install), wszystkie Twoje zależności zarządzane przez ten BOM zostaną automatycznie zaktualizowane do wersji kompatybilnych z 2.7.6.


Co jeśli chcę użyć innej wersji niż ta z BOM?​

Czasami musisz użyć konkretnej wersji biblioteki, innej niż rekomendowana w BOM (np. z powodu krytycznej poprawki bezpieczeństwa). Maven na to pozwala.

Zasada jest prosta: najbliższa definicja wygrywa.

Jeśli zadeklarujesz wersję bezpośrednio w swojej sekcji <dependencies>, ta wersja nadpisze wersję z <dependencyManagement>.

<dependencies>
<!-- Ta zależność użyje wersji z BOM (np. 2.13.4) -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>

<!-- Ale dla tej zależności JA decyduję o wersji, ignorując BOM -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.14.0</version> <!-- Moja wersja! -->
</dependency>
</dependencies>

To daje elastyczność, ale używaj tego ostrożnie, ponieważ sam bierzesz na siebie odpowiedzialność za ewentualne konflikty.


Kluczowe korzyści w pigułce​

  1. Centralne zarządzanie wersjami: Jedno miejsce do aktualizacji całego stosu technologicznego.
  2. Gwarancja kompatybilności: Używasz zestawu bibliotek, który został przetestowany przez twórców frameworka.
  3. Czysty i czytelny pom.xml: Brak zaśmiecania pliku tagami <version>.
  4. Spójność w projektach wielomodułowych: Jeśli masz projekt składający się z wielu modułów, importujesz ten sam BOM w nadrzędnym pom.xml, a wszystkie podmoduły automatycznie dziedziczą spójne wersje.
  5. Unikanie konfliktów: Znacząco redukuje ryzyko problemów z zależnościami przechodnimi.

Podsumowanie​

Myśl o Maven BOM jako o kontrakcie na wersje. Importując go, zgadzasz się na używanie zestawu wersji, który ktoś (np. zespół Springa) przygotował i przetestował za Ciebie. To fundamentalna i niezwykle ważna technika w profesjonalnym tworzeniu oprogramowania w Javie, która czyni Twoje buildy stabilniejszymi, łatwiejszymi w utrzymaniu i bardziej przewidywalnymi.