Spring Boot Starter & BOM — hết dependency hell
Starter là jar rỗng gom transitive dependency; BOM spring-boot-dependencies khóa version 200+ lib. Cơ chế Maven/Gradle giúp pom.xml Boot chỉ còn vài dòng.
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 — spring-boot-starter kéo theo spring-boot + spring-boot-autoconfigure + logback-classic + snakeyaml; spring-boot-starter-json kéo ba module Jackson; spring-boot-starter-tomcat kéo tomcat-embed-core + tomcat-embed-websocket; spring-webmvc kéo spring-aop + spring-context + spring-expression + 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; role của từng nhóm nằm ở bảng section 4.
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 04.
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. Vì sao tầng <dependencyManagement> lại thắng được cơ chế resolve mặc định của Maven là chủ đề của section "Cơ chế bên dưới" cuối bài.
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 04.
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.

Đâ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 04 — 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 04 — 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 02 — 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.jarvà thấy không có file.classnào. Vì sao? Cơ chế nào giúp Maven biết phải pull dependency nào? - Q2BOM
spring-boot-dependencieskhác gìspring-boot-starter-parent? Khi nào bạn buộc phải dùng BOM import thay vì<parent>? - 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>. - 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.
- Q5Team bạn có project với
acme-parent(corporate parent). Developer mới thêmspring-boot-starter-parentlàm parent thứ hai vàopom.xml. Điều gì xảy ra? Giải pháp đúng là gì?
Bài tiếp theo: Auto-configuration deep dive — cơ chế @EnableAutoConfiguration
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
Bài này có giúp bạn hiểu bản chất không?
Ôn phỏng vấn
Bài này trả lời được các câu phỏng vấn sau — tự trả lời thử trước khi mở đáp án.
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