Starter & BOM — tại sao Spring Boot hết dependency hell
Starter là jar rỗng gom transitive dependency; BOM spring-boot-dependencies quản version đồng bộ 200+ thư viện. Bài này giải thích cơ chế vật lý của starter, cách BOM giải bài toán version conflict, bóc spring-boot-starter-web thành từng jar, và excludes tinh chỉnh stack.
TL;DR: Starter là jar rỗng class — chỉ chứa pom.xml liệt kê transitive dependencies. spring-boot-starter-web kéo theo ~30 jar: Tomcat embedded, Jackson, Spring MVC, logging. BOM (spring-boot-dependencies) là một pom file chứa dependencyManagement định nghĩa version cho 200+ thư viện; Pivotal test mọi tổ hợp tương thích trước khi release. Bạn extends BOM qua spring-boot-starter-parent (1 dòng) hoặc import scope — không bao giờ khai <version> cho dependency Spring/Boot. Excludes cho phép hoán đổi Tomcat lấy Jetty mà không đổi code. Tất cả cơ chế này xoay quanh một bài toán cũ: dependency hell version conflict của Spring 4 era.
Bài trước (Spring Boot giải quyết gì) đặt tên 5 trụ cột của Boot. Bài này đào sâu trụ cột đầu tiên: Starter Dependencies — cụ thể là chúng hoạt động ra sao ở mức Maven/Gradle, và tại sao cơ chế BOM tồn tại.
1. Bài toán: dependency hell của Spring 4
Trước Spring Boot, thêm Spring MVC vào project đồng nghĩa với nghi lễ tra cứu version mỗi lần:
<!-- Spring 4 era -- phai tu chon version tuong thich -->
<dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>4.3.9.RELEASE</version></dependency>
<dependency><groupId>org.springframework</groupId><artifactId>spring-web</artifactId><version>4.3.9.RELEASE</version></dependency>
<dependency><groupId>org.springframework</groupId><artifactId>spring-webmvc</artifactId><version>4.3.9.RELEASE</version></dependency>
<dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-databind</artifactId><version>2.9.0</version></dependency>
<dependency><groupId>ch.qos.logback</groupId><artifactId>logback-classic</artifactId><version>1.2.3</version></dependency>
<!-- ... 10-15 dependency nua, moi dependency tu chon version -->
Câu hỏi mà mọi developer phải tra tay: jackson-databind 2.9.0 có tương thích với spring-webmvc 4.3.9 không? logback 1.2.3 có conflict với slf4j version nào? Mỗi lần upgrade Spring là phải tra lại toàn bộ bảng tương thích — hay chờ đến khi runtime explode.
Spring Boot 1.0 (2014) giải bài toán này bằng hai cơ chế kết hợp: starter (gom dependency) + BOM (quản version). Hiểu cơ chế vật lý của chúng giải thích tại sao pom.xml Boot chỉ cần 3-4 dòng mà đầy đủ.
2. Starter là gì — về mặt vật lý
Mở Maven Central, tải spring-boot-starter-web-3.4.0.jar, unzip ra:
spring-boot-starter-web-3.4.0.jar
├── META-INF/
│ ├── MANIFEST.MF
│ ├── maven/org.springframework.boot/spring-boot-starter-web/
│ │ ├── pom.properties
│ │ └── pom.xml <-- file duy nhat quan trong
│ └── spring.provides
└── (khong co .class file nao)
Jar hoàn toàn rỗng class. Nội dung duy nhất có ý nghĩa là pom.xml bên trong:
<project>
<artifactId>spring-boot-starter-web</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
</dependency>
</dependencies>
</project>
Không có <version> ở bất kỳ đâu. Mục đích duy nhất của starter: liệt kê transitive dependencies cần thiết cho một tính năng, để bạn chỉ khai một dòng thay vì 15 dòng.
Khi Maven thấy spring-boot-starter-web trong project của bạn, nó download jar nhỏ này (~5KB), đọc pom.xml bên trong, recurse pull từng transitive dependency. Mỗi transitive lại pull tiếp:
flowchart TB SBW["spring-boot-starter-web"] SB["spring-boot-starter<br/>(core + logging + autoconfig)"] SBJ["spring-boot-starter-json<br/>(Jackson)"] SBT["spring-boot-starter-tomcat<br/>(Tomcat embedded)"] SWMVC["spring-webmvc<br/>(DispatcherServlet)"] SBW --> SB SBW --> SBJ SBW --> SBT SBW --> SWMVC SB --> Core["spring-boot<br/>spring-boot-autoconfigure<br/>logback-classic<br/>snakeyaml"] SBJ --> Jackson["jackson-databind<br/>jackson-datatype-jsr310<br/>jackson-datatype-jdk8"] SBT --> Tomcat["tomcat-embed-core<br/>tomcat-embed-websocket"] SWMVC --> MVC["spring-aop<br/>spring-context<br/>spring-expression<br/>spring-web"]
Kết quả: ~30 transitive jar từ một dòng khai báo. Chạy mvn dependency:tree để xem toàn bộ cây.
Starter không "tự làm gì". Nó chỉ là file pom.xml đóng gói thành jar để Maven biết cần pull dependency nào. Auto-configuration là cơ chế riêng biệt — xem bài 03.
3. BOM — tại sao version không bị conflict
Version từ đâu? pom.xml của starter không khai <version>. Câu trả lời: từ BOM (Bill of Materials) — spring-boot-dependencies.
BOM là một pom file chỉ chứa <dependencyManagement>, không có <dependencies>. Nó định nghĩa version cho thư viện mà không pull thư viện đó vào classpath.
<!-- spring-boot-dependencies-3.4.0.pom (rut gon) -->
<project>
<artifactId>spring-boot-dependencies</artifactId>
<packaging>pom</packaging>
<properties>
<spring-framework.version>6.2.0</spring-framework.version>
<hibernate.version>6.6.3.Final</hibernate.version>
<jackson-bom.version>2.18.1</jackson-bom.version>
<tomcat.version>10.1.33</tomcat.version>
<hikaricp.version>5.1.0</hikaricp.version>
<logback.version>1.5.12</logback.version>
<!-- 200+ properties -->
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring-framework.version}</version>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>${hikaricp.version}</version>
</dependency>
<!-- 600+ entries -->
</dependencies>
</dependencyManagement>
</project>
Cơ chế hoạt động: khi Maven resolve version cho spring-context, nó leo lên cây POM tìm <dependencyManagement>. Nếu BOM được import, Maven đọc version từ BOM thay vì đòi bạn khai báo. Pivotal đã test tất cả combination trước khi phát hành Boot 3.4.0 — không phải bạn.
flowchart LR Proj["pom.xml cua ban<br/>(khai starter, khong co version)"] BOM["spring-boot-dependencies<br/>(BOM: 600+ version entries)"] Maven["Maven resolver"] Classpath["Classpath hop le<br/>(version da duoc Pivotal test)"] Proj -->|"declare starter"| Maven BOM -->|"provide version"| Maven Maven -->|"resolve + download"| Classpath
3.1 Hai cách extend BOM
Cách 1 — spring-boot-starter-parent (phổ biến nhất):
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.0</version>
</parent>
spring-boot-starter-parent là một pom inherit từ spring-boot-dependencies và cộng thêm: Java version mặc định (17), UTF-8 encoding, Maven plugin defaults (Surefire, Failsafe), resource filtering. Một dòng <parent> thiết lập toàn bộ project.
Hạn chế: Maven chỉ cho phép một <parent>. Nếu team đã có acme-parent cho enterprise lib management, không dùng được.
Cách 2 — import BOM trực tiếp (khi đã có parent khác):
<parent>
<groupId>com.acme</groupId>
<artifactId>acme-parent</artifactId>
<version>1.0</version>
</parent>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.4.0</version>
<type>pom</type>
<scope>import</scope> <!-- import dependencyManagement cua BOM -->
</dependency>
</dependencies>
</dependencyManagement>
scope=import + type=pom ra lệnh cho Maven: "nhập toàn bộ <dependencyManagement> từ BOM này vào đây". Version từ BOM apply như parent, nhưng bạn phải tự config Java version, encoding, plugin.
3.2 Override version khi cần thiết
BOM không khoá cứng version. Bạn override được qua property:
<properties>
<!-- Override hibernate xuong 6.5 vi bug specific trong 6.6 -->
<hibernate.version>6.5.0.Final</hibernate.version>
</properties>
Property ${hibernate.version} được BOM dùng cho mọi module Hibernate — override một property đồng bộ tất cả. Nhưng khi override, bạn tự nhận trách nhiệm test compat: Pivotal không guarantee, và bug report có thể bị reject vì "version mismatch".
Sai lầm phổ biến nhất trong project Boot mới: khai <version> explicit cho starter. Override version starter = phá vỡ đồng bộ toàn bộ BOM ecosystem, dễ gây mismatch runtime.
4. Bóc spring-boot-starter-web — role từng jar
Đi qua từng nhóm transitive dependency để hiểu role:
| Nhóm | Jar chính | Vai trò |
|---|---|---|
spring-boot-starter | spring-boot, spring-boot-autoconfigure, logback-classic, snakeyaml | Bootstrap, auto-config engine, logging mặc định, parse YAML |
spring-boot-starter-json | jackson-databind, jackson-datatype-jsr310, jackson-datatype-jdk8 | JSON serialization; jsr310 cần cho LocalDate/ZonedDateTime |
spring-boot-starter-tomcat | tomcat-embed-core, tomcat-embed-websocket | Servlet container nhúng — chạy app không cần deploy WAR |
spring-webmvc | spring-aop, spring-context, spring-expression, spring-web | DispatcherServlet, @Controller, HandlerMapping, MessageConverter |
Điểm đáng chú ý: spring-boot-starter (không suffix) xuất hiện trong mọi starter khác. Nó là core — mang lại SpringApplication, auto-configuration engine (spring-boot-autoconfigure với 143 class autoconfig), và logging stack mặc định (Logback + bridge cho Log4j/JUL).
5. Excludes — hoán đổi thành phần
Starter-web pull Tomcat mặc định. Muốn dùng Jetty thay thế:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId> <!-- replacement -->
</dependency>
Boot auto-configuration detect Jetty trên classpath, tạo JettyServletWebServerFactory thay TomcatServletWebServerFactory. Không sửa một dòng code. Cơ chế này (tại sao autoconfig detect được) là chủ đề của bài 03.
Pattern tương tự cho logging: exclude spring-boot-starter-logging, add spring-boot-starter-log4j2.
Exclude Tomcat mà không add embedded server thay thế: Maven build thành công, nhưng khi SpringApplication.run() chạy, Boot không tìm thấy ServletWebServerFactory nên throw NoSuchBeanDefinitionException. Build pass, runtime crash.
Cơ chế bên dưới — tại sao BOM giải được version hell
Vấn đề gốc của dependency hell: Maven nearest-wins algorithm khi có multiple version conflict. Nếu dependency A yêu cầu jackson 2.14, dependency B yêu cầu jackson 2.18, Maven chọn version nào gần hơn trong cây — không nhất thiết là version tương thích nhất.
BOM giải bằng cách áp đặt version vào tầng <dependencyManagement>. Maven đọc <dependencyManagement> trước khi resolve conflict — nên version trong BOM luôn thắng nearest-wins, không phụ thuộc cây transitive dependency.
flowchart TB Conflict["Conflict: dependency-A muon jackson 2.14<br/>dependency-B muon jackson 2.18"] BOM["BOM khai jackson 2.18.1<br/>trong dependencyManagement"] Result["Maven chon 2.18.1<br/>(BOM override nearest-wins)"] Pivotal["Pivotal da test 2.18.1<br/>hop le voi spring-webmvc 6.2"] Conflict --> BOM BOM --> Result Result --> Pivotal
Đây chính là lý do BOM tồn tại: thay vì mỗi developer tra tay bảng compat, Pivotal centralize test + publish result dưới dạng dependencyManagement. Bạn "subscribe" vào tested combination đó bằng một dòng <parent> hoặc BOM import.
Pitfall
Nhầm 1 — Khai <version> explicit cho starter trong Boot project:
<!-- SAI -- override BOM, pha von ecosystem -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.2.0</version>
</dependency>
Kết quả: starter 3.2 pull transitive dependency của 3.2, trong khi BOM đang định nghĩa version cho 3.4. Mismatch xảy ra silent — đôi khi chỉ manifest ở runtime với ClassNotFoundException hoặc IncompatibleClassChangeError.
Nhầm 2 — Nhầm starter với auto-configuration:
Starter là cú pháp gom dependency (Maven mechanic). Auto-configuration là cơ chế tạo bean (Spring mechanic — @Conditional, AutoConfiguration.imports). Hai thứ độc lập: bạn có thể có starter mà không có autoconfig (lib thuần), hoặc có autoconfig mà không cần starter. Xem bài 03 — Auto-configuration deep dive để phân biệt rõ.
Liên hệ các bài khác
- Bài 01 — Spring Boot giải quyết gì: bài này đặt tên 5 trụ cột; bài hiện tại đào sâu trụ cột đầu (Starter Dependencies) ở mức cơ chế vật lý Maven/Gradle.
- Bài 03 — Auto-configuration deep dive: starter pull jar vào classpath → auto-configuration đọc classpath, tạo bean tương ứng. Hai cơ chế nối tiếp nhau — hiểu starter trước, hiểu autoconfig sau.
- Bài 04 — Externalized configuration:
snakeyamltrongspring-boot-starterlà thứ parseapplication.yml;@ConfigurationPropertiesbind property vào bean — bài 04 mổ xẻ cơ chế đó.
Tóm tắt
- Starter là jar rỗng class, chỉ chứa
pom.xmlliệt kê transitive dependency. Không có Java code, không có magic. - BOM
spring-boot-dependenciesđịnh nghĩa version cho 200+ thư viện trong<dependencyManagement>— override Maven nearest-wins, centralize tested combination. - Extend BOM qua
spring-boot-starter-parent(1 dòng, full inherit) hoặcimport scope(khi đã có corporate parent). - Không khai
<version>cho dependency Spring/Boot — để BOM resolve. Override có thể nhưng tự nhận rủi ro compat. spring-boot-starter-webkéo theo ~30 jar: Tomcat, Jackson (kèm jsr310 module), Spring MVC, auto-config engine, Logback.- Excludes + add starter thay thế = hoán đổi thành phần (Tomcat → Jetty) mà không sửa code.
- BOM tồn tại vì bài toán cụ thể: version hell Spring 4 era — developer phải tự tra compat table. Boot centralize test combination, publish dưới dạng BOM.
Tự kiểm tra
Q1Bạn tải spring-boot-starter-web-3.4.0.jar và thấy không có file .class nào. Vì sao? Cơ chế nào giúp Maven biết phải pull dependency nào?▸
spring-boot-starter-web-3.4.0.jar và thấy không có file .class nào. Vì sao? Cơ chế nào giúp Maven biết phải pull dependency nào?Starter là jar rỗng class — chỉ chứa META-INF/maven/.../pom.xml bên trong. File pom.xml này liệt kê transitive dependencies (starter-json, starter-tomcat, spring-webmvc...) mà không có <version>.
Khi Maven thấy starter trong project, nó download jar nhỏ (~5KB), đọc pom.xml bên trong, rồi recurse pull từng transitive dependency liệt kê. Version được resolve từ BOM spring-boot-dependencies trong <dependencyManagement>.
Kết quả: một dòng khai báo starter → ~30 jar transitive đúng version, đã được Pivotal test tương thích. Đây là toàn bộ cơ chế — không phải Spring magic, chỉ là Maven dependency resolution + POM packaging.
Q2BOM spring-boot-dependencies khác gì spring-boot-starter-parent? Khi nào bạn buộc phải dùng BOM import thay vì <parent>?▸
spring-boot-dependencies khác gì spring-boot-starter-parent? Khi nào bạn buộc phải dùng BOM import thay vì <parent>?spring-boot-dependencies là BOM thuần — chỉ chứa <dependencyManagement> định nghĩa version 200+ lib. Không pull dependency nào, không cấu hình gì thêm.
spring-boot-starter-parent là parent pom inherit từ BOM đó và cộng thêm: Java 17 default, UTF-8 encoding, plugin defaults (Surefire, Failsafe), resource filtering. Dùng qua <parent>.
Buộc phải dùng BOM import khi project đã có parent khác (vd corporate acme-parent): Maven chỉ cho phép một <parent>. Giải pháp: giữ corporate parent, import BOM qua scope=import type=pom trong <dependencyManagement>. Trade-off: phải tự cấu hình Java version, plugin defaults mà starter-parent làm hộ.
Q3Tại sao BOM giải được "dependency hell version conflict" mà Maven nearest-wins algorithm gây ra? Giải thích theo cơ chế <dependencyManagement>.▸
<dependencyManagement>.Maven nearest-wins chọn version gần nhất trong dependency tree khi conflict. Nếu dependency A muốn jackson 2.14, dependency B muốn jackson 2.18, Maven chọn version của dependency gần root hơn — không nhất thiết là version tương thích với toàn bộ ecosystem.
BOM giải bằng cách đặt version vào <dependencyManagement>. Maven đọc <dependencyManagement> trước khi chạy nearest-wins: nếu một lib đã có entry trong <dependencyManagement>, version đó thắng bất kỳ conflict nào trong cây dependency. BOM không bị override bởi nearest-wins.
Pivotal test mọi combination (Spring MVC 6.2 + Jackson 2.18.1 + Tomcat 10.1.33 + ...) trước khi release Boot 3.4.0, publish kết quả dưới dạng <dependencyManagement> trong BOM. Bạn "subscribe" vào tested combination đó bằng một dòng <parent> — không còn tra tay bảng compat nữa.
Q4Bạn cần đổi từ Tomcat sang Jetty trong Boot app. Viết đoạn XML cần thiết và giải thích vì sao không cần sửa code Java.▸
Exclude spring-boot-starter-tomcat từ spring-boot-starter-web và add spring-boot-starter-jetty:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>Không cần sửa code Java vì Boot auto-configuration detect classpath: khi JettyServletWebServerFactory có trên classpath (do starter-jetty pull vào) và TomcatServletWebServerFactory vắng mặt, autoconfig chọn Jetty để tạo embedded server. Business code không hề biết đến sự tồn tại của Jetty hay Tomcat — chỉ tương tác qua Servlet API abstraction.
Lưu ý: exclude Tomcat mà không add replacement → Boot không tìm thấy ServletWebServerFactory, throw NoSuchBeanDefinitionException lúc startup. Maven build vẫn pass vì Maven không kiểm tra ràng buộc runtime này.
Q5Team bạn có project với acme-parent (corporate parent). Developer mới thêm spring-boot-starter-parent làm parent thứ hai vào pom.xml. Điều gì xảy ra? Giải pháp đúng là gì?▸
acme-parent (corporate parent). Developer mới thêm spring-boot-starter-parent làm parent thứ hai vào pom.xml. Điều gì xảy ra? Giải pháp đúng là gì?Maven không cho phép hai <parent> — file sẽ fail parse ngay khi Maven đọc pom.xml: "Project has more than one parent" hoặc tương tự. Build không chạy được từ bước đọc POM.
Giải pháp đúng: giữ acme-parent làm parent, import BOM của Boot qua <dependencyManagement>:
<parent>
<groupId>com.acme</groupId>
<artifactId>acme-parent</artifactId>
<version>1.0</version>
</parent>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.4.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>Trade-off: không có Java version, encoding, plugin defaults từ spring-boot-starter-parent — phải tự config trong pom.xml hoặc acme-parent cần cung cấp. Đây là lựa chọn phổ biến trong enterprise vì corporate parent thường đã config sẵn những thứ này theo chuẩn công ty.
Bài tiếp theo: Auto-configuration deep dive — cơ chế @EnableAutoConfiguration
Bài này có giúp bạn hiểu bản chất không?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên